iOS 應用開發中的斷點續傳實踐總結
斷點續傳概述
斷點續傳就是從檔案上次中斷的地方開始重新下載或上傳資料,而不是從檔案開頭。(本文的斷點續傳僅涉及下載,上傳不在討論之內)當下載大檔案的時候,如果沒有實現斷點續傳功能,那麼每次出現異常或者使用者主動的暫停,都會去重頭下載,這樣很浪費時間。所以專案中要實現大檔案下載,斷點續傳功能就必不可少了。當然,斷點續傳有一種特殊的情況,就是 iOS 應用被使用者 kill 掉或者應用 crash,要實現應用重啟之後的斷點續傳。這種特殊情況是本文要解決的問題。
斷點續傳原理
要實現斷點續傳 , 伺服器必須支援。目前最常見的是兩種方式:FTP 和 HTTP。下面來簡單介紹 HTTP 斷點續傳的原理。
HTTP
通過 HTTP,可以非常方便的實現斷點續傳。斷點續傳主要依賴於 HTTP 頭部定義的 Range 來完成。具體 Range 的說明參見 RFC2616中 14.35.2 節,在請求某範圍內的資源時,可以更有效地對大資源發出請求或從傳輸錯誤中恢復下載。有了 Range,應用可以通過 HTTP 請求曾經獲取失敗的資源的某一個返回或者是部分,來恢復下載該資源。當然並不是所有的伺服器都支援 Range,但大多數伺服器是可以的。Range 是以位元組計算的,請求的時候不必給出結尾位元組數,因為請求方並不一定知道資源的大小。Range 的定義如圖 1 所示:
圖 1. HTTP-Range
圖 2 展示了 HTTP request 的頭部資訊:
圖 2. HTTP request 例子
在上面的例子中的“Range: bytes=1208765-”表示請求資源開頭 1208765 位元組之後的部分。
圖 3 展示了 HTTP response 的頭部資訊:
圖 3. HTTP response 例子
上面例子中的”Accept-Ranges: bytes”表示伺服器端接受請求資源的某一個範圍,並允許對指定資源進行位元組型別訪問。”Content-Range: bytes 1208765-20489997/20489998”說明了返回提供了請求資源所在的原始實體內的位置,還給出了整個資源的長度。這裡需要注意的是 HTTP return code 是 206 而不是 200。
斷點續傳分析 -AFHTTPRequestOperation
瞭解了斷點續傳的原理之後,我們就可以動手來實現 iOS 應用中的斷點續傳了。由於筆者專案的資源都是部署在 HTTP 伺服器上 , 所以斷點續傳功能也是基於 HTTP 實現的。首先來看下第三方網路框架 AFNetworking 中提供的實現。清單 1 示例程式碼是用來實現斷點續傳部分的程式碼:
清單 1. 使用 AFHTTPRequestOperation 實現斷點續傳的程式碼
// 1 指定下載檔案地址 URLString // 2 獲取儲存的檔案路徑 filePath // 3 建立 NSURLRequest NSURLRequest *request = [NSURLRequest requestWithURL:[NSURL URLWithString:URLString]]; unsigned long long downloadedBytes = 0; if ([[NSFileManager defaultManager] fileExistsAtPath:filePath]) { // 3.1 若之前下載過 , 則在 HTTP 請求頭部加入 Range // 獲取已下載檔案的 size downloadedBytes = [self fileSizeForPath:filePath]; // 驗證是否下載過檔案 if (downloadedBytes > 0) { // 若下載過 , 斷點續傳的時候修改 HTTP 頭部部分的 Range NSMutableURLRequest *mutableURLRequest = [request mutableCopy]; NSString *requestRange = [NSString stringWithFormat:@"bytes=%llu-", downloadedBytes]; [mutableURLRequest setValue:requestRange forHTTPHeaderField:@"Range"]; request = mutableURLRequest; } } // 4 建立 AFHTTPRequestOperation AFHTTPRequestOperation *operation = [[AFHTTPRequestOperation alloc] initWithRequest:request]; // 5 設定操作輸出流 , 儲存在第 2 步的檔案中 operation.outputStream = [NSOutputStream outputStreamToFileAtPath:filePath append:YES]; // 6 設定下載進度處理 block [operation setDownloadProgressBlock:^(NSUInteger bytesRead, long long totalBytesRead, long long totalBytesExpectedToRead) { // bytesRead 當前讀取的位元組數 // totalBytesRead 讀取的總位元組數 , 包含斷點續傳之前的 // totalBytesExpectedToRead 檔案總大小 }]; // 7 設定 success 和 failure 處理 block [operation setCompletionBlockWithSuccess:^(AFHTTPRequestOperation *operation, id responseObject) { } failure:^(AFHTTPRequestOperation *operation, NSError *error) { }]; // 8 啟動 operation [operation start];
使用以上程式碼 , 斷點續傳功能就實現了,應用重新啟動或者出現異常情況下 , 都可以基於已經下載的部分開始繼續下載。關鍵的地方就是把已經下載的資料持久化。接下來簡單看下 AFHTTPRequestOperation 是怎麼實現的。通過檢視原始碼 , 我們發現 AFHTTPRequestOperation 繼承自 AFURLConnectionOperation , 而 AFURLConnectionOperation 實現了 NSURLConnectionDataDelegate 協議。處理流程如圖 4 所示:
圖 4. AFURLHTTPrequestOperation 處理流程
這裡 AFNetworking 為什麼採取子執行緒調非同步介面的方式 , 是因為直接在主執行緒呼叫非同步介面 , 會有一個 Runloop 的問題。當主執行緒呼叫 [[NSURLConnection alloc] initWithRequest:request delegate:self startImmediately:YES] 時 , 請求發出之後的監聽任務會加入到主執行緒的 Runloop 中 ,RunloopMode 預設為 NSDefaultRunLoopMode, 這個表示只有當前執行緒的 Runloop 處理 NSDefaultRunLoopMode 時,這個任務才會被執行。而當使用者在滾動 TableView 和 ScrollView 的時候,主執行緒的 Runloop 處於 NSEventTrackingRunLoop 模式下,就不會執行 NSDefaultRunLoopMode 的任務。
另外由於採取子執行緒呼叫介面的方式 , 所以這邊的 DownloadProgressBlock,success 和 failure Block 都需要回到主執行緒來處理。
斷點續傳實戰
瞭解了原理和 AFHTTPRequestOperation 的例子之後 , 來看下實現斷點續傳的三種方式:
NSURLConnection
基於 NSURLConnection 實現斷點續傳 , 關鍵是滿足 NSURLConnectionDataDelegate 協議,主要實現瞭如下三個方法:
清單 2. NSURLConnection 的實現
// SWIFT // 請求失敗處理 func connection(connection: NSURLConnection, didFailWithError error: NSError) { self.failureHandler(error: error) } // 接收到伺服器響應是呼叫 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 } // 當伺服器返回實體資料是呼叫 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) } } // 下載完畢後呼叫 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 所示 , 說明了 NSURLConnection 的一般處理流程。(程式碼詳見下載包)
圖 5. NSURLConnection 流程
根據圖 5 的一般流程,在 didReceiveResponse 中初始化 fileHandler, 在 didReceiveData 中 , 將接收到的資料持久化的檔案中 , 在 connectionDidFinishLoading 中,清空資料和關閉 fileHandler,並將檔案儲存到 Document 目錄下。所以當請求出現異常或應用被使用者殺掉,都可以通過持久化的中間檔案來斷點續傳。初始化 NSURLConnection 的時候要注意設定 scheduleInRunLoop 為 NSRunLoopCommonModes,不然就會出現進度條 UI 無法更新的現象。實現效果如圖 6 所示:
圖 6. NSURLConnection 演示
NSURLSessionDataTask
蘋果在 iOS7 開始,推出了一個新的類 NSURLSession, 它具備了 NSURLConnection 所具備的方法,並且更強大。由於通過 NSURLConnection 從 2015 年開始被棄用了,所以讀者推薦基於 NSURLSession 去實現續傳。NSURLConnection 和 NSURLSession delegate 方法的對映關係 , 如圖 7 所示。所以關鍵是要滿足 NSURLSessionDataDelegate 和 NSURLsessionTaskDelegate。
圖 7. 協議之間對映關係
程式碼如清單 3 所示 , 基本和 NSURLConnection 實現的一樣。
清單 3. NSURLSessionDataTask 的實現
// SWIFT // 接收資料 func URLSession(session: NSURLSession, dataTask: NSURLSessionDataTask, idReceiveData data: NSData) { //. . . } // 接收伺服器響應 func URLSession(session: NSURLSession, dataTask: NSURLSessionDataTask, didReceiveResponse response: NSURLResponse, completionHandler: (NSURLSessionResponseDisposition) -> Void) { // . . . completionHandler(.Allow) } // 請求完成 func URLSession(session: NSURLSession, task: NSURLSessionTask, didCompleteWithError error: NSError?) { if error == nil { // . . . self.successHandler(responseObject:self.fileName!) } else { self.failureHandler(error:error!) } }
區別在與 didComleteWithError, 它將 NSURLConnection 中的 connection:didFailWithError:
和 connectionDidFinishLoading: 整合到了一起 , 所以這邊要根據 error 區分執行成功的 Block 和失敗的 Block。實現效果如圖 8 所示:
圖 8. NSURLSessionDataTask 演示
NSURLSessionDownTask
最後來看下 NSURLSession 中用來下載的類 NSURLSessionDownloadTask,對應的協議是 NSURLSessionDownloadDelegate,如圖 9 所示:
圖 9. NSURLSessionDownloadDelegate 協議
其中在退出 didFinishDownloadingToURL 後,會自動刪除 temp 目錄下對應的檔案。所以有關檔案操作必須要在這個方法裡面處理。之前筆者曾想找到這個 tmp 檔案 , 基於這個檔案做斷點續傳 , 無奈一直找不到這個檔案的路徑。等以後 SWIFT 公佈 NSURLSession 的原始碼之後,興許會有方法找到。基於 NSURLSessionDownloadTask 來實現的話 , 需要在 cancelByProducingResumeData 中儲存已經下載的資料。進度通知就非常簡單了,直接在 URLSession:downloadTask:didWriteData:totalBytesWritten:totalBytesWritten:totalBytesExpectedToWrite: 實現即可。程式碼如清單 4 所示:
清單 4. NSURLSessionDownloadTask 的實現
//SWIFT //UI 觸發 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) }
實現效果如圖 10 所示:
圖 10. NSURLSessionDownloadTask 演示
總結
本文從斷點續傳概述開始,介紹了斷點續傳的應用背景,通過原理的描述,相信讀者對斷點續傳有了基本的認識和理解。接著筆者介紹了通過 AFHTTPRequestOpeartion 實現的程式碼,並對 AFHTTPRequestOpeartion 做了簡單的分析。最後筆者結合的實際需求,基於 NSURLConnection, NSURLSeesionDataTask 和 NSURLSessionDownloadtask。其實,下載的實現遠不止這些內容,本文只介紹了簡單的使用。希望在進一步的學習和應用中能繼續與大家分享。
相關文章
- iOS開發NSURLConnection 斷點續傳iOS斷點
- TypeScript 在開發應用中的實踐總結TypeScript
- 上傳——斷點續傳之實踐篇斷點
- iOS 開發之 NSURLSession 下載和斷點續傳iOSSession斷點
- 用Java實現斷點續傳(HTTP)Java斷點HTTP
- iOS大檔案斷點續傳iOS斷點
- 斷點續傳斷點
- scp實現斷點續傳---rsync斷點
- iOS11 下載之斷點續傳的bugiOS斷點
- 基於tcp的http應用,斷點續傳,範圍請求TCPHTTP斷點
- 簡單的斷點續傳斷點
- Java實現檔案斷點續傳Java斷點
- ASP.NET中的AJAX應用開發總結ASP.NET
- Oracle的RMAN總結繼續,具體實踐開始Oracle
- Android 斷點續傳Android斷點
- 小程式開發實踐總結
- OSS網頁上傳和斷點續傳(終結篇)網頁斷點
- Android中的多執行緒斷點續傳Android執行緒斷點
- 12. 斷點續傳的原理斷點
- C# 斷點續傳原理與實現C#斷點
- iOS App 瘦身實踐總結iOSAPP
- iOS程式碼實踐總結iOS
- Flutter與Native混合開發-FlutterBoost整合應用和開發實踐(iOS)FlutteriOS
- JAVA實現大檔案分片上傳斷點續傳Java斷點
- 斷點續傳教學例子斷點
- 斷點續傳更新版斷點
- 上傳——斷點續傳之理論篇斷點
- iOS 中的事件傳遞和響應機制 - 實踐篇iOS事件
- iOS 中的事件傳遞和響應機制 – 實踐篇iOS事件
- OkHttp使用+檔案的上傳+斷點續傳HTTP斷點
- Taro實踐 - 深度開發實踐體驗及總結
- 使用Visual C#實現斷點續傳C#斷點
- HTTP檔案斷點續傳的原理HTTP斷點
- 類簇在iOS開發中的應用iOS
- java開發一個應用的總結Java
- C#開發一應用的總結C#
- Git斷點續傳和離線增量更新的實現Git斷點
- Android 中 Service+Notification 斷點續傳下載Android斷點