Swift--URLsession后台下载

前言

URLSession是一个能够响应发送或者接受HTTP请求的关键类。首先使用全局的 URLSession.shareddownloadTask 来建立一个简单的下载任务:api

let url = URL(string: "https://mobileappsuat.pwchk.com/MobileAppsManage/UploadFiles/20190719144725271.png")
let request = URLRequest(url: url!)
let session = URLSession.shared
let downloadTask = session.downloadTask(with: request,
       completionHandler: { (location:URL?, response:URLResponse?, error:Error?)
        -> Void in
        print("location:\(location)")
        let locationPath = location!.path
        let documnets:String = NSHomeDirectory() + "/Documents/1.png"
        let fileManager = FileManager.default
        try! fileManager.moveItem(atPath: locationPath, toPath: documnets)
        print("new location:\(documnets)")
    })
downloadTask.resume()
复制代码

能够看到这里的下载是前台下载,也就是说若是程序退到后台(好比按下 home 键、或者切到其它应用程序上),当前的下载任务便会马上中止,这个样话对于一些较大的文件,下载过程当中用户没法切换到后台,对用户来讲是一种不太友好的体验。下面来看一下在后台下载的具体实现:缓存

URLsession后台下载

咱们能够经过URLSessionConfiguration类新建URLSession实例,而URLSessionConfiguration这个类是有三种模式的: bash

URLSessionConfiguration 的三种模以下式:session

  • default:默认会话模式(使用的是基于磁盘缓存的持久化策略,一般使用最多的也是这种模式,在default模式下系统会建立一个持久化的缓存并在用户的钥匙串中存储证书)
  • ephemeral:暂时会话模式(该模式不使用磁盘保存任何数据。而是保存在 RAM 中,全部内容的生命周期都与session相同,所以当session会话无效时,这些缓存的数据就会被自动清空。)
  • background:后台会话模式(该模式能够在后台完成上传和下载。)

注意:background模式与default模式很是类似,不过background模式会用一个独立线程来进行数据传输。background模式能够在程序挂起,退出,崩溃的状况下运行task。也能够在APP下次启动的时候,利用标识符来恢复下载。闭包

下面先来建立一个后台下载的任务background session,而且指定一个 identifierapp

let urlstring = URL(string: "https://dldir1.qq.com/qqfile/QQforMac/QQ_V6.5.5.dmg")!

// 第一步:初始化一个background后台模式的会话配置configuration
let configuration = URLSessionConfiguration.background(withIdentifier: "com.Henry.cn")
 
// 第二步:根据配置的configuration,初始化一个session会话
let session = URLSession.init(configuration: configuration, delegate: self, delegateQueue: OperationQueue.main)

// 第三步:传入URL,建立downloadTask下载任务,开始下载
session.downloadTask(with: url).resume()
复制代码

接下来实现session的下载代理URLSessionDownloadDelegateURLSessionDelegate的方法:async

extension ViewController:URLSessionDownloadDelegate{
    // 下载代理方法,下载结束
    func urlSession(_ session: URLSession, downloadTask: URLSessionDownloadTask, didFinishDownloadingTo location: URL) {
        // 下载完成 - 开始沙盒迁移
        print("下载完成地址 - \(location)")
        let locationPath = location.path
        //拷贝到用户目录
        let documnets = NSHomeDirectory() + "/Documents/" + "com.Henry.cn" + ".dmg"
        print("移动到新地址:\(documnets)")
        //建立文件管理器
        let fileManager = FileManager.default
        try! fileManager.moveItem(atPath: locationPath, toPath: documnets)

    }
    //下载代理方法,监听下载进度
    func urlSession(_ session: URLSession, downloadTask: URLSessionDownloadTask, didWriteData bytesWritten: Int64, totalBytesWritten: Int64, totalBytesExpectedToWrite: Int64) {
        print(" bytesWritten \(bytesWritten)\n totalBytesWritten \(totalBytesWritten)\n totalBytesExpectedToWrite \(totalBytesExpectedToWrite)")
        print("下载进度: \(Double(totalBytesWritten)/Double(totalBytesExpectedToWrite))\n")
    }
}
复制代码

设置完这些代码以后,还不能达到后台下载的目的,还须要在AppDelegate中开启后台下载的权限,实现handleEventsForBackgroundURLSession方法:ide

class AppDelegate: UIResponder, UIApplicationDelegate {
    var window: UIWindow?
    //用于保存后台下载的completionHandler
    var backgroundSessionCompletionHandler: (() -> Void)?
    func application(_ application: UIApplication, handleEventsForBackgroundURLSession identifier: String, completionHandler: @escaping () -> Void) {
        self.backgroundSessionCompletionHandler = completionHandler
    }
}
复制代码

实现到这里已基本实现了后台下载的功能,在应用程序切换到后台以后,session 会和 ApplicationDelegate 作交互,session 中的task还会继续下载,当全部的task完成以后(不管下载失败仍是成功),系统都会调用ApplicationDelegateapplication:handleEventsForBackgroundURLSession:completionHandler:回调,在处理事件以后,在 completionHandler参数中执行 闭包,这样应用程序就能够获取用户界面的刷新。ui

若是咱们查看handleEventsForBackgroundURLSession这个api的话,会发现苹果文档要求在实现下载完成后须要实现URLSessionDidFinishEvents的代理,以达到更新屏幕的目的。url

func urlSessionDidFinishEvents(forBackgroundURLSession session: URLSession) {
    print("后台任务")
    DispatchQueue.main.async {
        guard let appDelegate = UIApplication.shared.delegate as? AppDelegate, let backgroundHandle = appDelegate.backgroundSessionCompletionHandler else { return }
        backgroundHandle()
    }
}
复制代码

若是没有实现此方法的话⚠️️:后台下载的实现是不会有影响的,只是在应用程序由后台切换到前台的过程当中可能会形成卡顿或者掉帧,同时可能在控制台输出警告:

Alamofire后台下载

经过上面的例子🌰会发现若是要实现一个后台下载,须要写不少的代码,同时还要注意后台下载权限的开启,完成下载以后回调的实现,漏掉了任何一步,后台下载都不可能完美的实现,下面就来对比一下,在Alamofire中是怎么实现后台下载的。

首先先建立一个ZHBackgroundManger的后台下载管理类:

struct ZHBackgroundManger {    
    static let shared = ZHBackgroundManger()

    let manager: SessionManager = {
        let configuration = URLSessionConfiguration.background(withIdentifier: "com.Henry.AlamofireDemo")
        configuration.httpAdditionalHeaders = SessionManager.defaultHTTPHeaders
        configuration.timeoutIntervalForRequest = 10
        configuration.timeoutIntervalForResource = 10
        configuration.sharedContainerIdentifier = "com.Henry.AlamofireDemo"
        return SessionManager(configuration: configuration)
    }()
}
复制代码

后台下载的实现:

ZHBackgroundManger.shared.manager
    .download(self.urlDownloadStr) { (url, response) -> (destinationURL: URL, options: DownloadRequest.DownloadOptions) in
    let documentUrl = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).first
    let fileUrl     = documentUrl?.appendingPathComponent(response.suggestedFilename!)
    return (fileUrl!,[.removePreviousFile,.createIntermediateDirectories])
    }
    .response { (downloadResponse) in
        print("下载回调信息: \(downloadResponse)")
    }
    .downloadProgress { (progress) in
        print("下载进度 : \(progress)")
}
复制代码

并在AppDelegate作统一的处理:

func application(_ application: UIApplication, handleEventsForBackgroundURLSession identifier: String, completionHandler: @escaping () -> Void) {
    ZHBackgroundManger.shared.manager.backgroundCompletionHandler = completionHandler
}
复制代码

这里可能会有疑问🤔,为甚么要建立一个ZHBackgroundManger单例类?

那么下面就带着这个疑问❓来探究一下

若是点击ZHBackgroundManger.shared.manager.download这里的manager会发现这是SessionManager,那么就跟进去SessionManager的源码来看一下:

能够看到在 SessionManagerdefault方法中,是对 URLSessionConfiguration作了一些配置,并初始化 SessionManager.

那么再来看SessionManager的初始化方法:

SessionManagerinit初始化方法中,能够看到这里把 URLSessionConfiguration设置成 default模式, 在内容的前篇,在建立一个URLSession的后台下载的时候,咱们已经知道须要把URLSessionConfiguration设置成background模式才能够。

在初始化方法里还有一个SessionDelegatedelegate,并且这个delegate被传入到URLSession中做为其代理,而且session的这个初始化也就使得之后的回调都将会由 self.delegate 来处理了。也就是SessionManager实例建立一个SessionDelegate对象来处理底层URLSession生成的不一样类型的代理回调。(这又称为代理移交)。

代理移交以后,在commonInit()的方法中会作另外的一些配置信息:

在这里 delegate.sessionManager被设置为自身 self,而 self实际上是持有 delegate 的。并且 delegatesessionManagerweak属性修饰符。

这里这么写delegate.sessionManager = self的缘由是

  • delegate在处理回调的时候能够和sessionManager进行通讯
  • delegate将不属于本身的回调处理从新交给sessionManager进行再次分发
  • 减小与其余逻辑内容的依赖

并且这里的delegate.sessionDidFinishEventsForBackgroundURLSession闭包,只要后台任务下载完成就会回调到这个闭包内部,在闭包内部,回调了主线程,调用了 backgroundCompletionHandler,这也就是在AppDelegateapplication:handleEventsForBackgroundURLSession方法中的completionHandler。至此,SessionManager的流程大概就是这样。

对于上面的疑问:

  • 1. 经过源码咱们能够知道SessionManager在设置URLSessionConfiguration的默认的是default模式,由于须要后台下载的话,就须要把URLSessionConfiguration的模式修改成background模式。包括咱们也能够修改URLSessionConfiguration其余的配置
  • 2. 在下载的时候,应用程序进入到后台下载,若是对于上面的配置,不作成一个单例的话,或者没有被持有的状况下,在进入后台后就会被释放掉,从而会产生错误Error Domain=NSURLErrorDomain Code=-999 "cancelled"
  • 3. 并且将SessionManager从新包装成一个单例后,在AppDelegate中的代理方法中能够直接使用。

总结

  • 首先在 AppDelegateapplication:handleEventsForBackgroundURLSession的方法里,把回调闭包completionHandler传给了 SessionManagerbackgroundCompletionHandler.
  • 在下载完成的时候 SessionDelegateurlSessionDidFinishEvents代理的调用会触发 SessionManagersessionDidFinishEventsForBackgroundURLSession代理的调用
  • 而后sessionDidFinishEventsForBackgroundURLSession 执行SessionManagerbackgroundCompletionHandler的闭包.
  • 最后会来到 AppDelegateapplication:handleEventsForBackgroundURLSession的方法里的 completionHandler 的调用.

关于Alamofire后台下载的代码就分析到这里,其实经过源码发现,和利用URLSession进行后台下载原理是大体相同的,只不过利用Alamofire使代码看起来更加简介,并且Alamofire中会有不少默认的配置,咱们只须要修改须要的配置项便可。

相关文章
相关标签/搜索