Alamofire(一)后台下载基本使用及原理解析
前言
这篇文章主要是分析后台下载,通过先写URLSession
的后台下载,然后使用Alamofire
这两种不同的情况,来详细解析在这个过程中遇到的坑和疑惑点,并且学习Alamofire
的核心设计思想!
URLSession 后台下载
let configuration = URLSessionConfiguration.background(withIdentifier: self.createID())let session = URLSession.init(configuration: configuration, delegate: self, delegateQueue: OperationQueue.main)session.downloadTask(with: URL(string: self.urlDownloadStr2)!).resume()
这里用到了
URLSessionConfiguration.background
模式,是专门用来后台下载的,一共有三种模式,常用的是default
default:默认模式,系统会创建一个持久化的缓存并在用户的钥匙串中存储证书。还设置了代理方法监听下载进度和下载完成:
ephemeral:和default
相反,系统不创建持久性存储,所有内容的生命周期与session
相同。当session
无效时,所有内容自动释放。
background:创建一个可以在后台甚至APP已经关闭的时候仍在传输数据的session
。
extension ViewController:URLSessionDownloadDelegate{
func urlSession(_ session: URLSession, downloadTask: URLSessionDownloadTask, didFinishDownloadingTo location: URL) {
// 下载完成 - 开始沙盒迁移
print("下载完成 - \(location)")
let locationPath = location.path
//拷贝到用户目录(文件名以时间戳命名)
let documnets = NSHomeDirectory() + "/Documents/" + self.lgCurrentDataTurnString() + ".mp4"
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")
}
}
这里是因为http的分段传输显得下载有很多段,内部是对这个代理方法不断调用,才能监听进度的回调。
在传输层中会由TCP对HTTP报文做了分段传输,达到目标地址后再对所有TCP段进行重组。
delegate没有接收到下载回调 测试发现,在切入到桌面后,
Delegate
没有接收到回调,这是因为还需要在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
}
}
- 直到所有Task全都完成后,系统会调用
ApplicationDelegate
的application:handleEventsForBackgroundURLSession:completionHandler:
回调,在处理事件之后,在completionHandler
参数中执行block
,这样应用程序就可以获取用户界面的刷新。 - 对于每一个后台下载的Task调用
Session
的Delegate
中的URLSession:downloadTask:didFinishDownloadingToURL:
(成功的话)和URLSession:task:didCompleteWithError:
(成功或者失败都会调用)
在上面的viewCotroller
扩展里,多实现一个URLSessionDownloadDelegate
的代理方法监听下载回来:注意要切换到主线程,因为要刷新界面
func urlSessionDidFinishEvents(forBackgroundURLSession session: URLSession) {
print("后台任务下载回来")
DispatchQueue.main.async {
guard let appDelegate = UIApplication.shared.delegate as? AppDelegate, let backgroundHandle = appDelegate.backgroundSessionCompletionHandler else { return }
backgroundHandle()
}
}
如果不实现
urlSessionDidFinishEvents
这个代理方法会发生:- 后台下载能力不影响,会正常下载完
- 耗费性能,
completionHandler
一直没调用,界面刷新会卡顿,影响用户体验
刚才搞定了
URLSession
的后台下载,但是使用起来是非常恶心的,非常麻烦,现在用Alamofire
体验一下快速实现后台下载功能。DLBackgroundManger.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)")
}
使用链式直接完成请求和响应,同时还监听下载的回调,代码非常简洁而且可读性高。
封装了一个单例
DLBackgroundManger
用来管理后台下载,同时可以在里面配置很多基本参数,便于管理struct LGBackgroundManger {
static let shared = LGBackgroundManger()let manager: SessionManager = {
let configuration = URLSessionConfiguration.background(withIdentifier: "com.test.alamofire")
configuration.httpAdditionalHeaders = SessionManager.defaultHTTPHeaders
configuration.timeoutIntervalForRequest = 10
configuration.timeoutIntervalForResource = 10
configuration.sharedContainerIdentifier = "com.test.alamofire"
return SessionManager(configuration: configuration)
}()
}
在这里使用单例管理的原因:
- 之前没有使用单例管理类,直接用
SessionManager
去调用,发现在切入后台的时候,控制台会报错如下,是因为被释放了,所以就报错了
Error Domain=NSURLErrorDomain Code=-999 "cancelled"
- 在
AppDelegate
的回调里使用也非常方便
func application(_ application: UIApplication, handleEventsForBackgroundURLSession identifier: String, completionHandler: @escaping () -> Void) {
DLBackgroundManger.shared.manager.backgroundCompletionHandler = completionHandler
}
SessionManager源码解析
习惯性的使用框架,一定要分析它的实现原理。
点进去
SessionManager
,看到它有一个default
,类似URLSession
的default
,发现这里也确实设置成URLSessionConfiguration.default
,然后还在SessionManager.defaultHTTPHeaders
这里设置了一些初始化的headerpublic static let `default`: SessionManager = {
let configuration = URLSessionConfiguration.default
configuration.httpAdditionalHeaders = SessionManager.defaultHTTPHeadersreturn SessionManager(configuration: configuration)
}()
找到它的初始化
init
方法public init(
configuration: URLSessionConfiguration = URLSessionConfiguration.default,
delegate: SessionDelegate = SessionDelegate(),
serverTrustPolicyManager: ServerTrustPolicyManager? = nil)
{
self.delegate = delegate
self.session = URLSession(configuration: configuration, delegate: delegate, delegateQueue: nil)commonInit(serverTrustPolicyManager: serverTrustPolicyManager)
}
这里都做了什么呢,其实很简单: 初始化了
URLSession
,默认用default
模式,使用了SessionDelegate
来接收URLSession
的delegate
,其实就是一个代理的移交。接着点进去
commonInit
,发现这里回调了当前的delegate.sessionDidFinishEventsForBackgroundURLSession
,还做了[weak self]
弱引用操作,同时还做了DispatchQueue.main.async
切换主线程操作private func commonInit(serverTrustPolicyManager: ServerTrustPolicyManager?) {
session.serverTrustPolicyManager = serverTrustPolicyManagerdelegate.sessionManager = selfdelegate.sessionDidFinishEventsForBackgroundURLSession = { [weak self] session in
guard let strongSelf = self else { return }
DispatchQueue.main.async { strongSelf.backgroundCompletionHandler?() }
}
}
SessionDelegate
点进去看看
SessionDelegate
,发现它实现了所有的代理:URLSessionDelegate
,URLSessionTaskDelegate
,URLSessionDataDelegate
,URLSessionDownloadDelegate
,URLSessionStreamDelegate
我们按照在上面探索到的
sessionDidFinishEventsForBackgroundURLSession
,发现这里是我们所需要找的方法,根据注释,这里是负责执行这个闭包的方法,然后在上面探索到的是这个闭包的具体实现#if !os(macOS)
/// Tells the delegate that all messages enqueued for a session have been delivered.
///
/// - parameter session: The session that no longer has any outstanding requests.
open func urlSessionDidFinishEvents(forBackgroundURLSession session: URLSession) {
sessionDidFinishEventsForBackgroundURLSession?(session)
}
#endif
}
总结
我们联系到刚刚在
AppDelegate
写的代码DLBackgroundManger.shared.manager.backgroundCompletionHandler = completionHandler
,然后梳理一下整个流程:- 把
AppDelegate
的completionHandler
传递给SessionManager
的backgroundCompletionHandler
- 下载完成的时候,
SessionDelegate
里的urlSessionDidFinishEvents
执行,会调用到SessionManager
里的sessionDidFinishEventsForBackgroundURLSession
-
sessionDidFinishEventsForBackgroundURLSession
的闭包里会执行当前的backgroundCompletionHandler
-
backgroundCompletionHandler
是AppDelegate
传递过来的,所以就会调用它的completionHandler
推荐阅读
- 一个人的旅行,三亚
- 一个小故事,我的思考。
- 《真与假的困惑》???|《真与假的困惑》??? ——致良知是一种伟大的力量
- 开学第一天(下)
- 一个人的碎碎念
- 2018年11月19日|2018年11月19日 星期一 亲子日记第144篇
- 遇到一哭二闹三打滚的孩子,怎么办┃山伯教育
- 第326天
- Y房东的后半生14
- 奔向你的城市