姬長信(Redy)

Alamofire-后台下载

>上一篇文章提到了后台下载/uff0c下面看看在`Alamofire`中是如何处理后台下载的。首先使用原生写法来实现一个后台下载任务/uff0c在使用`Alamofire`来实现/uff0c通过对比来看看`Alamofire`的优势。 数据源地址/uff1ahttp://onapp.yahibo.top/public/videos/video.mp4 #### 一、URLSession后台下载 首先需要创建会话并设置会话参数/uff1a ``` //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、创建一个下载任务/uff0c并发起请求 session.downloadTask(with: url).resume() ``` * 配置会话为`background`模式/uff0c开启后台下载功能 * 创建下载任务并执行`resume`启动任务 * 会话初始化设置代理后/uff0c任务回调只走代理方法/uff0c不会通过闭包进行数据回调/uff0c如果使用闭包回传也会报错提示 ``` session.downloadTask(with: url) { (url, response, error) in print(url) print(response) print(error) }.resume() ``` ``` 错误信息/uff1aCompletion handler blocks are not supported in background sessions. Use a delegate instead. ``` 在后台会话中不支持`block`块回调数据/uff0c要求使用代理/uff0c因此在后台下载中/uff0c我们直接使用代理方法来处理数据。代理方法如下/uff1a ``` 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("下载完成/uff1a/(location.path)") //存储到用户目录 let documents = NSHomeDirectory() + "/Documents/my.mp4" print("存储位置/uff1a/(documents)") //复制视频到目标地址 let fileManager = FileManager.default try!fileManager.moveItem(atPath: locationPath, toPath: documents) } } ``` >实现了对下载任务进度的监听/uff0c下载任务完成的监听/uff0c在文件下载完成时首先会保存在沙盒中`tmp`文件下/uff0c该文件只存储临时数据/uff0c使用完后会自动清理/uff0c因此需要将`tmp`中下载的文件复制到`Documents`文件夹中存储。 通过打印的路径查看文件下载情况/uff0c以上操作实际并没有真正完成后台下载/uff0c应用返回后台/uff0c下载任务就已停止/uff0c进入前台才能看到下载完成/uff0c界面不能够及时更新。 ``` 下载进度: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} 下载完成/uff1a/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 存储位置/uff1a/Users/hibo/Library/Developer/CoreSimulator/Devices/404EDFDD-735E-454B-A576-70268D8A17C0/data/Containers/Data/Application/E3175312-D6B8-4576-9B84-4EBD7751A4C0/Documents/20190819152314.mp4 ``` 上篇文章有提到/uff0c苹果官方要求在进行后台任务下载时需要实现两个代理方法/uff0c来及时通知系统更新界面。 **1、在AppDelegate中实现** ``` var backgroundCompletionHandler: (()->Void)? = nil //设置此处开启后台下载权限 func application(_ application: UIApplication, handleEventsForBackgroundURLSession identifier: String, completionHandler: @escaping () -> Void) { self.backgroundCompletionHandler = completionHandler } ``` * 开启后台下载权限/uff0c实现代理方法即为开通 **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() } } ``` * 后台任务完成会调用该方法/uff0c在该方法内部调用`AppDelegate`中的闭包/uff0c通知系统更新界面/uff0c否则会出现掉帧 >添加以上方法再次运行下载/uff0c退出前台/uff0c等待几秒钟能够看到在控制台是有后台下载完成回调打印的/uff0c在该情况下/uff0c我们再次进入前台/uff0c我们的页面实际上已经被更新了。至此我们就完成了一个后台下载的功能。 **总结/uff1a后台下载任务需要实现四个代理方法** 控制器/uff1a * `URLSessionDownloadTask/uff1a`获取下载进度 * `didFinishDownloadingTo/uff1a`下载完成处理下载文件 * `urlSessionDidFinishEvents/uff1a`后台下载完成调用/uff0c提示系统及时更新界面/uff0c执行Application中的闭包函数 Application/uff1a * `backgroundCompletionHandler/uff1a`后台下载完成接收通知消息的闭包 >从多年的开发经验来看/uff08太装了/ud83d/ude02/uff09/uff0c以上这种实现方式其实不是理想结果/uff0c功能代码分散。下面就看一下`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 } } ``` 在以上代码中/uff0c`Alamofire`可以直接通过`request`发送请求/uff0c同样在框架中也存在`download`方法来完成下载任务。查看[官方文档](https://github.com/Alamofire/Alamofire)。 ``` //下载文件 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("完成下载/uff1a/(response)") }) ``` * `DownloadRequest.DownloadOptions/uff1a`设置下载文件的存储地 * `downloadProgress/uff1a`获取下载进度 以上虽然可以下载我们需要的文件/uff0c但是不能在后台下载。首先官方指出/uff1a >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`模式/uff0c而在以上使用的`download`中实际上使用的是`default模式`/uff0c并不能支持后台下载。如下代码/uff1a ``` public static let `default`: SessionManager = { let configuration = URLSessionConfiguration.default configuration.httpAdditionalHeaders = SessionManager.defaultHTTPHeaders return SessionManager(configuration: configuration) }() ``` 通过[官方文档](https://github.com/Alamofire/Alamofire)和源码的查看/uff0c实际上我们只需要重新设置会话的配置信息就可以了。 **修改会话模式** ``` let configuration = URLSessionConfiguration.background(withIdentifier:"com.yahibo.background_id") configuration.httpAdditionalHeaders = SessionManager.defaultHTTPHeaders sessionManager = SessionManager(configuration: configuration) ``` 以上`sessionManager`需要设置为一个单例对象/uff0c以便于在后台下载模式中接收`Appdelegate`的代理闭包函数/uff0c通过闭包通知系统及时更新界面。代码如下/uff1a ``` 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) }() } ``` 下面就开始实现下载功能/uff1a ``` 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("完成下载/uff1a/(response)") }) ``` * 同上直接调用`download`方法来下载/uff0c并存储数据 应苹果要求我们还需要调用`handleEventsForBackgroundURLSession`中的的代码块/uff0c通知系统及时更新界面/uff0c在`SessionManager`中如何做连接呢。代码如下/uff1a ``` //设置此处开启后台下载权限 func application(_ application: UIApplication, handleEventsForBackgroundURLSession identifier: String, completionHandler: @escaping () -> Void) { BackgroundManager.shared.manager.backgroundCompletionHandler = completionHandler } ``` * `SessionManager`中已经备好了需要的`backgroundCompletionHandler`代码块声明/uff0c以便接收闭包/uff0c调用闭包 >简单几步就实现了我们想要的后台下载功能了/uff0c编码简洁/uff0c逻辑清晰。这里我们只在`Application`中实现了开启后台下载权限的代理/uff0c但并没有在控制器中设置`delegate`和实现`urlSessionDidFinishEvents`代理方法/uff0c这里不难猜测`URLSessionDownloadTask、didFinishDownloadingTo、urlSessionDidFinishEvents`代理方法应该是在`SessionManager`中实现/uff0c统一管理再以闭包的形式回传到当前界面。下面就看一下`SessionManager`是不是这么实现的。 #### 三、SessionManager源码探索 首先顺着`SessionManager`的创建找到类中的初始化方法/uff1a ``` 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) } ``` 初始化有三个初始参数/uff0c并设有缺省值/uff0c该方法返回一个新的`SessionManager`对象。在上面后台下载中我们只配置了`configuration`参数/uff0c设置为了后台下载模式。上面也提到了/uff0c在`SessionManager`中应该是有我们的后台下载相关的代理实现/uff0c在该函数中看到初始化了一个`SessionDelegate`对象/uff0c并将`URLSession`的代理实现指向了`SessionDelegate`对象/uff0c不难猜出`URLSession`相关的代理方法应该都在`SessionDelegate`类中实现。 **SessionDelegate** >在`SessionDelegate.swift`中/uff0c`SessionDelegate`继承自`NSObject`/uff0c声明了所有与`URLSession`代理相关连的闭包函数/uff0c用来向界面回传代理事件产生的结果。 在扩展方法中实现了以下几个代理的方法/uff1a >URLSessionDelegate URLSessionTaskDelegate URLSessionDataDelegate URLSessionDownloadDelegate URLSessionStreamDelegate 下面就看一下下载相关的代理方法内部实现了哪些功能。代码如下/uff1a ``` 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 ) } } } ``` 以上三个方法用来监控下载进度/uff0c及下载是否完成/uff0c在回调内部通过闭包回调代理事件到主界面。该文件中实现了上面提到的代理的所有方法/uff0c通过声明的闭包向外界传值/uff0c在外部只需要调用闭包即可。这里和外界桥接的闭包函数返回一个`self`/uff0c因此能够以链式的形式/uff0c来获取代理传来的数据。如下/uff1a ``` open func downloadProgress(queue: DispatchQueue = DispatchQueue.main, closure: @escaping ProgressHandler) -> Self { downloadDelegate.progressHandler = (closure, queue) return self } ``` * 桥接界面与内部`SessionDelegate`扩展代理/uff0c完成下载进度的监听 其他桥接方法省略/u2026/u2026 针对后台下载找到了继承自`URLSessionDelegate`的扩展/uff1a ``` extension SessionDelegate: URLSessionDelegate { open func urlSessionDidFinishEvents(forBackgroundURLSession session: URLSession) { sessionDidFinishEventsForBackgroundURLSession?(session) } } ``` 后台下载完成/uff0c会执行该方法/uff0c在该方法中/uff0c调用了外界实现的闭包/uff0c此闭包实现在`SessionManager`中/uff0c如下/uff1a ``` 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`/uff0c此处出现循环引用/uff0c这里的`delegate.sessionManager`使用`weak`修饰解决 * 实现`delegate`中后台下载完成回调闭包/uff0c在此处接收后台下载完成消息 * 在主线程中/uff0c调用`backgroundCompletionHandler`将消息发送至`backgroundCompletionHandler`的闭包实现 这里应该就清楚了/uff0c`backgroundCompletionHandler`是`SessionManager`声明的闭包/uff0c在`Application`中获取系统闭包实现/uff0c用来与系统通讯/uff0c告诉系统在后台及时更新界面。