前言
URLSession
是一个可以响应发送或者接受HTTP请求的关键类。首先使用全局的 URLSession.shared
和 downloadTask
来创建一个简单的下载任务:
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
这个类是有三种模式的:
URLSessionConfiguration
的三种模如下式:
-
default
:默认会话模式(使用的是基于磁盘缓存的持久化策略,通常使用最多的也是这种模式,在default
模式下系统会创建一个持久化的缓存并在用户的钥匙串中存储证书) -
ephemeral
:暂时会话模式(该模式不使用磁盘保存任何数据。而是保存在RAM
中,所有内容的生命周期都与session
相同,因此当session
会话无效时,这些缓存的数据就会被自动清空。) -
background
:后台会话模式(该模式可以在后台完成上传和下载。)
注意:
background
模式与default
模式非常相似,不过background
模式会用一个独立线程来进行数据传输。background
模式可以在程序挂起,退出,崩溃的情况下运行task
。也可以在APP
下次启动的时候,利用标识符来恢复下载。
下面先来创建一个后台下载的任务background
session
,并且指定一个 identifier
:
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
的下载代理URLSessionDownloadDelegate
,URLSessionDelegate
的方法:
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
方法:
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
完成之后(无论下载失败还是成功),系统都会调用ApplicationDelegate
的application:handleEventsForBackgroundURLSession:completionHandler:
回调,在处理事件之后,在 completionHandler
参数中执行 闭包,这样应用程序就可以获取用户界面的刷新。
如果我们查看handleEventsForBackgroundURLSession
这个api
的话,会发现苹果文档要求在实现下载完成后需要实现URLSessionDidFinishEvents
的代理,以达到更新屏幕的目的。
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
的源码来看一下:
可以看到在
SessionManager
的default
方法中,是对URLSessionConfiguration
做了一些配置,并初始化SessionManager
.
那么再来看SessionManager
的初始化方法:
在
SessionManager
的init
初始化方法中,可以看到这里把URLSessionConfiguration
设置成default
模式,在内容的前篇,在创建一个URLSession
的后台下载的时候,我们已经知道需要把URLSessionConfiguration
设置成background
模式才可以。
在初始化方法里还有一个SessionDelegate
的delegate
,而且这个delegate
被传入到URLSession
中作为其代理,并且session
的这个初始化也就使得以后的回调都将会由 self.delegate
来处理了。也就是SessionManager
实例创建一个SessionDelegate
对象来处理底层URLSession
生成的不同类型的代理回调。(这又称为代理移交)。
代理移交之后,在commonInit()
的方法中会做另外的一些配置信息:
在这里
delegate.sessionManager
被设置为自身 self
,而 self
其实是持有 delegate
的。而且 delegate
的 sessionManager
是weak
属性修饰符。
这里这么写delegate.sessionManager = self
的原因是
delegate
在处理回调的时候可以和sessionManager
进行通信delegate
将不属于自己的回调处理重新交给sessionManager
进行再次分发- 减少与其他逻辑内容的依赖
而且这里的delegate.sessionDidFinishEventsForBackgroundURLSession
闭包,只要后台任务下载完成就会回调到这个闭包内部,在闭包内部,回调了主线程,调用了 backgroundCompletionHandler
,这也就是在AppDelegate
中application:handleEventsForBackgroundURLSession
方法中的completionHandler
。至此,SessionManager
的流程大概就是这样。
对于上面的疑问:
-
1. 通过源码我们可以知道
SessionManager
在设置URLSessionConfiguration
的默认的是default
模式,因为需要后台下载的话,就需要把URLSessionConfiguration
的模式修改为background
模式。包括我们也可以修改URLSessionConfiguration
其他的配置 -
2. 在下载的时候,应用程序进入到后台下载,如果对于上面的配置,不做成一个单例的话,或者没有被持有的情况下,在进入后台后就会被释放掉,从而会产生错误
Error Domain=NSURLErrorDomain Code=-999 "cancelled"
-
3. 而且将
SessionManager
重新包装成一个单例后,在AppDelegate
中的代理方法中可以直接使用。
总结
- 首先在
AppDelegate
的application:handleEventsForBackgroundURLSession
的方法里,把回调闭包completionHandler
传给了SessionManager
的backgroundCompletionHandler
. - 在下载完成的时候
SessionDelegate
的urlSessionDidFinishEvents
代理的调用会触发SessionManager
的sessionDidFinishEventsForBackgroundURLSession
代理的调用 - 然后
sessionDidFinishEventsForBackgroundURLSession
执行SessionManager
的backgroundCompletionHandler
的闭包. - 最后会来到
AppDelegate
的application:handleEventsForBackgroundURLSession
的方法里的completionHandler
的调用.
关于Alamofire
后台下载的代码就分析到这里,其实通过源码发现,和利用URLSession
进行后台下载原理是大致相同的,只不过利用Alamofire
使代码看起来更加简介,而且Alamofire
中会有很多默认的配置,我们只需要修改需要的配置项即可。