上一篇文章提到了后台下载,下面看看在
Alamofire
中是如何处理后台下载的。首先使用原生写法来实现一个后台下载任务,在使用Alamofire
来实现,通过对比来看看Alamofire
的优势。
数据源地址:http://testapi.onapp.top/public/videos/video.mp4
一、URLSession后台下载
首先需要创建会话并设置会话参数:
//1、配置请求参数
let configuration = URLSessionConfiguration.background(withIdentifier: "com.yahibo.background_id")
let session = URLSession.init(configuration: configuration,delegate: self,delegateQueue: OperationQueue.main)
//2、设置数据源
let videoUrl = "http://onapp.yahibo.top/public/videos/video.mp4"
let url = URL.init(string: videoUrl)!
//3、创建一个下载任务,并发起请求
session.downloadTask(with: url).resume()
- 配置会话为
background
模式,开启后台下载功能 - 创建下载任务并执行
resume
启动任务 - 会话初始化设置代理后,任务回调只走代理方法,不会通过闭包进行数据回调,如果使用闭包回传也会报错提示
session.downloadTask(with: url) { (url, response, error) in
print(url)
print(response)
print(error)
}.resume()
错误信息:Completion handler blocks are not supported in background sessions. Use a delegate instead.
在后台会话中不支持block
块回调数据,要求使用代理,因此在后台下载中,我们直接使用代理方法来处理数据。代理方法如下:
extension Alamofire2Controller: URLSessionDownloadDelegate{
//1、下载进度
func urlSession(_ session: URLSession, downloadTask: URLSessionDownloadTask, didWriteData bytesWritten: Int64, totalBytesWritten: Int64, totalBytesExpectedToWrite: Int64) {
print("下载进度:\(Double(totalBytesWritten)/Double(totalBytesExpectedToWrite))")
}
//2、下载完成
func urlSession(_ session: URLSession, downloadTask: URLSessionDownloadTask, didFinishDownloadingTo location: URL) {
let locationPath = location.path
print("下载完成:\(location.path)")
//存储到用户目录
let documents = NSHomeDirectory() + "/Documents/my.mp4"
print("存储位置:\(documents)")
//复制视频到目标地址
let fileManager = FileManager.default
try!fileManager.moveItem(atPath: locationPath, toPath: documents)
}
}
实现了对下载任务进度的监听,下载任务完成的监听,在文件下载完成时首先会保存在沙盒中
tmp
文件下,该文件只存储临时数据,使用完后会自动清理,因此需要将tmp
中下载的文件复制到Documents
文件夹中存储。
通过打印的路径查看文件下载情况,以上操作实际并没有真正完成后台下载,应用返回后台,下载任务就已停止,进入前台才能看到下载完成,界面不能够及时更新。
下载进度:0.3653140762324527
下载进度:0.4018703091059228
2019-08-19 15:23:14.237923+0800 AlamofireDemo[849:9949] An error occurred on the xpc connection requesting pending callbacks for the background session: Error Domain=NSCocoaErrorDomain Code=4097 "connection to service named com.apple.nsurlsessiond" UserInfo={NSDebugDescription=connection to service named com.apple.nsurlsessiond}
下载完成:/Users/hibo/Library/Developer/CoreSimulator/Devices/404EDFDD-735E-454B-A576-70268D8A17C0/data/Containers/Data/Application/E3175312-D6B8-4576-9B84-4EBD7751A4C0/Library/Caches/com.apple.nsurlsessiond/Downloads/com.yahibo.background_id/CFNetworkDownload_eo4RMO.tmp
存储位置:/Users/hibo/Library/Developer/CoreSimulator/Devices/404EDFDD-735E-454B-A576-70268D8A17C0/data/Containers/Data/Application/E3175312-D6B8-4576-9B84-4EBD7751A4C0/Documents/20190819152314.mp4
上篇文章有提到,苹果官方要求在进行后台任务下载时需要实现两个代理方法,来及时通知系统更新界面。
1、在AppDelegate中实现
var backgroundCompletionHandler: (()->Void)? = nil
//设置此处开启后台下载权限
func application(_ application: UIApplication, handleEventsForBackgroundURLSession identifier: String, completionHandler: @escaping () -> Void) {
self.backgroundCompletionHandler = completionHandler
}
- 开启后台下载权限,实现代理方法即为开通
2、在上面Alamofire2Controller扩展中实现代理方法
//后台任务下载回调
func urlSessionDidFinishEvents(forBackgroundURLSession session: URLSession) {
print("后台任务下载回来")
DispatchQueue.main.async {
guard let appDelegate = UIApplication.shared.delegate as? AppDelegate, let backgroundHandle = appDelegate.backgroundCompletionHandler else { return }
backgroundHandle()
}
}
- 后台任务完成会调用该方法,在该方法内部调用
AppDelegate
中的闭包,通知系统更新界面,否则会出现掉帧
添加以上方法再次运行下载,退出前台,等待几秒钟能够看到在控制台是有后台下载完成回调打印的,在该情况下,我们再次进入前台,我们的页面实际上已经被更新了。至此我们就完成了一个后台下载的功能。
总结:后台下载任务需要实现四个代理方法
控制器:
-
URLSessionDownloadTask:
获取下载进度 -
didFinishDownloadingTo:
下载完成处理下载文件 -
urlSessionDidFinishEvents:
后台下载完成调用,提示系统及时更新界面,执行Application中的闭包函数
Application:
-
backgroundCompletionHandler:
后台下载完成接收通知消息的闭包
从多年的开发经验来看(太装了),以上这种实现方式其实不是理想结果,功能代码分散。下面就看一下
Alamofire
是如何实现的。
二、Alamofire后台下载
Alamofire.request(url,method: .post,parameters: ["page":"1","size":"20"]).responseJSON {
(response) in
switch response.result{
case .success(let json):
print("json:\(json)")
break
case .failure(let error):
print("error:\(error)")
break
}
}
在以上代码中,Alamofire
可以直接通过request
发送请求,同样在框架中也存在download
方法来完成下载任务。查看官方文档。
//下载文件
Alamofire.download(url, to: { (url, response) -> (destinationURL: URL, options: DownloadRequest.DownloadOptions) in
let documentsURL = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask)[0]
let fileURL = documentsURL.appendingPathComponent("\(self.currentDateStr()).mp4")
return (fileURL, [.removePreviousFile, .createIntermediateDirectories])
})
.downloadProgress { (progress) in
print(progress)
}.response(queue: DispatchQueue.global(qos: .utility), completionHandler: { (response) in
print("完成下载:\(response)")
})
-
DownloadRequest.DownloadOptions:
设置下载文件的存储地 -
downloadProgress:
获取下载进度
以上虽然可以下载我们需要的文件,但是不能在后台下载。首先官方指出:
The Alamofire.download APIs should also be used if you need to download data while your app is in the background. For more information, please see the Session Manager Configurations section.
需要我们手动配置会话为background
模式,而在以上使用的download
中实际上使用的是default模式
,并不能支持后台下载。如下代码:
public static let `default`: SessionManager = {
let configuration = URLSessionConfiguration.default
configuration.httpAdditionalHeaders = SessionManager.defaultHTTPHeaders
return SessionManager(configuration: configuration)
}()
通过官方文档和源码的查看,实际上我们只需要重新设置会话的配置信息就可以了。
修改会话模式
let configuration = URLSessionConfiguration.background(withIdentifier:"com.yahibo.background_id")
configuration.httpAdditionalHeaders = SessionManager.defaultHTTPHeaders
sessionManager = SessionManager(configuration: configuration)
以上sessionManager
需要设置为一个单例对象,以便于在后台下载模式中接收Appdelegate
的代理闭包函数,通过闭包通知系统及时更新界面。代码如下:
struct BackgroundManager {
static let shared = BackgroundManager()
let manager: SessionManager = {
let configuration = URLSessionConfiguration.background(withIdentifier:"com.yahibo.background_id")
configuration.httpAdditionalHeaders = SessionManager.defaultHTTPHeaders
return SessionManager(configuration: configuration)
}()
}
下面就开始实现下载功能:
BackgroundManager.shared.manager.download(url) { (url, response) -> (destinationURL: URL, options: DownloadRequest.DownloadOptions) in
let documentsURL = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask)[0]
let fileURL = documentsURL.appendingPathComponent("\(self.currentDateStr()).mp4")
return (fileURL, [.removePreviousFile, .createIntermediateDirectories])
}.downloadProgress(queue: DispatchQueue.global(qos: .utility)) { (progress) in
print(progress)
}.response(queue: DispatchQueue.global(qos: .utility), completionHandler: { (response) in
print("完成下载:\(response)")
})
- 同上直接调用
download
方法来下载,并存储数据
应苹果要求我们还需要调用handleEventsForBackgroundURLSession
中的的代码块,通知系统及时更新界面,在SessionManager
中如何做连接呢。代码如下:
//设置此处开启后台下载权限
func application(_ application: UIApplication, handleEventsForBackgroundURLSession identifier: String, completionHandler: @escaping () -> Void) {
BackgroundManager.shared.manager.backgroundCompletionHandler = completionHandler
}
-
SessionManager
中已经备好了需要的backgroundCompletionHandler
代码块声明,以便接收闭包,调用闭包
简单几步就实现了我们想要的后台下载功能了,编码简洁,逻辑清晰。这里我们只在
Application
中实现了开启后台下载权限的代理,但并没有在控制器中设置delegate
和实现urlSessionDidFinishEvents
代理方法,这里不难猜测URLSessionDownloadTask、didFinishDownloadingTo、urlSessionDidFinishEvents
代理方法应该是在SessionManager
中实现,统一管理再以闭包的形式回传到当前界面。下面就看一下SessionManager
是不是这么实现的。
三、SessionManager源码探索
首先顺着SessionManager
的创建找到类中的初始化方法:
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)
}
初始化有三个初始参数,并设有缺省值,该方法返回一个新的SessionManager
对象。在上面后台下载中我们只配置了configuration
参数,设置为了后台下载模式。上面也提到了,在SessionManager
中应该是有我们的后台下载相关的代理实现,在该函数中看到初始化了一个SessionDelegate
对象,并将URLSession
的代理实现指向了SessionDelegate
对象,不难猜出URLSession
相关的代理方法应该都在SessionDelegate
类中实现。
SessionDelegate
在
SessionDelegate.swift
中,SessionDelegate
继承自NSObject
,声明了所有与URLSession
代理相关连的闭包函数,用来向界面回传代理事件产生的结果。
在扩展方法中实现了以下几个代理的方法:
URLSessionDelegate
URLSessionTaskDelegate
URLSessionDataDelegate
URLSessionDownloadDelegate
URLSessionStreamDelegate
下面就看一下下载相关的代理方法内部实现了哪些功能。代码如下:
extension SessionDelegate: URLSessionDownloadDelegate {
open func urlSession(
_ session: URLSession,
downloadTask: URLSessionDownloadTask,
didFinishDownloadingTo location: URL)
{
if let downloadTaskDidFinishDownloadingToURL = downloadTaskDidFinishDownloadingToURL {
downloadTaskDidFinishDownloadingToURL(session, downloadTask, location)
} else if let delegate = self[downloadTask]?.delegate as? DownloadTaskDelegate {
delegate.urlSession(session, downloadTask: downloadTask, didFinishDownloadingTo: location)
}
}
open func urlSession(
_ session: URLSession,
downloadTask: URLSessionDownloadTask,
didWriteData bytesWritten: Int64,
totalBytesWritten: Int64,
totalBytesExpectedToWrite: Int64)
{
if let downloadTaskDidWriteData = downloadTaskDidWriteData {
downloadTaskDidWriteData(session, downloadTask, bytesWritten, totalBytesWritten, totalBytesExpectedToWrite)
} else if let delegate = self[downloadTask]?.delegate as? DownloadTaskDelegate {
delegate.urlSession(
session,
downloadTask: downloadTask,
didWriteData: bytesWritten,
totalBytesWritten: totalBytesWritten,
totalBytesExpectedToWrite: totalBytesExpectedToWrite
)
}
}
open func urlSession(
_ session: URLSession,
downloadTask: URLSessionDownloadTask,
didResumeAtOffset fileOffset: Int64,
expectedTotalBytes: Int64)
{
if let downloadTaskDidResumeAtOffset = downloadTaskDidResumeAtOffset {
downloadTaskDidResumeAtOffset(session, downloadTask, fileOffset, expectedTotalBytes)
} else if let delegate = self[downloadTask]?.delegate as? DownloadTaskDelegate {
delegate.urlSession(
session,
downloadTask: downloadTask,
didResumeAtOffset: fileOffset,
expectedTotalBytes: expectedTotalBytes
)
}
}
}
以上三个方法用来监控下载进度,及下载是否完成,在回调内部通过闭包回调代理事件到主界面。该文件中实现了上面提到的代理的所有方法,通过声明的闭包向外界传值,在外部只需要调用闭包即可。这里和外界桥接的闭包函数返回一个self
,因此能够以链式的形式,来获取代理传来的数据。如下:
open func downloadProgress(queue: DispatchQueue = DispatchQueue.main, closure: @escaping ProgressHandler) -> Self {
downloadDelegate.progressHandler = (closure, queue)
return self
}
- 桥接界面与内部
SessionDelegate
扩展代理,完成下载进度的监听
其他桥接方法省略……
针对后台下载找到了继承自URLSessionDelegate
的扩展:
extension SessionDelegate: URLSessionDelegate {
open func urlSessionDidFinishEvents(forBackgroundURLSession session: URLSession) {
sessionDidFinishEventsForBackgroundURLSession?(session)
}
}
后台下载完成,会执行该方法,在该方法中,调用了外界实现的闭包,此闭包实现在SessionManager
中,如下:
private func commonInit(serverTrustPolicyManager: ServerTrustPolicyManager?) {
session.serverTrustPolicyManager = serverTrustPolicyManager
delegate.sessionManager = self
delegate.sessionDidFinishEventsForBackgroundURLSession = { [weak self] session in
guard let strongSelf = self else { return }
DispatchQueue.main.async { strongSelf.backgroundCompletionHandler?() }
}
}
- 向
SessionDelegate
中传入self
,此处出现循环引用,这里的delegate.sessionManager
使用weak
修饰解决 - 实现
delegate
中后台下载完成回调闭包,在此处接收后台下载完成消息 - 在主线程中,调用
backgroundCompletionHandler
将消息发送至backgroundCompletionHandler
的闭包实现
这里应该就清楚了,backgroundCompletionHandler
是SessionManager
声明的闭包,在Application
中获取系统闭包实现,用来与系统通讯,告诉系统在后台及时更新界面。