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处理
  • 调用外界传递进来的闭包,回传相应的数据给外界
相关文章
相关标签/搜索