Alamofire(六) 断点续传

1,834 阅读4分钟

前言

Alamofire中,还有一个断点续传的重要功能。

开始下载

首先封装一个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方法,专门用来处理下载及断点续传的功能

    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!
    }

暂停下载和继续下载及取消任务

暂停下载

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]
        )
    }

继续下载

func resume() {
        self.currentDownloadRequest?.resume()
    }

父类DownloadRequest里的resume方法,仍是

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]
        )
    }

取消下载

  func cancel() {
        self.currentDownloadRequest?.cancel()
    }

父类DownloadRequest里的cancel方法,这里还有一步额外的操作,它保存了当前下载的resumeData,便于下次恢复下载

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方法

 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处理
  • 调用外界传递进来的闭包,回传相应的数据给外界