Alamofire (2)—— 后臺(tái)下載

??????Alamofire專題目錄,歡迎及時(shí)反饋交流 ??????


Alamofire 目錄直通車 --- 和諧學(xué)習(xí),不急不躁!


這一篇主要講解后臺(tái)下載,后臺(tái)下載對(duì)于應(yīng)用程序來說,是一個(gè)非常重要也比較好用的功能。雖然用好后臺(tái)下載的確能夠大大提升用戶體驗(yàn),但是又很多時(shí)候我們也會(huì)遇到很多坑點(diǎn)以及疑惑點(diǎn)。其中會(huì)通過 URLSessionAlamofire 兩種形式分別展開討論,對(duì)比學(xué)習(xí)才能更能體會(huì) Alamofire 的設(shè)計(jì)思維。Alamofire持續(xù)更新中,希望大家希望!

一、URLSession處理后臺(tái)下載

URLSession在后臺(tái)處理方面還是比較簡單的。

// 1:初始化一個(gè)background的模式的configuration
let configuration = URLSessionConfiguration.background(withIdentifier: self.createID())
// 2:通過configuration初始化網(wǎng)絡(luò)下載會(huì)話
let session = URLSession.init(configuration: configuration, delegate: self, delegateQueue: OperationQueue.main)
// 3:session創(chuàng)建downloadTask任務(wù)-resume啟動(dòng)
session.downloadTask(with: url).resume()
  • 初始化一個(gè) background 的模式的 configurationconfiguration 三種模式 ,只有background 的模式才能進(jìn)行后臺(tái)下載。
  • 通過configuration初始化網(wǎng)絡(luò)下載會(huì)話 session,設(shè)置相關(guān)代理,回調(diào)數(shù)據(jù)信號(hào)響應(yīng)。
  • session創(chuàng)建downloadTask任務(wù)-resume啟動(dòng) (默認(rèn)狀態(tài):suspend)
  • 接下來依賴蘋果封裝的網(wǎng)絡(luò)處理,發(fā)起連接 - 發(fā)送相關(guān)請(qǐng)求 - 回調(diào)代理響應(yīng)
//MARK: - session代理
extension ViewController:URLSessionDownloadDelegate{
    func urlSession(_ session: URLSession, downloadTask: URLSessionDownloadTask, didFinishDownloadingTo location: URL) {
        // 下載完成 - 開始沙盒遷移
        print("下載完成 - \(location)")
        let locationPath = location.path
        //拷貝到用戶目錄(文件名以時(shí)間戳命名)
        let documnets = NSHomeDirectory() + "/Documents/" + self.lgCurrentDataTurnString() + ".mp4"
        print("移動(dòng)地址:\(documnets)")
        //創(chuàng)建文件管理器
        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("下載進(jìn)度: \(Double(totalBytesWritten)/Double(totalBytesExpectedToWrite))\n")
    }
}
  • 實(shí)現(xiàn)了 URLSessionDownloadDelegatedidFinishDownloadingTo代理,實(shí)現(xiàn)下載完成轉(zhuǎn)移臨時(shí)文件里的數(shù)據(jù)到相應(yīng)沙盒保存
  • 通過urlSession(_ session: downloadTask:didWriteData bytesWritten: totalBytesWritten: totalBytesExpectedToWrite: ) 的代理監(jiān)聽下載進(jìn)度
  • 這里也是因?yàn)?http的分片傳輸 才導(dǎo)致的進(jìn)度有段的感覺,其實(shí)證明內(nèi)部也是對(duì)這個(gè)代理方法不斷調(diào)用,才能進(jìn)度回調(diào)!

這里實(shí)現(xiàn)了下載功能,但是對(duì)于我們需要的后臺(tái)下載還差一段

Applications using an NSURLSession with a background configuration may be launched or resumed in the background in order to handle the completion of tasks in that session, or to handle authentication. This method will be called with the identifier of the session needing attention. Once a session has been created from a configuration object with that identifier, the session's delegate will begin receiving callbacks. If such a session has already been created (if the app is being resumed, for instance), then the delegate will start receiving callbacks without any action by the application. You should call the completionHandler as soon as you're finished handling the callbacks.

蘋果爸爸總是能在合適時(shí)間給你優(yōu)秀的建議,閱讀文檔的能力決定你是否能夠在這個(gè)時(shí)代站穩(wěn)自己的腳尖

class AppDelegate: UIResponder, UIApplicationDelegate {
    var window: UIWindow?
    //用于保存后臺(tái)下載的completionHandler
    var backgroundSessionCompletionHandler: (() -> Void)?
    func application(_ application: UIApplication, handleEventsForBackgroundURLSession identifier: String, completionHandler: @escaping () -> Void) {
        self.backgroundSessionCompletionHandler = completionHandler
    }
}
  • 實(shí)現(xiàn)handleEventsForBackgroundURLSession就可以完美后臺(tái)下載
  • 告訴代理與 URLSession 相關(guān)的事件正在等待處理。
  • 應(yīng)用程序在所有與 URLSession對(duì)象 關(guān)聯(lián)的后臺(tái)傳輸完成后調(diào)用此方法,無論傳輸成功完成還是導(dǎo)致錯(cuò)誤。如果一個(gè)或多個(gè)傳輸需要認(rèn)證,應(yīng)用程序也會(huì)調(diào)用這個(gè)方法。
  • 使用此方法可以重新連接任何 URLSession 并更新應(yīng)用程序的用戶界面。例如,您可以使用此方法更新進(jìn)度指示器或?qū)⑿聝?nèi)容合并到視圖中。在處理事件之后,在 completionHandler 參數(shù)中執(zhí)行 block,這樣應(yīng)用程序就可以獲取用戶界面的刷新。
  • 我們通過handleEventsForBackgroundURLSession保存相應(yīng)的回調(diào),這也是非常必要的!告訴系統(tǒng)后臺(tái)下載回來及時(shí)刷新屏幕

urlSessionDidFinishEvents的代理實(shí)現(xiàn)調(diào)用

func urlSessionDidFinishEvents(forBackgroundURLSession session: URLSession) {
    print("后臺(tái)任務(wù)下載回來")
    DispatchQueue.main.async {
        guard let appDelegate = UIApplication.shared.delegate as? AppDelegate, let backgroundHandle = appDelegate.backgroundSessionCompletionHandler else { return }
        backgroundHandle()
    }
}
  • 拿到UIApplication.shared.delegate的回調(diào)函數(shù)執(zhí)行
  • 注意線程切換主線程,畢竟刷新界面

那么如果不實(shí)現(xiàn)這個(gè)代理里面的回調(diào)函數(shù)的執(zhí)行,那么會(huì)發(fā)生什么呢

  • 后臺(tái)下載的能力是不會(huì)影響的
  • 但是會(huì)爆出非常驗(yàn)證界面刷新卡頓,影響用戶體驗(yàn)
  • 同時(shí)打印臺(tái)會(huì)爆出警告
Warning: Application delegate received call to -
application:handleEventsForBackgroundURLSession:completionHandler: 
but the completion handler was never called.

二、Alamofire后臺(tái)下載

Alamofire框架還是比較有感覺的,這個(gè)節(jié)奏也是函數(shù)式回調(diào),還支持鏈?zhǔn)秸?qǐng)求和響應(yīng)!事務(wù)邏輯非常清晰,還有代碼可讀性也是非常簡潔

LGBackgroundManger.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("下載回調(diào)信息: \(downloadResponse)")
    }
    .downloadProgress { (progress) in
        print("下載進(jìn)度 : \(progress)")
}
  • 這里封裝了一個(gè)單利LGBackgroundManger的后臺(tái)下載管理類,調(diào)用manger的手法也是非常直接。
  • 封裝的思想再也不需要去處理惡心的代理事件
struct LGBackgroundManger {    
    static let shared = LGBackgroundManger()

    let manager: SessionManager = {
        let configuration = URLSessionConfiguration.background(withIdentifier: "com.lgcooci.AlamofireTest.demo")
        configuration.httpAdditionalHeaders = SessionManager.defaultHTTPHeaders
        configuration.timeoutIntervalForRequest = 10
        configuration.timeoutIntervalForResource = 10
        configuration.sharedContainerIdentifier = "group.com.lgcooci.AlamofireTest"
        return SessionManager(configuration: configuration)
    }()
}

可能很多同學(xué)都在質(zhì)疑為什么要做成單利,URLSession的時(shí)候不是挺好的?

  • 如果你是 SessionManager.defalut 顯然是不可以的!畢竟要求后臺(tái)下載,那么我們的會(huì)話 session 的配置 URLSessionConfiguration 是要求 background模式的
  • 如果你配置出來不做成單利,或者不被持有!在進(jìn)入后臺(tái)就會(huì)釋放,網(wǎng)絡(luò)也就會(huì)報(bào)錯(cuò):Error Domain=NSURLErrorDomain Code=-999 "cancelled"
  • 應(yīng)用層與網(wǎng)絡(luò)層也可以達(dá)到分離。
  • 能夠幫助在AppDelegate 的回調(diào)方便直接接收
func application(_ application: UIApplication, handleEventsForBackgroundURLSession identifier: String, completionHandler: @escaping () -> Void) {
    LGBackgroundManger.shared.manager.backgroundCompletionHandler = completionHandler
}

三、SessionManger流程分析

一篇優(yōu)秀的博客,畢竟還要跟大家交代這樣清晰的代碼的背后流程

1、SessionManger初始化
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)
}
  • 初始化了session,其中configurationdefault的模式,設(shè)置了一些基本的 SessionManager.defaultHTTPHeaders 請(qǐng)求頭信息
  • 代理移交,通過創(chuàng)建 SessionDelegate 這個(gè)專門處理代理的類來實(shí)現(xiàn) URLSession的代理
2、代理完成回調(diào)

SessionDelegate 是一個(gè)非常重要的類,集合所有的代理

  • URLSessionDelegate
  • URLSessionTaskDelegate
  • URLSessionDataDelegate
  • URLSessionDownloadDelegate
  • URLSessionStreamDelegate

這里我們根據(jù)需求來到 urlSessionDidFinishEvents 的代理

open func urlSessionDidFinishEvents(forBackgroundURLSession session: URLSession) {
    sessionDidFinishEventsForBackgroundURLSession?(session)
}
  • 這里執(zhí)行了 sessionDidFinishEventsForBackgroundURLSession 閉包的執(zhí)行,那么這個(gè)閉包在什么時(shí)候申明的呢?
  • 如果你足夠聰明,這里你應(yīng)該是能夠想到的,SessionDelegate只是處理代理的專門類,但不是邏輯數(shù)據(jù)的處理類,按照封裝設(shè)計(jì)的常規(guī)思路必將交給管理者類來下發(fā)

在我們的 SessionManger 里面的初始化的時(shí)候,有一個(gè)方法commonInit

delegate.sessionDidFinishEventsForBackgroundURLSession = { [weak self] session in
    guard let strongSelf = self else { return }
    DispatchQueue.main.async { strongSelf.backgroundCompletionHandler?() }
}
  • 這里就是代理的delegate.sessionDidFinishEventsForBackgroundURLSession閉包的聲明
  • 只要后臺(tái)下載完成就會(huì)來到這個(gè)閉包內(nèi)部
  • 回調(diào)了主線程,調(diào)用了 backgroundCompletionHandler , 這也是 SessionManger 對(duì)外提供的功能!聰明的你應(yīng)該知道知道了我在application的操作的本質(zhì)了!

3、流程總結(jié)

  • 首先在 AppDelegatehandleEventsForBackgroundURLSession方法里,把回調(diào)閉包傳給了 SessionManagerbackgroundCompletionHandler
  • 在下載完成回來的時(shí)候 SessionDelegateurlSessionDidFinishEvents代理的調(diào)用 -> sessionDidFinishEventsForBackgroundURLSession 調(diào)用
  • 然后sessionDidFinishEventsForBackgroundURLSession 執(zhí)行 -> SessionManagerbackgroundCompletionHandler的執(zhí)行
  • 最后導(dǎo)致 AppDelegatecompletionHandler 的調(diào)用

無論你是使用 URLSession 的方式,還是 Alamofire 進(jìn)行后臺(tái)下載,但是原理還是一樣的,只是 Alamofire 使用更加達(dá)到依賴下沉,網(wǎng)絡(luò)層下沉,使用更簡潔,這也是很多時(shí)候我們需要第三方框架的原因。這一篇你估計(jì)已經(jīng)感受到了 Alamofire 的舒服,那么如果你喜歡的話,麻煩點(diǎn)心,關(guān)注一下。我會(huì)持續(xù)更新一個(gè) Alamofire 的系列專題,謝謝!

就問此時(shí)此刻還有誰?45度仰望天空,該死!我這無處安放的魅力!

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

友情鏈接更多精彩內(nèi)容