Alamofire之后台下载

2019-08-19  本文已影响0人  越来越胖了

后台下载的一般代码写法如下:

SessionManager.default.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)")
        }

一步步分析,首先查看SessionManager.default到底做了什么:

open class SessionManager {
…
 public static let `default`: SessionManager = {
        let configuration = URLSessionConfiguration.default
        configuration.httpAdditionalHeaders = SessionManager.defaultHTTPHeaders

        return SessionManager(configuration: configuration)
    }()
…
}

configuration.httpAdditionalHeaders = SessionManager.defaultHTTPHeaders 配置头部信息 ,可以做一个基本的网络校验;return SessionManager(configuration: configuration);调用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)
    }

这里很重要的一点:delegate一般我们传self,这里传的是SessionDelegate()这样一个class,进行了代理移交;然后调用commonInit(serverTrustPolicyManager: serverTrustPolicyManager),代码如下:

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

delegate.sessionManager = self;delegate为 public let delegate: SessionDelegate,这个delegate就是之前代理移交的class,这个class类的sessionManager == self,也就是 == SessionManager;所以很明显,需要处理循环引用

 weak var sessionManager: SessionManager?

那为什么要传入sessionManager呢?目的是为了所有的事情都通过sessionManager来统一调配,信息回传,减少依赖。如果不传入manager调配,下层的通信会非常的错乱,各种依赖难以管理。如图:红线杂乱无章,通过蓝线沟通manager统一调配就好多了.能把这么抽象的思维画的这么通俗易懂,我都佩服我自己🙃

灵魂画手🙄

传入sessionManager后,delegate设置了如下一个闭包:

 delegate.sessionDidFinishEventsForBackgroundURLSession = { [weak self] session in
            guard let strongSelf = self else { return }
            DispatchQueue.main.async { strongSelf.backgroundCompletionHandler?() }
        }

这个是后台下载完成后的事件回传,去执行backgroundCompletionHandler方法.

未完待续,广告之后马上回来~~~~

下面开始后台下载:

SessionManager.default.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)")
        }

然而我们发现这样并不能后台下载,原因是default不支持后台下载。

  • default :默认模式,通常我们用这种模式就够了,default模式下系统会创建一个持久化的缓存并在用户的钥匙串中存储证书;
  • ephemeral :系统没有任何持久化存储,所有的内容生命周期都和session相同,当session无效时,所有内容自动释放;
  • background: 创建一个可以在后台甚至是APP已经关闭的时候仍然在传输数据的会话

所以修改如下,主要就是对sessionManager进行后台下载的配置:

let configuration = URLSessionConfiguration.background(withIdentifier: "com.lgcooci.backgroundDownload")
 let manager = SessionManager(configuration: configuration)
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)")
        }

这里就做一个最简单的配置了,其实URLSessionConfiguration有很多的属性可以让我们去配置:

  • open var identifier: String? { get } //配置对象的后台会话标识符;
  • open var httpAdditionalHeaders: [AnyHashable : Any]?//于请求一起发送的附加头文件的字典
  • open var timeoutIntervalForRequest: TimeInterval //等待其他数据是使用的超时间隔
  • open var timeoutIntervalForResource: TimeInterval //资源请求允许的最大时间量
  • open var networkServiceType: NSURLRequest.NetworkServiceType//网络服务类型
  • open var allowsCellularAccess: Bool //用于确认是否应该通过蜂窝网络进行连接
  • open var sharedContainerIdentifier: String? //后台下载url会话中的文件的共享容器的标识符。
  • open var waitsForConnectivity: Bool //指示会话是否应该等待连接变为可用或者立即失败

启动下载,发现,提示: Task <EFAB535F-86EA-4557-8F4C-A5E3B153123A>.<1> load failed with error Error Domain=NSURLErrorDomain Code=-999 "cancelled"

后台下载,意味着程序进入后台时,我们需要持有这个下载对象,所以改成全局的。

let urlDownloadStr = "http://dldir1.qq.com/qqfile/QQforMac/QQ_V6.5.5.dmg"
 var manager = SessionManager()

 override func viewDidLoad() {
        super.viewDidLoad()
        print("------")
    }

  override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {

        let configuration = URLSessionConfiguration.background(withIdentifier: "com.lgcooci.backgroundDownload")
        manager = SessionManager(configuration: configuration)
        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)")
        }
}

进入后台后,发现没有继续下载,也没有完成的输出,权限问题:

func application(_ application: UIApplication, handleEventsForBackgroundURLSession identifier: String, completionHandler: @escaping () -> Void) {

        print("在后台下载完成了~~~")
       
    }

很多人写到这,感觉就完事了,其实不然,查看苹果文档对这个方法的描述如下:

Discussion
The app calls this method after all background transfers associated with an URLSession object are done, whether they finished successfully or resulted in an error. The app also calls this method if authentication is required for one or more transfers.
Use this method to reconnect any URL sessions and to update your app’s user interface. For example, you might use this method to update progress indicators or to incorporate new content into your views. After processing the events, execute the block in the completionHandler parameter so that the app can take a new snapshot of your user interface.
If a URL session finishes its work when your app is not running, the system launches your app in the background so that it can process the event. In that situation, use the provided identifier to create a new URLSessionConfiguration and URLSession object. You must configure the other options of your URLSessionConfiguration object in the same way that you did when you started the uploads or downloads. Upon creating and configuring the new URLSession object, that object calls the appropriate delegate methods to process the events.
If your app already has a session object with the specified identifier and is running or suspended, you do not need to create a new session object using this method. Suspended apps are moved into the background. As soon as the app is running again, the URLSession object with the identifier receives the events and processes them normally.
At launch time, the app does not call this method if there are uploads or downloads in progress but not yet finished. If you want to display the current progress of those transfers in your app’s user interface, you must recreate the session object yourself. In that situation, cache the identifier value persistently and use it to recreate your session object.

噼里啪啦一大堆,其实就是告诉我们,下载完成了,需要去调用completionHandler方法,不然会屏幕刷新、掉帧,而且没有调用的情况下,我们会收到一条警告⚠️:Warning: Application delegate received call to application:handleEventsForBackgroundURLSession:completionHandler: but the completion handler was never called.

这里我们采取创建一个单例的方式去调用系统的completionHandler方法:

struct LGBackgroundManger {
    
    static let shared = LGBackgroundManger()
    
    let manager: SessionManager = {
        let configuration = URLSessionConfiguration.background(withIdentifier: "com.lgcooci.AlamofireTest.demo")
        configuration.httpAdditionalHeaders = SessionManager.defaultHTTPHeaders
        return SessionManager(configuration: configuration)
    }()
}

所以后台回调方法如下:

 func application(_ application: UIApplication, handleEventsForBackgroundURLSession identifier: String, completionHandler: @escaping () -> Void) {

LGBackgroundManger.shared.manager.backgroundCompletionHandler = completionHandler
       
    }

为什么我们可以直接设置LGBackgroundManger.shared.manager.backgroundCompletionHandler 呢?因为sessionManager内部已经帮我们调用了sessionDidFinishEventsForBackgroundURLSession方法了:

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

sessionDidFinishEventsForBackgroundURLSession?(session)其实在初始化时声明的闭包:

 delegate.sessionDidFinishEventsForBackgroundURLSession = { [weak self] session in
            guard let strongSelf = self else { return }
            DispatchQueue.main.async { strongSelf.backgroundCompletionHandler?() }
        }

以上逻辑捋一捋:

为什么SessionManager有一个方法backgroundCompletionHandler可以让我们去实现呢?怎么做到的?其实就是一个sessionManager的open的属性:

 /// The background completion handler closure provided by the UIApplicationDelegate
    /// `application:handleEventsForBackgroundURLSession:completionHandler:` method. By setting the background
    /// completion handler, the SessionDelegate `sessionDidFinishEventsForBackgroundURLSession` closure implementation
    /// will automatically call the handler.
    ///
    /// If you need to handle your own events before the handler is called, then you need to override the
    /// SessionDelegate `sessionDidFinishEventsForBackgroundURLSession` and manually call the handler when finished.
    ///
    /// `nil` by default.
    open var backgroundCompletionHandler: (() -> Void)?

最后附上demo参考:后台下载

注:后台下载代理的回调,前提是进入了后台,如果直接在前台完成了下载任务,是不会触发后台下载代理的🙄。

上一篇 下一篇

猜你喜欢

热点阅读