URLSession
是一个能够响应发送或者接受HTTP请求的关键类。首先使用全局的 URLSession.shared
和 downloadTask
来建立一个简单的下载任务: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
键、或者切到其它应用程序上),当前的下载任务便会马上中止,这个样话对于一些较大的文件,下载过程当中用户没法切换到后台,对用户来讲是一种不太友好的体验。下面来看一下在后台下载的具体实现:缓存
咱们能够经过URLSessionConfiguration
类新建URLSession
实例,而URLSessionConfiguration
这个类是有三种模式的: bash
URLSessionConfiguration
的三种模以下式:session
default
:默认会话模式(使用的是基于磁盘缓存的持久化策略,一般使用最多的也是这种模式,在default
模式下系统会建立一个持久化的缓存并在用户的钥匙串中存储证书)ephemeral
:暂时会话模式(该模式不使用磁盘保存任何数据。而是保存在 RAM
中,全部内容的生命周期都与session
相同,所以当session
会话无效时,这些缓存的数据就会被自动清空。)background
:后台会话模式(该模式能够在后台完成上传和下载。)注意:
background
模式与default
模式很是类似,不过background
模式会用一个独立线程来进行数据传输。background
模式能够在程序挂起,退出,崩溃的状况下运行task
。也能够在APP
下次启动的时候,利用标识符来恢复下载。闭包
下面先来建立一个后台下载的任务background
session
,而且指定一个 identifier
:app
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
的方法: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
完成以后(不管下载失败仍是成功),系统都会调用ApplicationDelegate
的application: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
中是怎么实现后台下载的。
首先先建立一个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
的流程大概就是这样。
对于上面的疑问:
SessionManager
在设置URLSessionConfiguration
的默认的是default
模式,由于须要后台下载的话,就须要把URLSessionConfiguration
的模式修改成background
模式。包括咱们也能够修改URLSessionConfiguration
其余的配置Error Domain=NSURLErrorDomain Code=-999 "cancelled"
SessionManager
从新包装成一个单例后,在AppDelegate
中的代理方法中能够直接使用。AppDelegate
的application:handleEventsForBackgroundURLSession
的方法里,把回调闭包completionHandler
传给了 SessionManager
的 backgroundCompletionHandler
.SessionDelegate
的 urlSessionDidFinishEvents
代理的调用会触发 SessionManager
的sessionDidFinishEventsForBackgroundURLSession
代理的调用sessionDidFinishEventsForBackgroundURLSession
执行SessionManager
的 backgroundCompletionHandler
的闭包.AppDelegate
的application:handleEventsForBackgroundURLSession
的方法里的 completionHandler
的调用.关于Alamofire
后台下载的代码就分析到这里,其实经过源码发现,和利用URLSession
进行后台下载原理是大体相同的,只不过利用Alamofire
使代码看起来更加简介,并且Alamofire
中会有不少默认的配置,咱们只须要修改须要的配置项便可。