Alamofire-後臺下載

上一篇文章提到了後臺下載,下面看看在Alamofire中是如何處理後臺下載的。首先使用原生寫法來實現一個後臺下載任務,在使用Alamofire來實現,經過對比來看看Alamofire的優點。git

數據源地址:http://onapp.yahibo.top/public/videos/video.mp4github

1、URLSession後臺下載

首先須要建立會話並設置會話參數:json

//一、配置請求參數
let configuration = URLSessionConfiguration.background(withIdentifier: "com.yahibo.background_id")
let session = URLSession.init(configuration: configuration,delegate: self,delegateQueue: OperationQueue.main)
//二、設置數據源
let videoUrl = "http://onapp.yahibo.top/public/videos/video.mp4"
let url = URL.init(string: videoUrl)!
//三、建立一個下載任務,併發起請求
session.downloadTask(with: url).resume()複製代碼複製代碼
  • 配置會話爲background模式,開啓後臺下載功能
  • 建立下載任務並執行resume啓動任務
  • 會話初始化設置代理後,任務回調只走代理方法,不會經過閉包進行數據回調,若是使用閉包回傳也會報錯提示
    session.downloadTask(with: url) { (url, response, error) in
    print(url)
    print(response)
    print(error)
    }.resume()複製代碼複製代碼
    錯誤信息:Completion handler blocks are not supported in background sessions. Use a delegate instead.複製代碼複製代碼

    在後臺會話中不支持block塊回調數據,要求使用代理,所以在後臺下載中,咱們直接使用代理方法來處理數據。代理方法以下:swift

    extension Alamofire2Controller: URLSessionDownloadDelegate{
    //一、下載進度
    func urlSession(_ session: URLSession, downloadTask: URLSessionDownloadTask, didWriteData bytesWritten: Int64, totalBytesWritten: Int64, totalBytesExpectedToWrite: Int64) {
        print("下載進度:\(Double(totalBytesWritten)/Double(totalBytesExpectedToWrite))")
    }
    //二、下載完成
    func urlSession(_ session: URLSession, downloadTask: URLSessionDownloadTask, didFinishDownloadingTo location: URL) {
        let locationPath = location.path
        print("下載完成:\(location.path)")
        //存儲到用戶目錄
        let documents = NSHomeDirectory() + "/Documents/my.mp4"
        print("存儲位置:\(documents)")
        //複製視頻到目標地址
        let fileManager = FileManager.default
        try!fileManager.moveItem(atPath: locationPath, toPath: documents)
    }
    }複製代碼複製代碼

    實現了對下載任務進度的監聽,下載任務完成的監聽,在文件下載完成時首先會保存在沙盒中tmp文件下,該文件只存儲臨時數據,使用完後會自動清理,所以須要將tmp中下載的文件複製到Documents文件夾中存儲。api

經過打印的路徑查看文件下載狀況,以上操做實際並無真正完成後臺下載,應用返回後臺,下載任務就已中止,進入前臺才能看到下載完成,界面不可以及時更新。bash

下載進度:0.3653140762324527
下載進度:0.4018703091059228
2019-08-19 15:23:14.237923+0800 AlamofireDemo[849:9949] An error occurred on the xpc connection requesting pending callbacks for the background session: Error Domain=NSCocoaErrorDomain Code=4097 "connection to service named com.apple.nsurlsessiond" UserInfo={NSDebugDescription=connection to service named com.apple.nsurlsessiond}
下載完成:/Users/hibo/Library/Developer/CoreSimulator/Devices/404EDFDD-735E-454B-A576-70268D8A17C0/data/Containers/Data/Application/E3175312-D6B8-4576-9B84-4EBD7751A4C0/Library/Caches/com.apple.nsurlsessiond/Downloads/com.yahibo.background_id/CFNetworkDownload_eo4RMO.tmp
存儲位置:/Users/hibo/Library/Developer/CoreSimulator/Devices/404EDFDD-735E-454B-A576-70268D8A17C0/data/Containers/Data/Application/E3175312-D6B8-4576-9B84-4EBD7751A4C0/Documents/20190819152314.mp4複製代碼複製代碼

上篇文章有提到,蘋果官方要求在進行後臺任務下載時須要實現兩個代理方法,來及時通知系統更新界面。session

一、在AppDelegate中實現閉包

var backgroundCompletionHandler: (()->Void)? = nil
//設置此處開啓後臺下載權限
func application(_ application: UIApplication, handleEventsForBackgroundURLSession identifier: String, completionHandler: @escaping () -> Void) {
    self.backgroundCompletionHandler = completionHandler
}複製代碼複製代碼
  • 開啓後臺下載權限,實現代理方法即爲開通

二、在上面Alamofire2Controller擴展中實現代理方法併發

//後臺任務下載回調
func urlSessionDidFinishEvents(forBackgroundURLSession session: URLSession) {
    print("後臺任務下載回來")
    DispatchQueue.main.async {
        guard let appDelegate = UIApplication.shared.delegate as? AppDelegate, let backgroundHandle = appDelegate.backgroundCompletionHandler else { return }
        backgroundHandle()
    }
}複製代碼複製代碼
  • 後臺任務完成會調用該方法,在該方法內部調用AppDelegate中的閉包,通知系統更新界面,不然會出現掉幀

添加以上方法再次運行下載,退出前臺,等待幾秒鐘可以看到在控制檯是有後臺下載完成回調打印的,在該狀況下,咱們再次進入前臺,咱們的頁面實際上已經被更新了。至此咱們就完成了一個後臺下載的功能。app

總結:後臺下載任務須要實現四個代理方法

控制器:

  • URLSessionDownloadTask:獲取下載進度
  • didFinishDownloadingTo:下載完成處理下載文件
  • urlSessionDidFinishEvents:後臺下載完成調用,提示系統及時更新界面,執行Application中的閉包函數

Application:

  • backgroundCompletionHandler:後臺下載完成接收通知消息的閉包

從多年的開發經驗來看(太裝了������),以上這種實現方式其實不是理想結果,功能代碼分散。下面就看一下Alamofire是如何實現的。

2、Alamofire後臺下載

Alamofire.request(url,method: .post,parameters: ["page":"1","size":"20"]).responseJSON {
    (response) in
    switch response.result{
    case .success(let json):
        print("json:\(json)")
        break
    case .failure(let error):
        print("error:\(error)")
        break
    }
}複製代碼複製代碼

在以上代碼中,Alamofire能夠直接經過request發送請求,一樣在框架中也存在download方法來完成下載任務。查看官方文檔

//下載文件
Alamofire.download(url, to: { (url, response) -> (destinationURL: URL, options: DownloadRequest.DownloadOptions) in
    let documentsURL = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask)[0]
    let fileURL = documentsURL.appendingPathComponent("\(self.currentDateStr()).mp4")
    return (fileURL, [.removePreviousFile, .createIntermediateDirectories])
})
.downloadProgress { (progress) in
    print(progress)
}.response(queue: DispatchQueue.global(qos: .utility), completionHandler: { (response) in
    print("完成下載:\(response)")
})複製代碼複製代碼
  • DownloadRequest.DownloadOptions:設置下載文件的存儲地
  • downloadProgress:獲取下載進度

以上雖然能夠下載咱們須要的文件,可是不能在後臺下載。首先官方指出:

The Alamofire.download APIs should also be used if you need to download data while your app is in the background. For more information, please see the Session Manager Configurations section.

須要咱們手動配置會話爲background模式,而在以上使用的download中實際上使用的是default模式,並不能支持後臺下載。以下代碼:

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

    return SessionManager(configuration: configuration)
}()複製代碼複製代碼

經過官方文檔和源碼的查看,實際上咱們只須要從新設置會話的配置信息就能夠了。

修改會話模式

let configuration = URLSessionConfiguration.background(withIdentifier:"com.yahibo.background_id")
configuration.httpAdditionalHeaders = SessionManager.defaultHTTPHeaders
sessionManager = SessionManager(configuration: configuration)複製代碼複製代碼

以上sessionManager須要設置爲一個單例對象,以便於在後臺下載模式中接收Appdelegate的代理閉包函數,經過閉包通知系統及時更新界面。代碼以下:

struct BackgroundManager {
    static let shared = BackgroundManager()
    let manager: SessionManager = {
        let configuration = URLSessionConfiguration.background(withIdentifier:"com.yahibo.background_id")
        configuration.httpAdditionalHeaders = SessionManager.defaultHTTPHeaders
        return SessionManager(configuration: configuration)
    }()
}複製代碼複製代碼

下面就開始實現下載功能:

BackgroundManager.shared.manager.download(url) { (url, response) -> (destinationURL: URL, options: DownloadRequest.DownloadOptions) in
        let documentsURL = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask)[0]
        let fileURL = documentsURL.appendingPathComponent("\(self.currentDateStr()).mp4")
        return (fileURL, [.removePreviousFile, .createIntermediateDirectories])
    }.downloadProgress(queue: DispatchQueue.global(qos: .utility)) { (progress) in
        print(progress)
    }.response(queue: DispatchQueue.global(qos: .utility), completionHandler: { (response) in
        print("完成下載:\(response)")
    })複製代碼複製代碼
  • 同上直接調用download方法來下載,並存儲數據

應蘋果要求咱們還須要調用handleEventsForBackgroundURLSession中的的代碼塊,通知系統及時更新界面,在SessionManager中如何作鏈接呢。代碼以下:

//設置此處開啓後臺下載權限
func application(_ application: UIApplication, handleEventsForBackgroundURLSession identifier: String, completionHandler: @escaping () -> Void) {
    BackgroundManager.shared.manager.backgroundCompletionHandler = completionHandler
}複製代碼複製代碼
  • SessionManager中已經備好了須要的backgroundCompletionHandler代碼塊聲明,以便接收閉包,調用閉包

簡單幾步就實現了咱們想要的後臺下載功能了,編碼簡潔,邏輯清晰。這裏咱們只在Application中實現了開啓後臺下載權限的代理,但並無在控制器中設置delegate和實現urlSessionDidFinishEvents代理方法,這裏不難猜想URLSessionDownloadTask、didFinishDownloadingTo、urlSessionDidFinishEvents代理方法應該是在SessionManager中實現,統一管理再以閉包的形式回傳到當前界面。下面就看一下SessionManager是否是這麼實現的。

3、SessionManager源碼探索

首先順着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)
}複製代碼複製代碼

初始化有三個初始參數,並設有缺省值,該方法返回一個新的SessionManager對象。在上面後臺下載中咱們只配置了configuration參數,設置爲了後臺下載模式。上面也提到了,在SessionManager中應該是有咱們的後臺下載相關的代理實現,在該函數中看到初始化了一個SessionDelegate對象,並將URLSession的代理實現指向了SessionDelegate對象,不難猜出URLSession相關的代理方法應該都在SessionDelegate類中實現。

SessionDelegate

SessionDelegate.swift中,SessionDelegate繼承自NSObject,聲明瞭全部與URLSession代理相關連的閉包函數,用來向界面回傳代理事件產生的結果。

在擴展方法中實現瞭如下幾個代理的方法:

URLSessionDelegate URLSessionTaskDelegate URLSessionDataDelegate URLSessionDownloadDelegate URLSessionStreamDelegate

下面就看一下下載相關的代理方法內部實現了哪些功能。代碼以下:

extension SessionDelegate: URLSessionDownloadDelegate {

    open func urlSession(
        _ session: URLSession,
        downloadTask: URLSessionDownloadTask,
        didFinishDownloadingTo location: URL)
    {
        if let downloadTaskDidFinishDownloadingToURL = downloadTaskDidFinishDownloadingToURL {
            downloadTaskDidFinishDownloadingToURL(session, downloadTask, location)
        } else if let delegate = self[downloadTask]?.delegate as? DownloadTaskDelegate {
            delegate.urlSession(session, downloadTask: downloadTask, didFinishDownloadingTo: location)
        }
    }

    open func urlSession(
        _ session: URLSession,
        downloadTask: URLSessionDownloadTask,
        didWriteData bytesWritten: Int64,
        totalBytesWritten: Int64,
        totalBytesExpectedToWrite: Int64)
    {
        if let downloadTaskDidWriteData = downloadTaskDidWriteData {
            downloadTaskDidWriteData(session, downloadTask, bytesWritten, totalBytesWritten, totalBytesExpectedToWrite)
        } else if let delegate = self[downloadTask]?.delegate as? DownloadTaskDelegate {
            delegate.urlSession(
                session,
                downloadTask: downloadTask,
                didWriteData: bytesWritten,
                totalBytesWritten: totalBytesWritten,
                totalBytesExpectedToWrite: totalBytesExpectedToWrite
            )
        }
    }

    open func urlSession(
        _ session: URLSession,
        downloadTask: URLSessionDownloadTask,
        didResumeAtOffset fileOffset: Int64,
        expectedTotalBytes: Int64)
    {
        if let downloadTaskDidResumeAtOffset = downloadTaskDidResumeAtOffset {
            downloadTaskDidResumeAtOffset(session, downloadTask, fileOffset, expectedTotalBytes)
        } else if let delegate = self[downloadTask]?.delegate as? DownloadTaskDelegate {
            delegate.urlSession(
                session,
                downloadTask: downloadTask,
                didResumeAtOffset: fileOffset,
                expectedTotalBytes: expectedTotalBytes
            )
        }
    }
}複製代碼複製代碼

以上三個方法用來監控下載進度,及下載是否完成,在回調內部經過閉包回調代理事件到主界面。該文件中實現了上面提到的代理的全部方法,經過聲明的閉包向外界傳值,在外部只須要調用閉包便可。這裏和外界橋接的閉包函數返回一個self,所以可以以鏈式的形式,來獲取代理傳來的數據。以下:

open func downloadProgress(queue: DispatchQueue = DispatchQueue.main, closure: @escaping ProgressHandler) -> Self {
    downloadDelegate.progressHandler = (closure, queue)
    return self
}複製代碼複製代碼
  • 橋接界面與內部SessionDelegate擴展代理,完成下載進度的監聽

其餘橋接方法省略……

針對後臺下載找到了繼承自URLSessionDelegate的擴展:

extension SessionDelegate: URLSessionDelegate {
    open func urlSessionDidFinishEvents(forBackgroundURLSession session: URLSession) {
        sessionDidFinishEventsForBackgroundURLSession?(session)
    }
}複製代碼複製代碼

後臺下載完成,會執行該方法,在該方法中,調用了外界實現的閉包,此閉包實如今SessionManager中,以下:

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?() }
    }
}複製代碼複製代碼
  • SessionDelegate中傳入self,此處出現循環引用,這裏的delegate.sessionManager使用weak修飾解決
  • 實現delegate中後臺下載完成回調閉包,在此處接收後臺下載完成消息
  • 在主線程中,調用backgroundCompletionHandler將消息發送至backgroundCompletionHandler的閉包實現

這裏應該就清楚了,backgroundCompletionHandlerSessionManager聲明的閉包,在Application中獲取系統閉包實現,用來與系統通信,告訴系統在後臺及時更新界面。

來源:本文爲第三方轉載,若有侵權請聯繫小編刪除。 

相關文章
相關標籤/搜索