你好,歡迎來到IOS教程網

 Ios教程網 >> IOS編程開發 >> IOS開發基礎 >> iOS 應用開發中的斷點續傳實踐總結

iOS 應用開發中的斷點續傳實踐總結

編輯:IOS開發基礎

斷點續傳概述

斷點續傳就是從文件上次中斷的地方開始重新下載或上傳數據,而不是從文件開頭。(本文的斷點續傳僅涉及下載,上傳不在討論之內)當下載大文件的時候,如果沒有實現斷點續傳功能,那麼每次出現異常或者用戶主動的暫停,都會去重頭下載,這樣很浪費時間。所以項目中要實現大文件下載,斷點續傳功能就必不可少了。當然,斷點續傳有一種特殊的情況,就是 iOS 應用被用戶 kill 掉或者應用 crash,要實現應用重啟之後的斷點續傳。這種特殊情況是本文要解決的問題。

斷點續傳原理

要實現斷點續傳 , 服務器必須支持。目前最常見的是兩種方式:FTP 和 HTTP。下面來簡單介紹 HTTP 斷點續傳的原理。

HTTP

通過 HTTP,可以非常方便的實現斷點續傳。斷點續傳主要依賴於 HTTP 頭部定義的 Range 來完成。具體 Range 的說明參見 RFC2616中 14.35.2 節,在請求某范圍內的資源時,可以更有效地對大資源發出請求或從傳輸錯誤中恢復下載。有了 Range,應用可以通過 HTTP 請求曾經獲取失敗的資源的某一個返回或者是部分,來恢復下載該資源。當然並不是所有的服務器都支持 Range,但大多數服務器是可以的。Range 是以字節計算的,請求的時候不必給出結尾字節數,因為請求方並不一定知道資源的大小。Range 的定義如圖 1 所示:

image001.png

圖 1. HTTP-Range

HTTP-Range

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

image002.png

圖 2. HTTP request 例子

HTTP request 例子

在上面的例子中的“Range: bytes=1208765-”表示請求資源開頭 1208765 字節之後的部分。

圖 3 展示了 HTTP response 的頭部信息:

image003.png

圖 3. HTTP response 例子

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 所示:

image004.png

圖 4. AFURLHTTPrequestOperation 處理流程

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 的一般處理流程。(代碼詳見下載包)

image005.png

圖 5. NSURLConnection 流程

NSURLConnection 流程

根據圖 5 的一般流程,在 didReceiveResponse 中初始化 fileHandler, 在 didReceiveData 中 , 將接收到的數據持久化的文件中 , 在 connectionDidFinishLoading 中,清空數據和關閉 fileHandler,並將文件保存到 Document 目錄下。所以當請求出現異常或應用被用戶殺掉,都可以通過持久化的中間文件來斷點續傳。初始化 NSURLConnection 的時候要注意設置 scheduleInRunLoop 為 NSRunLoopCommonModes,不然就會出現進度條 UI 無法更新的現象。實現效果如圖 6 所示:

image006.gif

圖 6. NSURLConnection 演示

NSURLSessionDataTask

蘋果在 iOS7 開始,推出了一個新的類 NSURLSession, 它具備了 NSURLConnection 所具備的方法,並且更強大。由於通過 NSURLConnection 從 2015 年開始被棄用了,所以讀者推薦基於 NSURLSession 去實現續傳。NSURLConnection 和 NSURLSession delegate 方法的映射關系 , 如圖 7 所示。所以關鍵是要滿足 NSURLSessionDataDelegate 和 NSURLsessionTaskDelegate。

image007.png

圖 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 所示:

image008.gif

圖 8. NSURLSessionDataTask 演示

NSURLSessionDownTask

最後來看下 NSURLSession 中用來下載的類 NSURLSessionDownloadTask,對應的協議是 NSURLSessionDownloadDelegate,如圖 9 所示:

image009.png

圖 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 所示:

image010.gif

NSURLSessionDownloadTask 演示

總結

本文從斷點續傳概述開始,介紹了斷點續傳的應用背景,通過原理的描述,相信讀者對斷點續傳有了基本的認識和理解。接著筆者介紹了通過 AFHTTPRequestOpeartion 實現的代碼,並對 AFHTTPRequestOpeartion 做了簡單的分析。最後筆者結合的實際需求,基於 NSURLConnection, NSURLSeesionDataTask 和 NSURLSessionDownloadtask。其實,下載的實現遠不止這些內容,本文只介紹了簡單的使用。希望在進一步的學習和應用中能繼續與大家分享。

  1. 上一頁:
  2. 下一頁:
蘋果刷機越獄教程| IOS教程問題解答| IOS技巧綜合| IOS7技巧| IOS8教程
Copyright © Ios教程網 All Rights Reserved