總結(jié)iOS開(kāi)發(fā)中的斷點(diǎn)續(xù)傳與實(shí)踐
前言
斷點(diǎn)續(xù)傳概述
斷點(diǎn)續(xù)傳就是從文件上次中斷的地方開(kāi)始重新下載或上傳數(shù)據(jù),而不是從文件開(kāi)頭。(本文的斷點(diǎn)續(xù)傳僅涉及下載,上傳不在討論之內(nèi))當(dāng)下載大文件的時(shí)候,如果沒(méi)有實(shí)現(xiàn)斷點(diǎn)續(xù)傳功能,那么每次出現(xiàn)異常或者用戶(hù)主動(dòng)的暫停,都會(huì)去重頭下載,這樣很浪費(fèi)時(shí)間。所以項(xiàng)目中要實(shí)現(xiàn)大文件下載,斷點(diǎn)續(xù)傳功能就必不可少了。當(dāng)然,斷點(diǎn)續(xù)傳有一種特殊的情況,就是 iOS 應(yīng)用被用戶(hù) kill 掉或者應(yīng)用 crash,要實(shí)現(xiàn)應(yīng)用重啟之后的斷點(diǎn)續(xù)傳。這種特殊情況是本文要解決的問(wèn)題。
斷點(diǎn)續(xù)傳原理
要實(shí)現(xiàn)斷點(diǎn)續(xù)傳 , 服務(wù)器必須支持。目前最常見(jiàn)的是兩種方式:FTP 和 HTTP。
下面來(lái)簡(jiǎn)單介紹 HTTP 斷點(diǎn)續(xù)傳的原理。
HTTP
通過(guò) HTTP,可以非常方便的實(shí)現(xiàn)斷點(diǎn)續(xù)傳。斷點(diǎn)續(xù)傳主要依賴(lài)于 HTTP 頭部定義的 Range 來(lái)完成。在請(qǐng)求某范圍內(nèi)的資源時(shí),可以更有效地對(duì)大資源發(fā)出請(qǐng)求或從傳輸錯(cuò)誤中恢復(fù)下載。有了 Range,應(yīng)用可以通過(guò) HTTP 請(qǐng)求曾經(jīng)獲取失敗的資源的某一個(gè)返回或者是部分,來(lái)恢復(fù)下載該資源。當(dāng)然并不是所有的服務(wù)器都支持 Range,但大多數(shù)服務(wù)器是可以的。Range 是以字節(jié)計(jì)算的,請(qǐng)求的時(shí)候不必給出結(jié)尾字節(jié)數(shù),因?yàn)檎?qǐng)求方并不一定知道資源的大小。
Range 的定義如圖 1 所示:
圖 1. HTTP-Range
圖 2 展示了 HTTP request 的頭部信息:
圖 2. HTTP request 例子
在上面的例子中的“Range: bytes=1208765-”表示請(qǐng)求資源開(kāi)頭 1208765 字節(jié)之后的部分。
圖 3 展示了 HTTP response 的頭部信息:
圖 3. HTTP response 例子
上面例子中的”Accept-Ranges: bytes”表示服務(wù)器端接受請(qǐng)求資源的某一個(gè)范圍,并允許對(duì)指定資源進(jìn)行字節(jié)類(lèi)型訪問(wèn)。”Content-Range: bytes 1208765-20489997/20489998
”說(shuō)明了返回提供了請(qǐng)求資源所在的原始實(shí)體內(nèi)的位置,還給出了整個(gè)資源的長(zhǎng)度。這里需要注意的是 HTTP return code 是 206 而不是 200。
斷點(diǎn)續(xù)傳分析 -AFHTTPRequestOperation
了解了斷點(diǎn)續(xù)傳的原理之后,我們就可以動(dòng)手來(lái)實(shí)現(xiàn) iOS 應(yīng)用中的斷點(diǎn)續(xù)傳了。由于筆者項(xiàng)目的資源都是部署在 HTTP 服務(wù)器上 , 所以斷點(diǎn)續(xù)傳功能也是基于 HTTP 實(shí)現(xiàn)的。首先來(lái)看下第三方網(wǎng)絡(luò)框架 AFNetworking 中提供的實(shí)現(xiàn)。清單 1 示例代碼是用來(lái)實(shí)現(xiàn)斷點(diǎn)續(xù)傳部分的代碼:
清單 1. 使用 AFHTTPRequestOperation 實(shí)現(xiàn)斷點(diǎn)續(xù)傳的代碼
// 1 指定下載文件地址 URLString // 2 獲取保存的文件路徑 filePath // 3 創(chuàng)建 NSURLRequest NSURLRequest *request = [NSURLRequest requestWithURL:[NSURL URLWithString:URLString]]; unsigned long long downloadedBytes = 0; if ([[NSFileManager defaultManager] fileExistsAtPath:filePath]) { // 3.1 若之前下載過(guò) , 則在 HTTP 請(qǐng)求頭部加入 Range // 獲取已下載文件的 size downloadedBytes = [self fileSizeForPath:filePath]; // 驗(yàn)證是否下載過(guò)文件 if (downloadedBytes > 0) { // 若下載過(guò) , 斷點(diǎn)續(xù)傳的時(shí)候修改 HTTP 頭部部分的 Range NSMutableURLRequest *mutableURLRequest = [request mutableCopy]; NSString *requestRange = [NSString stringWithFormat:@"bytes=%llu-", downloadedBytes]; [mutableURLRequest setValue:requestRange forHTTPHeaderField:@"Range"]; request = mutableURLRequest; } } // 4 創(chuàng)建 AFHTTPRequestOperation AFHTTPRequestOperation *operation = [[AFHTTPRequestOperation alloc] initWithRequest:request]; // 5 設(shè)置操作輸出流 , 保存在第 2 步的文件中 operation.outputStream = [NSOutputStream outputStreamToFileAtPath:filePath append:YES]; // 6 設(shè)置下載進(jìn)度處理 block [operation setDownloadProgressBlock:^(NSUInteger bytesRead, long long totalBytesRead, long long totalBytesExpectedToRead) { // bytesRead 當(dāng)前讀取的字節(jié)數(shù) // totalBytesRead 讀取的總字節(jié)數(shù) , 包含斷點(diǎn)續(xù)傳之前的 // totalBytesExpectedToRead 文件總大小 }]; // 7 設(shè)置 success 和 failure 處理 block [operation setCompletionBlockWithSuccess:^(AFHTTPRequestOperation *operation, id responseObject) { } failure:^(AFHTTPRequestOperation *operation, NSError *error) { }]; // 8 啟動(dòng) operation [operation start];
使用以上代碼 , 斷點(diǎn)續(xù)傳功能就實(shí)現(xiàn)了,應(yīng)用重新啟動(dòng)或者出現(xiàn)異常情況下 , 都可以基于已經(jīng)下載的部分開(kāi)始繼續(xù)下載。關(guān)鍵的地方就是把已經(jīng)下載的數(shù)據(jù)持久化。接下來(lái)簡(jiǎn)單看下 AFHTTPRequestOperation 是怎么實(shí)現(xiàn)的。通過(guò)查看源碼 , 我們發(fā)現(xiàn) AFHTTPRequestOperation 繼承自 AFURLConnectionOperation , 而 AFURLConnectionOperation 實(shí)現(xiàn)了 NSURLConnectionDataDelegate 協(xié)議。
處理流程如圖 4 所示:
圖 4. AFURLHTTPrequestOperation 處理流程
這里 AFNetworking 為什么采取子線程調(diào)異步接口的方式 , 是因?yàn)橹苯釉谥骶€程調(diào)用異步接口 , 會(huì)有一個(gè) Runloop 的問(wèn)題。當(dāng)主線程調(diào)用 [[NSURLConnection alloc] initWithRequest:request delegate:self startImmediately:YES]
時(shí) , 請(qǐng)求發(fā)出之后的監(jiān)聽(tīng)任務(wù)會(huì)加入到主線程的 Runloop 中 ,RunloopMode 默認(rèn)為 NSDefaultRunLoopMode, 這個(gè)表示只有當(dāng)前線程的 Runloop 處理 NSDefaultRunLoopMode 時(shí),這個(gè)任務(wù)才會(huì)被執(zhí)行。而當(dāng)用戶(hù)在滾動(dòng) TableView 和 ScrollView 的時(shí)候,主線程的 Runloop 處于 NSEventTrackingRunLoop 模式下,就不會(huì)執(zhí)行 NSDefaultRunLoopMode 的任務(wù)。
另外由于采取子線程調(diào)用接口的方式 , 所以這邊的 DownloadProgressBlock,success 和 failure Block 都需要回到主線程來(lái)處理。
斷點(diǎn)續(xù)傳實(shí)戰(zhàn)
了解了原理和 AFHTTPRequestOperation 的例子之后 , 來(lái)看下實(shí)現(xiàn)斷點(diǎn)續(xù)傳的三種方式:
NSURLConnection
基于 NSURLConnection 實(shí)現(xiàn)斷點(diǎn)續(xù)傳 , 關(guān)鍵是滿足 NSURLConnectionDataDelegate 協(xié)議,主要實(shí)現(xiàn)了如下三個(gè)方法:
清單 2. NSURLConnection 的實(shí)現(xiàn)
// SWIFT // 請(qǐng)求失敗處理 func connection(connection: NSURLConnection, didFailWithError error: NSError) { self.failureHandler(error: error) } // 接收到服務(wù)器響應(yīng)是調(diào)用 func connection(connection: NSURLConnection, didReceiveResponse response: NSURLResponse) { if self.totalLength != 0 { return } self.writeHandle = NSFileHandle(forWritingAtPath: FileManager.instance.cacheFilePath(self.fileName!)) self.totalLength = response.expectedContentLength + self.currentLength } // 當(dāng)服務(wù)器返回實(shí)體數(shù)據(jù)是調(diào)用 func connection(connection: NSURLConnection, didReceiveData data: NSData) { let length = data.length // move to the end of file self.writeHandle.seekToEndOfFile() // write data to sanbox self.writeHandle.writeData(data) // calculate data length self.currentLength = self.currentLength + length print("currentLength\(self.currentLength)-totalLength\(self.totalLength)") if (self.downloadProgressHandler != nil) { self.downloadProgressHandler(bytes: length, totalBytes: self.currentLength, totalBytesExpected: self.totalLength) } } // 下載完畢后調(diào)用 func connectionDidFinishLoading(connection: NSURLConnection) { self.currentLength = 0 self.totalLength = 0 //close write handle self.writeHandle.closeFile() self.writeHandle = nil let cacheFilePath = FileManager.instance.cacheFilePath(self.fileName!) let documenFilePath = FileManager.instance.documentFilePath(self.fileName!) do { try FileManager.instance.moveItemAtPath(cacheFilePath, toPath: documenFilePath) } catch let e as NSError { print("Error occurred when to move file: \(e)") } self.successHandler(responseObject:fileName!) }
如圖 5 所示 , 說(shuō)明了 NSURLConnection 的一般處理流程。
圖 5. NSURLConnection 流程
根據(jù)圖 5 的一般流程,在 didReceiveResponse 中初始化 fileHandler, 在 didReceiveData 中 , 將接收到的數(shù)據(jù)持久化的文件中 , 在 connectionDidFinishLoading 中,清空數(shù)據(jù)和關(guān)閉 fileHandler,并將文件保存到 Document 目錄下。所以當(dāng)請(qǐng)求出現(xiàn)異?;驊?yīng)用被用戶(hù)殺掉,都可以通過(guò)持久化的中間文件來(lái)斷點(diǎn)續(xù)傳。初始化 NSURLConnection 的時(shí)候要注意設(shè)置 scheduleInRunLoop 為 NSRunLoopCommonModes,不然就會(huì)出現(xiàn)進(jìn)度條 UI 無(wú)法更新的現(xiàn)象。
實(shí)現(xiàn)效果如圖 6 所示:
圖 6. NSURLConnection 演示
NSURLSessionDataTask
蘋(píng)果在 iOS7 開(kāi)始,推出了一個(gè)新的類(lèi) NSURLSession, 它具備了 NSURLConnection 所具備的方法,并且更強(qiáng)大。由于通過(guò) NSURLConnection 從 2015 年開(kāi)始被棄用了,所以讀者推薦基于 NSURLSession 去實(shí)現(xiàn)續(xù)傳。NSURLConnection 和 NSURLSession delegate 方法的映射關(guān)系 , 如圖 7 所示。所以關(guān)鍵是要滿足 NSURLSessionDataDelegate 和 NSURLsessionTaskDelegate。
圖 7. 協(xié)議之間映射關(guān)系
代碼如清單 3 所示 , 基本和 NSURLConnection 實(shí)現(xiàn)的一樣。
清單 3. NSURLSessionDataTask 的實(shí)現(xiàn)
// SWIFT // 接收數(shù)據(jù) func URLSession(session: NSURLSession, dataTask: NSURLSessionDataTask, idReceiveData data: NSData) { //. . . } // 接收服務(wù)器響應(yīng) func URLSession(session: NSURLSession, dataTask: NSURLSessionDataTask, didReceiveResponse response: NSURLResponse, completionHandler: (NSURLSessionResponseDisposition) -> Void) { // . . . completionHandler(.Allow) } // 請(qǐng)求完成 func URLSession(session: NSURLSession, task: NSURLSessionTask, didCompleteWithError error: NSError?) { if error == nil { // . . . self.successHandler(responseObject:self.fileName!) } else { self.failureHandler(error:error!) } }
區(qū)別在與 didComleteWithError, 它將 NSURLConnection 中的 connection:didFailWithError:
和 connectionDidFinishLoading: 整合到了一起 , 所以這邊要根據(jù) error 區(qū)分執(zhí)行成功的 Block 和失敗的 Block。
實(shí)現(xiàn)效果如圖 8 所示:
圖 8. NSURLSessionDataTask 演示
NSURLSessionDownTask
最后來(lái)看下 NSURLSession 中用來(lái)下載的類(lèi) NSURLSessionDownloadTask,對(duì)應(yīng)的協(xié)議是 NSURLSessionDownloadDelegate,如圖 9 所示:
圖 9. NSURLSessionDownloadDelegate 協(xié)議
其中在退出 didFinishDownloadingToURL 后,會(huì)自動(dòng)刪除 temp 目錄下對(duì)應(yīng)的文件。所以有關(guān)文件操作必須要在這個(gè)方法里面處理。之前筆者曾想找到這個(gè) tmp 文件 , 基于這個(gè)文件做斷點(diǎn)續(xù)傳 , 無(wú)奈一直找不到這個(gè)文件的路徑。等以后 SWIFT 公布 NSURLSession 的源碼之后,興許會(huì)有方法找到。基于 NSURLSessionDownloadTask 來(lái)實(shí)現(xiàn)的話 , 需要在 cancelByProducingResumeData 中保存已經(jīng)下載的數(shù)據(jù)。進(jìn)度通知就非常簡(jiǎn)單了,直接在 URLSession:downloadTask:didWriteData:totalBytesWritten:totalBytesWritten:totalBytesExpectedToWrite:
實(shí)現(xiàn)即可。
代碼如清單 4 所示:
清單 4. NSURLSessionDownloadTask 的實(shí)現(xiàn)
//SWIFT //UI 觸發(fā) pause func pause(){ self.downloadTask?.cancelByProducingResumeData({data -> Void in if data != nil { data!.writeToFile(FileManager.instance.cacheFilePath(self.fileName!), atomically: false) } }) self.downloadTask = nil } // MARK: - NSURLSessionDownloadDelegate func URLSession(session: NSURLSession, downloadTask: NSURLSessionDownloadTask, didWriteData bytesWritten: Int64, totalBytesWritten: Int64, totalBytesExpectedToWrite: Int64) { if (self.downloadProgressHandler != nil) { self.downloadProgressHandler(bytes: Int(bytesWritten), totalBytes: totalBytesWritten, totalBytesExpected: totalBytesExpectedToWrite) } } func URLSession(session: NSURLSession, task: NSURLSessionTask, didCompleteWithError error: NSError?) { if error != nil {//real error self.failureHandler(error:error!) } } func URLSession(session: NSURLSession, downloadTask: NSURLSessionDownloadTask, didFinishDownloadingToURL location: NSURL) { let cacheFilePath = FileManager.instance.cacheFilePath(self.fileName!) let documenFilePath = FileManager.instance.documentFilePath(self.fileName!) do { if FileManager.instance.fileExistsAtPath(cacheFilePath){ try FileManager.instance.removeItemAtPath(cacheFilePath) } try FileManager.instance.moveItemAtPath(location.path!, toPath: documenFilePath) } catch let e as NSError { print("Error occurred when to move file: \(e)") } self.successHandler(responseObject:documenFilePath) }
實(shí)現(xiàn)效果如圖 10 所示:
圖 10. NSURLSessionDownloadTask 演示
總結(jié)
以上就是本文總結(jié)iOS開(kāi)發(fā)中的斷點(diǎn)續(xù)傳與實(shí)踐的全部?jī)?nèi)容,其實(shí),下載的實(shí)現(xiàn)遠(yuǎn)不止這些內(nèi)容,本文只介紹了簡(jiǎn)單的使用。希望在進(jìn)一步的學(xué)習(xí)和應(yīng)用中能繼續(xù)與大家分享。希望本文能幫助到有需要的大家。
- iOS實(shí)現(xiàn)文件切片儲(chǔ)存并且上傳(仿斷點(diǎn)續(xù)傳機(jī)制)
- iOS11 下載之?dāng)帱c(diǎn)續(xù)傳的bug的解決方法
- iOS開(kāi)發(fā)網(wǎng)絡(luò)編程之?dāng)帱c(diǎn)續(xù)傳
- iOS NSURLSessionDownloadTask實(shí)現(xiàn)文件斷點(diǎn)下載的方法
- iOS利用AFNetworking3.0——實(shí)現(xiàn)文件斷點(diǎn)下載
- iOS開(kāi)發(fā)-實(shí)現(xiàn)大文件下載與斷點(diǎn)下載思路
- iOS開(kāi)發(fā)網(wǎng)絡(luò)篇—實(shí)現(xiàn)大文件的多線程斷點(diǎn)下載
- iOS使用NSURLConnection實(shí)現(xiàn)斷點(diǎn)續(xù)傳下載
相關(guān)文章
iOS開(kāi)發(fā)學(xué)習(xí) ViewController使用示例詳解
這篇文章主要為大家介紹了iOS開(kāi)發(fā)學(xué)習(xí) ViewController使用示例詳解,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2022-10-10iOS Xcode升級(jí)Xcode15報(bào)錯(cuò)SDK does not contain
這篇文章主要為大家介紹了iOS Xcode 升級(jí)Xcode15報(bào)錯(cuò): SDK does not contain 'libarclite'解決,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2023-11-11iOS高仿微信相冊(cè)界面翻轉(zhuǎn)過(guò)渡動(dòng)畫(huà)效果
在圖片界面點(diǎn)擊右下角的查看評(píng)論會(huì)翻轉(zhuǎn)到評(píng)論界面,評(píng)論界面點(diǎn)擊左上角的返回按鈕會(huì)反方向翻轉(zhuǎn)回圖片界面,真正的實(shí)現(xiàn)方法,與傳統(tǒng)的導(dǎo)航欄過(guò)渡其實(shí)只有一行代碼的區(qū)別,下面小編通過(guò)本文給大家介紹下ios高仿微信相冊(cè)界面翻轉(zhuǎn)過(guò)渡動(dòng)畫(huà)效果,一起看看吧2016-11-11IOS開(kāi)發(fā)中NSURL的基本操作及用法詳解
NSURL其實(shí)就是我們?cè)跒g覽器上看到的網(wǎng)站地址,這不就是一個(gè)字符串么,為什么還要在寫(xiě)一個(gè)NSURL呢,主要是因?yàn)榫W(wǎng)站地址的字符串都比較復(fù)雜,包括很多請(qǐng)求參數(shù),這樣在請(qǐng)求過(guò)程中需要解析出來(lái)每個(gè)部門(mén),所以封裝一個(gè)NSURL,操作很方便2015-12-12Objective-C實(shí)現(xiàn)身份證驗(yàn)證的方法示例
這篇文章主要給大家分享了Objective-C實(shí)現(xiàn)身份證驗(yàn)證的方法,文中給出了詳細(xì)的示例代碼,對(duì)大家具有一定的參考價(jià)值,需要的朋友們下面來(lái)一起看看吧。2017-03-03iOS開(kāi)發(fā)存儲(chǔ)應(yīng)用程序Info.plist知識(shí)全面詳解
這篇文章主要為大家介紹了iOS開(kāi)發(fā)存儲(chǔ)應(yīng)用程序Info.plist知識(shí)全面詳解,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2023-06-06