我之前写过一篇文章介绍如何使用 URLSession 来下载文件(点击查看),但都是前台下载。也就是说如果程序退到后台(比如按下 home 键、或者切到其它应用上),当前的下载任务便会立刻停止。
这样对于一些大文件的下载并不友好,因为用户不可能一直开着 App 等待下载完毕。下面演示如何在程序退到后台时,下载任务仍然会继续进行。
1,实现原理
(1)URLSessionConfiguration 有如下三种模式:
- default:默认会话模式(使用的是基于磁盘缓存的持久化策略)
- ephemeral:暂时会话模式(该模式不使用磁盘保存任何数据。而是保存在 RAM 中,因此当程序使会话无效,这些缓存的数据就会被自动清空。)
- background:后台会话模式(该模式可以在后台完成上传和下载)
(2)之前我们使用的都是 default 模式,要实现后台下载就必须使用 background 模式:
- 当 app 被终止时,系统会接管下载任务。
- 等到下载完成或需要 app 关注时,系统又会在后台唤醒 app(注意是后台唤醒,该 app 不会切换到前台显示)。
- 然后 app 再对下载文件进行后续处理。
特别注意:如果应用被强制关闭(双击 home 调出后台列表,并将其上滑关闭),那么后台下载就不再起作用了。
2,准备工作
(1)为方便使用,首先我们封装一个后台下载的工具类(DownloadManager.swift),具体内容如下:
- 创建一个 background session 用于后台下载(需指定一个 identifier)
- 实现 URLSessionDelegate、URLSessionDownloadDelegate 相关的代理协议方法(比如下载进度反馈,文件下载完成后将其移至用户文档目录保存)
import UIKit class DownloadManager: NSObject, URLSessionDelegate, URLSessionDownloadDelegate { //单例模式 static var shared = DownloadManager() //下载进度回调 var onProgress: ((Float) -> ())? //background session lazy var session:URLSession = { //只执行一次 let config = URLSessionConfiguration.background(withIdentifier: "background-session") let currentSession = URLSession(configuration: config, delegate: self, delegateQueue: nil) return currentSession }() //下载代理方法,下载结束 func urlSession(_ session: URLSession, downloadTask: URLSessionDownloadTask, didFinishDownloadingTo location: URL) { //下载结束 print("下载结束") if let onProgress = onProgress { onProgress(1) } //输出下载文件原来的存放目录 print("临时地址:\(location)") //location位置转换 let locationPath = location.path //拷贝到用户目录(文件名以时间戳命名) let fileName = date2String(Date(), dateFormat: "yyyyMMddHHmmss") let documnets = NSHomeDirectory() + "/Documents/" + fileName + ".tmp" //创建文件管理器 let fileManager = FileManager.default try! fileManager.moveItem(atPath: locationPath, toPath: documnets) print("文件保存到:\(documnets)") } //下载代理方法,监听下载进度 func urlSession(_ session: URLSession, downloadTask: URLSessionDownloadTask, didWriteData bytesWritten: Int64, totalBytesWritten: Int64, totalBytesExpectedToWrite: Int64) { //获取进度 let written = (Float)(totalBytesWritten) let total = (Float)(totalBytesExpectedToWrite) let pro = written/total if let onProgress = onProgress { onProgress(pro) } } //下载代理方法,下载偏移 func urlSession(_ session: URLSession, downloadTask: URLSessionDownloadTask, didResumeAtOffset fileOffset: Int64, expectedTotalBytes: Int64) { //下载偏移,主要用于暂停续传 } //session完成事件 func urlSessionDidFinishEvents(forBackgroundURLSession session: URLSession) { //主线程调用 DispatchQueue.main.async { if let appDelegate = UIApplication.shared.delegate as? AppDelegate, let completionHandler = appDelegate.backgroundSessionCompletionHandler { appDelegate.backgroundSessionCompletionHandler = nil //调用此方法告诉操作系统,现在可以安全的重新suspend你的app completionHandler() } } } //日期 -> 字符串 func date2String(_ date:Date, dateFormat:String = "yyyy-MM-dd HH:mm:ss") -> String { let formatter = DateFormatter() formatter.locale = Locale.init(identifier: "zh_CN") formatter.dateFormat = dateFormat let date = formatter.string(from: date) return date } }
(2)同时 AppDelegate.swift 中要做如下修改:
- 后台下载完毕后会调用 handleEventsForBackgroundURLSession 方法。
- 我们在此用提供的 identifier 创建新的 URLSessionConfiguration 和 URLSession 对象。
- 然后将新的 session 对象重新连接到先前的任务,并调用相应的 delegate。
注意:在前面 DownloadManager 里的 urlSessionDidFinishEvents() 这个 session 代理方法中,我们需要要在主线程里调用 AppDelegate 里保存的 completionHandler。这样就会告诉操作系统,现在可以安全的重新 suspend 我们的 app 了。
import UIKit @UIApplicationMain class AppDelegate: UIResponder, UIApplicationDelegate { //用于保存后台下载的completionHandler var backgroundSessionCompletionHandler: (() -> Void)? var window: UIWindow? //后台下载完毕后会调用(我们将其交由下载工具类做后续处理) func application(_ application: UIApplication, handleEventsForBackgroundURLSession identifier: String, completionHandler: @escaping () -> Void) { //用于保存后台下载的completionHandler backgroundSessionCompletionHandler = completionHandler //创建download session let configuration = URLSessionConfiguration.background(withIdentifier: identifier) let downloadssession = URLSession(configuration: configuration, delegate: DownloadManager.shared, delegateQueue: nil) //指定download session DownloadManager.shared.session = downloadssession } func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool { return true } func applicationWillResignActive(_ application: UIApplication) { } func applicationDidEnterBackground(_ application: UIApplication) { } func applicationWillEnterForeground(_ application: UIApplication) { } func applicationDidBecomeActive(_ application: UIApplication) { } func applicationWillTerminate(_ application: UIApplication) { } }
3,使用样例
(1)样例代码
import UIKit class ViewController: UIViewController { //点击按钮,开始下载 @IBAction func startDownload(_ sender: Any) { //下载地址 let url = URL(string: "https://dldir1.qq.com/dlomg/qqcom/mini/QQNewsMini5.exe") //请求 let request = URLRequest(url: url!) //下载任务 let downloadTask = DownloadManager.shared.session.downloadTask(with: request) //使用resume方法启动任务 downloadTask.resume() //实时打印出下载进度 DownloadManager.shared.onProgress = { (progress) in OperationQueue.main.addOperation { print("下载进度:\(progress)") } } } }
(2)运行效果:
- 点击界面上的按钮开始下载,可以看到控制台中不断地打印出下载进度。
- 按下 home 键将应用退到后台。虽然控制台不再继续打印下载进度,但事实上后台仍在继续下载,并在下载完毕后自动调用相关方法。