Alamofire(六) 斷點續傳

前言

Alamofire中,還有一個斷點續傳的重要功能。swift

開始下載

首先封裝一個DLDowloadManager,便於處理緩存

class DLDowloadManager: NSObject {
    static let shared = DLDowloadManager()
    var currentDownloadRequest: DownloadRequest?
    var resumeData: Data?
    var downloadTasks: Array<URLSessionDownloadTask>?
    var filePath: URL{
        return FileManager.default.urls(for: .documentDirectory, in: FileManager.SearchPathDomainMask.userDomainMask).first!.appendingPathComponent("com.download.denglei.cn")
    }

   //單例方便獲取
    let manager: SessionManager = {
        let configuration = URLSessionConfiguration.background(withIdentifier: "com.denglei.AlamofireDowload")
        configuration.httpAdditionalHeaders = SessionManager.defaultHTTPHeaders
        configuration.sharedContainerIdentifier = "group.com.denglei.AlamofireDowload"
        let manager = SessionManager(configuration: configuration)
        manager.startRequestsImmediately = true
    }
}
複製代碼

這裏封裝一個download方法,專門用來處理下載及斷點續傳的功能bash

func download(_ url: URLConvertible) -> DownloadRequest {
            //沒有緩存直接下載
    currentDownloadRequest = DLDowloadManager.shared.manager.download(url) { [weak self](url, reponse) -> (destinationURL: URL, options: DownloadRequest.DownloadOptions) in
                    let fileUrl = self?.filePath.appendingPathComponent(reponse.suggestedFilename!)
                    return (fileUrl!,[.removePreviousFile, .createIntermediateDirectories] )

    return currentDownloadRequest!
    }
複製代碼

暫停下載和繼續下載及取消任務

暫停下載session

func suspend() {
        self.currentDownloadRequest?.suspend()
    }
複製代碼

父類DownloadRequest裏的suspend方法,經過調用task.suspend來暫停下載任務閉包

open func suspend() {
        guard let task = task else { return }

        task.suspend()

        NotificationCenter.default.post(
            name: Notification.Name.Task.DidSuspend,
            object: self,
            userInfo: [Notification.Key.Task: task]
        )
    }
複製代碼

繼續下載app

func resume() {
        self.currentDownloadRequest?.resume()
    }
複製代碼

父類DownloadRequest裏的resume方法,還是ide

open func resume() {
        guard let task = task else { delegate.queue.isSuspended = false ; return }

        if startTime == nil { startTime = CFAbsoluteTimeGetCurrent() }

        task.resume()

        NotificationCenter.default.post(
            name: Notification.Name.Task.DidResume,
            object: self,
            userInfo: [Notification.Key.Task: task]
        )
    }
複製代碼

取消下載post

func cancel() {
        self.currentDownloadRequest?.cancel()
    }
複製代碼

父類DownloadRequest裏的cancel方法,這裏還有一步額外的操做,它保存了當前下載的resumeData,便於下次恢復下載ui

open override func cancel() {
        downloadDelegate.downloadTask.cancel { self.downloadDelegate.resumeData = $0 }

        NotificationCenter.default.post(
            name: Notification.Name.Task.DidCancel,
            object: self,
            userInfo: [Notification.Key.Task: task as Any]
        )
    }
複製代碼

那麼咱們在取消任務後,再次開始下載,須要判斷task裏的resumeData是否存在,若是存在則繼續上次下載,修改後的download方法url

func download(_ url: URLConvertible) -> DownloadRequest {
      //取消任務後繼續下載
    if let resumeData = DLDowloadManager.shared.currentDownloadRequest?.resumeData {
                currentDownloadRequest = DLDowloadManager.shared.manager.download(resumingWith: resumeData)
            }else{
            //沒有緩存直接下載
                currentDownloadRequest = DLDowloadManager.shared.manager.download(url) { [weak self](url, reponse) -> (destinationURL: URL, options: DownloadRequest.DownloadOptions) in
                    let fileUrl = self?.filePath.appendingPathComponent(reponse.suggestedFilename!)
                    return (fileUrl!,[.removePreviousFile, .createIntermediateDirectories] )
                }
            }
     return currentDownloadRequest!
 }
複製代碼

特殊狀況處理

前面寫的是正常使用時的斷點續傳功能,還有用戶主動殺死APP 和 APP出現崩潰異常退出的狀況須要處理。

用戶主動殺死APP

  • 首先猜測,若是用戶以前主動殺死APP,那麼在第二次打開後會不會走APP裏的某些代理方法呢?

  • 在以前對request的解析過程當中,知道全部系統的代理回調都會來到SessionDelegate裏的代理裏,因而在Download有關的代理方法裏都打上斷點。

  • 再次用Xcode運行APP後,發現didCompleteWithError裏的斷點被執行了,說明會運行到這裏

  • 在上面的代碼裏,strongSelf.taskDidComplete?(session, task, error),咱們發現會先執行當前的代理taskDidComplete

  • 在外界DownloadManager裏監聽一下這個taskDidComplete,把裏面的resumeData保存起來便可

// 用戶kill 進來
        manager.delegate.taskDidComplete = { (seesion,task, error) in
            if let error = error {
                print("taskDidComplete的error狀況: \(error)")
                if let resumeData = (error as NSError).userInfo[NSURLSessionDownloadTaskResumeData] as? Data {
                    // resumeData 存儲
                    DLDowloadManager.shared.resumeData = resumeData
                    print("來了")
                }
            }else{
                print("taskDidComplete的task狀況: \(task)")
            }
        }
複製代碼
  • 再次修改download方法,多加一種判斷來處理這種狀況
func download(_ url: URLConvertible) -> DownloadRequest {
        //處理用戶主動殺死APP的狀況
        if self.resumeData != nil {
            currentDownloadRequest = DLDowloadManager.shared.manager.download(resumingWith: self.resumeData!)
        }else{
        //取消任務後繼續下載
            if let resumeData = DLDowloadManager.shared.currentDownloadRequest?.resumeData {
                currentDownloadRequest = DLDowloadManager.shared.manager.download(resumingWith: resumeData)
            }else{
            //沒有緩存直接下載
                currentDownloadRequest = DLDowloadManager.shared.manager.download(url) { [weak self](url, reponse) -> (destinationURL: URL, options: DownloadRequest.DownloadOptions) in
                    let fileUrl = self?.filePath.appendingPathComponent(reponse.suggestedFilename!)
                    return (fileUrl!,[.removePreviousFile, .createIntermediateDirectories] )
                }
            }
        }
        return currentDownloadRequest!
    }
複製代碼

APP崩潰異常退出

在VC中主動製造一個崩潰的異常

override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
        let array = [1]
        print(array[2])
    }
複製代碼
  • 而後再次打開APP,等待一會發現居然再次來到了上面說的回調方法didCompleteWithError,發現error是空的,手動去找文件的目錄發現文件也是正常下載完成的

  • 說明在APP上次崩潰退出後,再次進來會開啓一個後臺下載任務,繼續下載上次崩潰的任務,直到下載完成

  • 因此咱們在taskDidComplete也能監聽到下載完成, 會和用戶主動殺死APP最後來到同一個回調,能夠一塊兒監聽

// 用戶kill 進來
        manager.delegate.taskDidComplete = { (seesion,task, error) in
            if let error = error {
                print("taskDidComplete的error狀況: \(error)")
                if let resumeData = (error as NSError).userInfo[NSURLSessionDownloadTaskResumeData] as? Data {
                    // resumeData 存儲
                    DLDowloadManager.shared.resumeData = resumeData
                    print("來了")
                }
            }else{
                print("taskDidComplete的task狀況: \(task)")
            }
        }
複製代碼
  • 一樣的在downloadTaskDidFinishDownloadingToURL裏也能監聽到下載完成的回調內容,在這裏能夠根據不一樣的下載完成的url作一些處理
manager.delegate.downloadTaskDidFinishDownloadingToURL = { (session, downloadTask, url) in
        }
複製代碼
  • 還有一個問題,咱們在這個後臺下載任務裏如何監聽下載進度,並更新到UI上呢?

  • 因爲會走下載完成的代理方法,那麼確定也會走正在下載的代理方法didWriteData

  • 在這個代理方法裏能夠拿到本次下載的字節bytesWritten,一共已經下載的字節totalBytesWritten和一共須要下載的字節totalBytesExpectedToWrite

  • 因此咱們在DLDowloadManager裏可以經過這個代理方法獲取到下載的進度,利用這裏的數據而後再在外界更新UI便可

manager.delegate.downloadTaskDidWriteData = {(session, downloadTask, bytesWritten, totalBytesWritten, totalBytesExpectedToWrite) in
            
        }
複製代碼

總結

在實現斷點續傳功能的整個過程當中,再一次感覺到了Alamofire的結構和流程

  • 外界傳遞須要處理的的閉包給SessionDelegate
  • 開啓DownloadTask任務
  • 經過SessionDelegate接收系統URLSession的下載回調
  • 再下發給具體的DownloadTaskDelegate處理
  • 調用外界傳遞進來的閉包,回傳相應的數據給外界
相關文章
相關標籤/搜索