iOS开发Swift

Swift – 实现大文件的后台下载功能(附样例)

    我之前写过一篇文章介绍如何使用 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
  • 实现 URLSessionDelegateURLSessionDownloadDelegate 相关的代理协议方法(比如下载进度反馈,文件下载完成后将其移至用户文档目录保存)
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 键将应用退到后台。虽然控制台不再继续打印下载进度,但事实上后台仍在继续下载,并在下载完毕后自动调用相关方法。