Alamofire 後臺下載和流程分析

前言

在開發中,爲了提高用戶體驗經常會把下載大文件等網絡請求放到後臺下載,這樣即便用戶進入了後臺,任務也能繼續進行。那麼這篇文章就來討論下如何使用Apple原生框架URLSession的API和基於URLSession的第三方框架Alamofire來實現後臺下載功能。編程

經過URLSession實現後臺下載

1、 首先發起一個background模式的請求:api

// 初始化一個background的模式的configuration. Identifier:配置對象的惟一標識符
let configuration = URLSessionConfiguration.background(withIdentifier: "com.test")
// 建立session
let session = URLSession.init(configuration: configuration, delegate: self, delegateQueue: OperationQueue.main)
// 建立downloadTask任務,而後resume啓動
session.downloadTask(with: URL(string: urlStr).resume()
複製代碼
  • URLSessionConfiguration有三種模式,只有background 的模式才能進行後臺下載。
    • default:默認模式,一般咱們用這種模式就足夠了。default 模式下系統會建立一個持久化的緩存並在用戶的鑰匙串中存儲證書。
    • ephemeral:系統沒有任何持久性存儲,全部內容的生命週期都與 session 相同,當 session 無效時,全部內容自動釋放。
    • background: 建立一個能夠在後臺甚至APP已經關閉的時候仍然在傳輸數據的會話。
  • background 模式與 default 模式很是類似,只不過 background 模式會用一個獨立線程來進行數據傳輸。background 模式能夠在程序掛起,退出,崩潰的狀況下運行 task,也能夠利用標識符來恢復進。注意,後臺 Session 必定要是惟一的 identifier ,這樣在 APP 下次運行的時候,可以根據 identifier 來進行相關的區分。若是用戶關閉了 APP , iOS 系統會關閉全部的background Session。並且,被用戶強制關閉了之後,iOS 系統不會主動喚醒 APP,只有用戶下次啓動了 APP,數據傳輸纔會繼續。

2、 實現相關代理回調緩存

extension ViewController:URLSessionDownloadDelegate {
    // 下載完成回調
    func urlSession(_ session: URLSession, downloadTask: URLSessionDownloadTask, didFinishDownloadingTo location: URL) {
        let locationPath = location.path
        let documnets = NSHomeDirectory() + "/Documents/" + "\(Date().timeIntervalSince1970)" + ".mp4"
        let fileManager = FileManager.default
        //拷貝到用戶目錄
        try! fileManager.moveItem(atPath: locationPath, toPath: documnets)
    }
    
    // 監聽下載進度。http分片斷傳輸,因此這個代理會回調屢次
    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")
    }
}
複製代碼
  • 實現了URLSessionDownloadDelegate的兩個代理方法來監聽和處理下載的數據。

3、AppDelegate中處理後臺下載回調閉包bash

  • 當若是隻實現了前面兩個步驟,是沒法實現後臺下載功能的。這是由於漏掉了一個很是重要的步驟。
  • 首先在Appdelegate中實現handleEventsForBackgroundURLSession回調,而且保存完成block。
class AppDelegate: UIResponder, UIApplicationDelegate {
    var window: UIWindow?
    //用於保存後臺下載的completionHandler
    var backgroundSessionCompletionHandler: (() -> Void)?
    func application(_ application: UIApplication, handleEventsForBackgroundURLSession identifier: String, completionHandler: @escaping () -> Void) {
        self.backgroundSessionCompletionHandler = completionHandler
    }
}
複製代碼
  • 而後須要實現URLSessionDownloadDelegate代理的urlSessionDidFinishEvents方法,而且在主線程中執行在AppDelegate中保存的block。注意線程切換主線程,由於會刷新界面。
extension ViewController:URLSessionDownloadDelegate {
    func urlSessionDidFinishEvents(forBackgroundURLSession session: URLSession) {
        print("後臺任務下載回來")
        DispatchQueue.main.async {
            guard let appDelegate = UIApplication.shared.delegate as? AppDelegate, let backgroundHandle = appDelegate.backgroundSessionCompletionHandler else { return }
            backgroundHandle()
        }
    }
}
複製代碼
  • 完成如上幾個步驟就能夠實現後臺下載功能。
  • 應用程序在全部與URLSession對象關聯的後臺傳輸完成後調用此方法,不管傳輸成功完成仍是致使錯誤。
  • 保存handleEventsForBackgroundURLSession方法的completionHandler回調,這是很是重要的。告訴系統後臺下載回來及時刷新屏幕。
  • 在切到後臺以後,URLSessionDownloadDelegate 的代理方法不會再收到 Task 相關的消息。當全部 Task 全都完成後,系統纔會調用 AppDelegateapplication:handleEventsForBackgroundURLSession:completionHandler:回調。
  • 若是在urlSessionDidFinishEvents這個代理方法中不執行保存在AppDelegate裏面的blcok,會致使剛進入前臺時頁面卡頓,影響用戶體驗,同時還會打印警告信息。

經過Alamofire實現後臺下載

1、建立一個下載任務網絡

BackgroundManger.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)")
}
複製代碼
  • 這裏封裝了一個專門用來後臺下載管理類BackgroundManger,它是一個單例。若是你配置出來不作成單利,或者不被持有。那麼會在進入後臺後就會釋放,網絡也就會報錯:Error Domain=NSURLErrorDomain Code=-999 "cancelled"
  • 單例方便直接接收在AppDelegate中的application:handleEventsForBackgroundURLSession:completionHandler:方法的回調
  • Alamofire框架使用鏈式編程,寫起來很是的方便簡潔,邏輯清晰,可讀性強。
struct BackgroundManger {
    static let shared = BackgroundManger()
    let manager: SessionManager = {
        let configuration = URLSessionConfiguration.background(withIdentifier: "com.text.demo")
        configuration.httpAdditionalHeaders = SessionManager.defaultHTTPHeaders
        return SessionManager(configuration: configuration)
    }()
}
複製代碼

2、處理AppDelegate中的回調session

func application(_ application: UIApplication, handleEventsForBackgroundURLSession identifier: String, completionHandler: @escaping () -> Void) {
    BackgroundManger.shared.manager.backgroundCompletionHandler = completionHandler
}
複製代碼
  • 這一步的做用跟用URLSession實現後臺下載時的做用同樣。這行代碼很是重要,必定要加上,否則下載完成後進入前臺會有掉幀的狀況,影響用戶體驗。

3、SessionManger流程分析 Alamofire使用起來很是簡單,那麼它內部是怎樣幫咱們處理一些繁瑣的事情的呢,下面一塊兒來分析下:閉包

  • 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)
}
複製代碼
  • 首先經過外界傳進來的configuration初始化了session對象。
  • 對代理進行移交,經過建立 SessionDelegate 這個專門處理代理的類來實現 URLSession的代理。這樣能夠達到業務下沉,便於閱讀和解耦,每一個類只需負責本身的任務,分工明確,不至於臃腫。
  • SessionDelegate 類中實現的代理有URLSessionDelegate``URLSessionTaskDelegate``URLSessionDataDelegate``URLSessionDownloadDelegate``URLSessionStreamDelegate
  • 咱們知道,當後臺下載任務完成後回回調urlSessionDidFinishEvents這個代理方法
open func urlSessionDidFinishEvents(forBackgroundURLSession session: URLSession) {
    sessionDidFinishEventsForBackgroundURLSession?(session)
}
複製代碼
  • 執行了 sessionDidFinishEventsForBackgroundURLSession 閉包,那麼這個閉包在何時賦值的呢?由於SessionDelegate這個類是專門處理代理的類,不處理其餘邏輯,因此這個block應該是管理類SessionManger來處理的。這是一種很是重要的設計思惟。
  • 通過查找發如今SessionManger初始化方法裏面有一個commonInit函數的調用
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.sessionDidFinishEventsForBackgroundURLSession閉包的聲明,只要後臺下載完成就會執行這個閉包
  • 閉包內部又在主線程調用了 backgroundCompletionHandler , 這是 SessionManger 對外提供的功能。這個閉包就是在AppDelegate裏面handleEventsForBackgroundURLSession代理中保存的閉包
  • 流程總結:
    • 首先在AppDelegatehandleEventsForBackgroundURLSession方法裏,把回調閉包completionHandler傳給了 SessionManagerbackgroundCompletionHandler保存下來。
    • 當下載完成回來的時候 SessionDelegateurlSessionDidFinishEvents代理會調用,而後執行 sessionDidFinishEventsForBackgroundURLSession 閉包
    • sessionDidFinishEventsForBackgroundURLSession 閉包裏面會在主線程執行SessionManagerbackgroundCompletionHandler閉包,這個閉包就是 AppDelegatecompletionHandler 閉包。

總結

Alamofire是對URLSession進行封裝,因此這兩種方式進行後臺下載,原理是同樣的。只是 Alamofire 使用更加簡潔方便,依賴下沉,網絡層下沉。在本身寫sdk或者項目的時候也能夠參照這種設計思想去實現。app

有問題或者建議和意見,歡迎你們評論或者私信。 喜歡的朋友能夠點下關注和喜歡,後續會持續更新文章。框架

相關文章
相關標籤/搜索