美文网首页
Alamofire与原生URLSession后台下载

Alamofire与原生URLSession后台下载

作者: lb_ | 来源:发表于2019-08-18 16:47 被阅读0次

    后台下载顾名思义既 iOS 应用程序放到后台时继续执行下载任务。但还是有几处需要额外处理的,需要稍微注意下。

    URLSession后台下载

    先来看代码怎么写:

    创建

    // 1:初始化一个background的模式的configuration
    let configuration = URLSessionConfiguration.background(withIdentifier: "LBURLSessionID")
    
    // 2:通过configuration初始化 URLSession
    let session = URLSession.init(configuration: configuration, delegate: self, delegateQueue: OperationQueue.main)
    
    // 3:创建downloadTask任务  不要忘记resume启动
    session.downloadTask(with: self.lbFileurl).resume()
    
    URLSessionConfiguration 有三种模式:
    • default:默认模式,通常我们用这种模式就足够了。default 模式下系统会创建一个持久化的缓存并在用户的钥匙串中存储证书

    • ephemeral:系统没有任何持久性存储,所有内容的生命周期都与 session 相同,当 session 无效时,所有内容自动释放。

    • background: 创建一个可以在后台甚至 APP 已经关闭的时候仍然在传输数据的会话。

    background 模式与 default 模式非常相似,不过 background 模式会用一个独立线程来进行数据传输。background 模式可以在程序挂起,退出,崩溃的情况下运行 task。也可以利用标识符来恢复进。注意,后台 Session 一定要是唯一的 identifier ,这样在 APP 下次运行的时候,能够根据 identifier 来进行相关的区分。如果用户关闭了 APP , iOS 系统会关闭所有的 background Session。而且,被用户强制关闭了以后,iOS 系统不会主动唤醒 APP,只有用户下次启动了 APP,数据传输才会继续.

    代理方法

    //MARK: - session代理
    extension ViewController:URLSessionDownloadDelegate{
        func urlSession(_ session: URLSession, downloadTask: URLSessionDownloadTask, didFinishDownloadingTo location: URL) {
            // 下载完成开始沙盒迁移
            print("下载完成 - \(location)")
            let locationPath = location.path
            //拷贝到Documents目录
            let documnets = NSHomeDirectory() + "/Documents/" + lbTimeStap() + ".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")
        }
    }
    

    AppDelegate中处理后台下载回调闭包

    class AppDelegate: UIResponder, UIApplicationDelegate {
        var window: UIWindow?
        //用于保存后台下载的completionHandler
        var backgroundSessionCompletionHandler: (() -> Void)?
        func application(_ application: UIApplication, handleEventsForBackgroundURLSession identifier: String, completionHandler: @escaping () -> Void) {
            self.backgroundSessionCompletionHandler = completionHandler
        }
    }
    

    实现了这个方法就可以获得后台下载的能力了。

    URLSessionDidFinishEvents的代理调用AppDelegate中闭包

    func urlSessionDidFinishEvents(forBackgroundURLSession session: URLSession) {
        print("后台任务下载回来")
        DispatchQueue.main.async {
            guard let appDelegate = UIApplication.shared.delegate as? AppDelegate, let backgroundHandle = appDelegate.backgroundSessionCompletionHandler else { return }
            backgroundHandle()
        }
    }
    
    • 拿到UIApplication.shared.delegate的回调函数执行
    • 如果不实现这个代理里面的回调函数的执行,会爆出如下警告:
    Warning: Application delegate received call to - application:handleEventsForBackgroundURLSession:completionHandler: 
    but the completion handler was never called.
    

    application中 handleEventsForBackgroundURLSession的调用机制

    如果是一个 BackgroundSession,在Task执行的时候,用户切到后台,Session 会和 ApplicationDelegate 做交互。当程序切到后台后,在 BackgroundSession 中的Task还会继续下载.

    • 当加入了多个 Task,程序没有切换到后台。

    这种情况 Task 会按照 NSURLSessionConfiguration 的设置正常下载,不会和 ApplicationDelegate 有交互。

    • 当加入了多个 Task ,程序切到后台,所有 Task 都完成下载。

    在切到后台之后,Session的Delegate 不会再收到 Task 相关的消息,直到所有 Task 全都完成后,系统会调用 ApplicationDelegateapplication:handleEventsForBackgroundURLSession:completionHandler:回调,之后“汇报”下载工作,对于每一个后台下载的 Task 调用 Session的Delegate 中的 URLSession:downloadTask:didFinishDownloadingToURL:(成功的话)和 URLSession:task:didCompleteWithError:(成功或者失败都会调用)。
    之后调用 SessionDelegate 回调 URLSessionDidFinishEventsForBackgroundURLSession:

    • 当加入了多个 Task ,程序切到后台,下载完成了几个 Task,然后用户又切换到前台。(程序没有退出)

    切到后台之后,Session的Delegate 仍然收不到消息。在下载完成几个 Task 之后再切换到前台,系统会先汇报已经下载完成的 Task 的情况,然后继续下载没有下载完成的 Task,后面的过程同第一种情况。

    • 当加入了多个 Task,程序切到后台,几个 Task 已经完成,但还有 Task 还没有下载完的时候关掉强制退出程序,然后再进入程序的时候。(程序退出了)

    最后这个情况比较奇葩,由于程序已经退出了,后面没有下完 Session 就不在了后面的 Task 肯定是失败了。但是已经下载成功的那些 Task,新启动的程序也没有听“汇报”的机会了。经过实验发现,这个时候之前在 NSURLSessionConfiguration 设置的 NSString 类型的 ID 起作用了,当 ID 相同的时候,一旦生成 Session 对象并设置 Delegate ,马上可以收到上一次关闭程序之前没有汇报工作的 Task 的结束情况(成功或者失败)。但是当 ID 不相同,这些情况就收不到了,因此为了不让自己的消息被别的应用程序收到,或者收到别的应用程序的消息,起见 ID 还是和程序的 Bundle 名称绑定上比较好,至少保证唯一性。

    Alamofire后台下载

    同样先写代码实现:

    后台下载创建
    LBBackgroundManger.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)")
    }
    
    • 由于 Alamofirerequestreturn 处理 ,我们可以随意使用链式写法。
    • 封装了一个单例 LBBackgroundManger 的后台下载管理类。
    后台下载实例单例
    struct LBBackgroundManger {    
        static let shared = LBBackgroundManger()
        let manager: SessionManager = {
            let configuration = URLSessionConfiguration.background(withIdentifier: "LBURLSessionBGIdentifier")
            configuration.httpAdditionalHeaders = SessionManager.defaultHTTPHeaders
            configuration.timeoutIntervalForRequest = 30
            configuration.timeoutIntervalForResource = 30
            configuration.sharedContainerIdentifier = "LBURLSessionBGIdentifierGroup"
            return SessionManager(configuration: configuration)
        }()
    }
    

    写单例的主要目的是:

    • 相较于上面我们原生 URLSession 的写法,使用单例就可以摆脱作用域的限制,在 AppDelegateURLSessionDownloadDelegate 中都可以获取使用 ,也就是说在 AppDelegate 统一设置好以后 在应用层就无需在添加执行回调闭包处理。
    • 如果不使用单例,就需要将 Session 设置为属性,否则会因为释放问题无法触发回调。具体可以参考RXSwift常见问题和注意事项 中我举过一个小案例。
    AppDelegate中统一回调处理
    func application(_ application: UIApplication, handleEventsForBackgroundURLSession identifier: String, completionHandler: @escaping () -> Void) {
        LBBackgroundManger.shared.manager.backgroundCompletionHandler = completionHandler
    }
    

    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)
    }
    

    默认封装原生 URLSessionConfiguration,使用 default 模式。
    将代理移交给 SessionDelegate ,具体代理实现由该类处理,完成回调给 SessionManager

    • 代理类事件回调
    delegate.sessionDidFinishEventsForBackgroundURLSession = { [weak self] session in
        guard let strongSelf = self else { return }
        DispatchQueue.main.async { strongSelf.backgroundCompletionHandler?() }
    }
    

    SessionDelegate

    这个类包含了很多代理,其实就是实际干活的人。活干完了交给项目经理处理后续对接事情。

    open func urlSessionDidFinishEvents(forBackgroundURLSession session: URLSession) {
        sessionDidFinishEventsForBackgroundURLSession?(session)
    }
    

    下载任务完成时,由 URLSessionDownloadDelegate 代理方法触发执行。然后回调到 SessionManager 中,继续触发一系列 responseAppDelegatecompletionHandler 的调用。

    通过具体实现方法封装中间类,然后执行结果事件回调触发的方式,将实际使用达到最简化。也不同关心实际与自己关系不大的琐事。

    参考:
    cooci:https://juejin.im/post/5d57ff89f265da03913512df
    cooci:https://juejin.im/post/5d544099e51d4561df780588

    相关文章

      网友评论

          本文标题:Alamofire与原生URLSession后台下载

          本文链接:https://www.haomeiwen.com/subject/owicsctx.html