總結(jié)iOS開發(fā)中的斷點續(xù)傳與實踐
前言
斷點續(xù)傳概述
斷點續(xù)傳就是從文件上次中斷的地方開始重新下載或上傳數(shù)據(jù),而不是從文件開頭。(本文的斷點續(xù)傳僅涉及下載,上傳不在討論之內(nèi))當(dāng)下載大文件的時候,如果沒有實現(xiàn)斷點續(xù)傳功能,那么每次出現(xiàn)異?;蛘哂脩糁鲃拥臅和?,都會去重頭下載,這樣很浪費時間。所以項目中要實現(xiàn)大文件下載,斷點續(xù)傳功能就必不可少了。當(dāng)然,斷點續(xù)傳有一種特殊的情況,就是 iOS 應(yīng)用被用戶 kill 掉或者應(yīng)用 crash,要實現(xiàn)應(yīng)用重啟之后的斷點續(xù)傳。這種特殊情況是本文要解決的問題。
斷點續(xù)傳原理
要實現(xiàn)斷點續(xù)傳 , 服務(wù)器必須支持。目前最常見的是兩種方式:FTP 和 HTTP。
下面來簡單介紹 HTTP 斷點續(xù)傳的原理。
HTTP
通過 HTTP,可以非常方便的實現(xiàn)斷點續(xù)傳。斷點續(xù)傳主要依賴于 HTTP 頭部定義的 Range 來完成。在請求某范圍內(nèi)的資源時,可以更有效地對大資源發(fā)出請求或從傳輸錯誤中恢復(fù)下載。有了 Range,應(yīng)用可以通過 HTTP 請求曾經(jīng)獲取失敗的資源的某一個返回或者是部分,來恢復(fù)下載該資源。當(dāng)然并不是所有的服務(wù)器都支持 Range,但大多數(shù)服務(wù)器是可以的。Range 是以字節(jié)計算的,請求的時候不必給出結(jié)尾字節(jié)數(shù),因為請求方并不一定知道資源的大小。
Range 的定義如圖 1 所示:
圖 1. HTTP-Range

圖 2 展示了 HTTP request 的頭部信息:
圖 2. HTTP request 例子

在上面的例子中的“Range: bytes=1208765-”表示請求資源開頭 1208765 字節(jié)之后的部分。
圖 3 展示了 HTTP response 的頭部信息:
圖 3. HTTP response 例子

上面例子中的”Accept-Ranges: bytes”表示服務(wù)器端接受請求資源的某一個范圍,并允許對指定資源進行字節(jié)類型訪問?!?code>Content-Range: bytes 1208765-20489997/20489998”說明了返回提供了請求資源所在的原始實體內(nèi)的位置,還給出了整個資源的長度。這里需要注意的是 HTTP return code 是 206 而不是 200。
斷點續(xù)傳分析 -AFHTTPRequestOperation
了解了斷點續(xù)傳的原理之后,我們就可以動手來實現(xiàn) iOS 應(yīng)用中的斷點續(xù)傳了。由于筆者項目的資源都是部署在 HTTP 服務(wù)器上 , 所以斷點續(xù)傳功能也是基于 HTTP 實現(xiàn)的。首先來看下第三方網(wǎng)絡(luò)框架 AFNetworking 中提供的實現(xiàn)。清單 1 示例代碼是用來實現(xiàn)斷點續(xù)傳部分的代碼:
清單 1. 使用 AFHTTPRequestOperation 實現(xià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 若之前下載過 , 則在 HTTP 請求頭部加入 Range
// 獲取已下載文件的 size
downloadedBytes = [self fileSizeForPath:filePath];
// 驗證是否下載過文件
if (downloadedBytes > 0) {
// 若下載過 , 斷點續(xù)傳的時候修改 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è)置下載進度處理 block
[operation setDownloadProgressBlock:^(NSUInteger bytesRead,
long long totalBytesRead, long long totalBytesExpectedToRead) {
// bytesRead 當(dāng)前讀取的字節(jié)數(shù)
// totalBytesRead 讀取的總字節(jié)數(shù) , 包含斷點續(xù)傳之前的
// totalBytesExpectedToRead 文件總大小
}];
// 7 設(shè)置 success 和 failure 處理 block
[operation setCompletionBlockWithSuccess:^(AFHTTPRequestOperation
*operation, id responseObject) {
} failure:^(AFHTTPRequestOperation *operation, NSError *error) {
}];
// 8 啟動 operation
[operation start];
使用以上代碼 , 斷點續(xù)傳功能就實現(xiàn)了,應(yīng)用重新啟動或者出現(xiàn)異常情況下 , 都可以基于已經(jīng)下載的部分開始繼續(xù)下載。關(guān)鍵的地方就是把已經(jīng)下載的數(shù)據(jù)持久化。接下來簡單看下 AFHTTPRequestOperation 是怎么實現(xiàn)的。通過查看源碼 , 我們發(fā)現(xiàn) AFHTTPRequestOperation 繼承自 AFURLConnectionOperation , 而 AFURLConnectionOperation 實現(xiàn)了 NSURLConnectionDataDelegate 協(xié)議。
處理流程如圖 4 所示:
圖 4. AFURLHTTPrequestOperation 處理流程

這里 AFNetworking 為什么采取子線程調(diào)異步接口的方式 , 是因為直接在主線程調(diào)用異步接口 , 會有一個 Runloop 的問題。當(dāng)主線程調(diào)用 [[NSURLConnection alloc] initWithRequest:request delegate:self startImmediately:YES] 時 , 請求發(fā)出之后的監(jiān)聽任務(wù)會加入到主線程的 Runloop 中 ,RunloopMode 默認(rèn)為 NSDefaultRunLoopMode, 這個表示只有當(dāng)前線程的 Runloop 處理 NSDefaultRunLoopMode 時,這個任務(wù)才會被執(zhí)行。而當(dāng)用戶在滾動 TableView 和 ScrollView 的時候,主線程的 Runloop 處于 NSEventTrackingRunLoop 模式下,就不會執(zhí)行 NSDefaultRunLoopMode 的任務(wù)。
另外由于采取子線程調(diào)用接口的方式 , 所以這邊的 DownloadProgressBlock,success 和 failure Block 都需要回到主線程來處理。
斷點續(xù)傳實戰(zhàn)
了解了原理和 AFHTTPRequestOperation 的例子之后 , 來看下實現(xiàn)斷點續(xù)傳的三種方式:
NSURLConnection
基于 NSURLConnection 實現(xiàn)斷點續(xù)傳 , 關(guān)鍵是滿足 NSURLConnectionDataDelegate 協(xié)議,主要實現(xiàn)了如下三個方法:
清單 2. NSURLConnection 的實現(xiàn)
// SWIFT
// 請求失敗處理
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ù)據(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 所示 , 說明了 NSURLConnection 的一般處理流程。
圖 5. NSURLConnection 流程

根據(jù)圖 5 的一般流程,在 didReceiveResponse 中初始化 fileHandler, 在 didReceiveData 中 , 將接收到的數(shù)據(jù)持久化的文件中 , 在 connectionDidFinishLoading 中,清空數(shù)據(jù)和關(guān)閉 fileHandler,并將文件保存到 Document 目錄下。所以當(dāng)請求出現(xiàn)異?;驊?yīng)用被用戶殺掉,都可以通過持久化的中間文件來斷點續(xù)傳。初始化 NSURLConnection 的時候要注意設(shè)置 scheduleInRunLoop 為 NSRunLoopCommonModes,不然就會出現(xiàn)進度條 UI 無法更新的現(xiàn)象。
實現(xiàn)效果如圖 6 所示:
圖 6. NSURLConnection 演示

NSURLSessionDataTask
蘋果在 iOS7 開始,推出了一個新的類 NSURLSession, 它具備了 NSURLConnection 所具備的方法,并且更強大。由于通過 NSURLConnection 從 2015 年開始被棄用了,所以讀者推薦基于 NSURLSession 去實現(xiàn)續(xù)傳。NSURLConnection 和 NSURLSession delegate 方法的映射關(guān)系 , 如圖 7 所示。所以關(guān)鍵是要滿足 NSURLSessionDataDelegate 和 NSURLsessionTaskDelegate。
圖 7. 協(xié)議之間映射關(guān)系

代碼如清單 3 所示 , 基本和 NSURLConnection 實現(xiàn)的一樣。
清單 3. NSURLSessionDataTask 的實現(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)
}
// 請求完成
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。
實現(xiàn)效果如圖 8 所示:
圖 8. NSURLSessionDataTask 演示

NSURLSessionDownTask
最后來看下 NSURLSession 中用來下載的類 NSURLSessionDownloadTask,對應(yīng)的協(xié)議是 NSURLSessionDownloadDelegate,如圖 9 所示:
圖 9. NSURLSessionDownloadDelegate 協(xié)議

其中在退出 didFinishDownloadingToURL 后,會自動刪除 temp 目錄下對應(yīng)的文件。所以有關(guān)文件操作必須要在這個方法里面處理。之前筆者曾想找到這個 tmp 文件 , 基于這個文件做斷點續(xù)傳 , 無奈一直找不到這個文件的路徑。等以后 SWIFT 公布 NSURLSession 的源碼之后,興許會有方法找到?;?NSURLSessionDownloadTask 來實現(xiàn)的話 , 需要在 cancelByProducingResumeData 中保存已經(jīng)下載的數(shù)據(jù)。進度通知就非常簡單了,直接在 URLSession:downloadTask:didWriteData:totalBytesWritten:totalBytesWritten:totalBytesExpectedToWrite: 實現(xiàn)即可。
代碼如清單 4 所示:
清單 4. NSURLSessionDownloadTask 的實現(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)
}
實現(xiàn)效果如圖 10 所示:
圖 10. NSURLSessionDownloadTask 演示

總結(jié)
以上就是本文總結(jié)iOS開發(fā)中的斷點續(xù)傳與實踐的全部內(nèi)容,其實,下載的實現(xiàn)遠不止這些內(nèi)容,本文只介紹了簡單的使用。希望在進一步的學(xué)習(xí)和應(yīng)用中能繼續(xù)與大家分享。希望本文能幫助到有需要的大家。
相關(guān)文章
iOS開發(fā)學(xué)習(xí) ViewController使用示例詳解
這篇文章主要為大家介紹了iOS開發(fā)學(xué)習(xí) ViewController使用示例詳解,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進步,早日升職加薪2022-10-10
iOS Xcode升級Xcode15報錯SDK does not contain
這篇文章主要為大家介紹了iOS Xcode 升級Xcode15報錯: SDK does not contain 'libarclite'解決,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進步,早日升職加薪2023-11-11
iOS開發(fā)存儲應(yīng)用程序Info.plist知識全面詳解
這篇文章主要為大家介紹了iOS開發(fā)存儲應(yīng)用程序Info.plist知識全面詳解,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進步,早日升職加薪2023-06-06

