Swift--URLsession后臺下載

前言

URLSession是一個可以響應(yīng)發(fā)送或者接受HTTP請求的關(guān)鍵類。首先使用全局的 URLSession.shareddownloadTask 來創(chuàng)建一個簡單的下載任務(wù):

let url = URL(string: "https://mobileappsuat.pwchk.com/MobileAppsManage/UploadFiles/20190719144725271.png")
let request = URLRequest(url: url!)
let session = URLSession.shared
let downloadTask = session.downloadTask(with: request,
       completionHandler: { (location:URL?, response:URLResponse?, error:Error?)
        -> Void in
        print("location:\(location)")
        let locationPath = location!.path
        let documnets:String = NSHomeDirectory() + "/Documents/1.png"
        let fileManager = FileManager.default
        try! fileManager.moveItem(atPath: locationPath, toPath: documnets)
        print("new location:\(documnets)")
    })
downloadTask.resume()

可以看到這里的下載是前臺下載,也就是說如果程序退到后臺(比如按下 home 鍵、或者切到其它應(yīng)用程序上),當(dāng)前的下載任務(wù)便會立刻停止,這個樣話對于一些較大的文件,下載過程中用戶無法切換到后臺,對用戶來說是一種不太友好的體驗。下面來看一下在后臺下載的具體實現(xiàn):

URLsession后臺下載

我們可以通過URLSessionConfiguration類新建URLSession實例,而URLSessionConfiguration這個類是有三種模式的:

URLSessionConfiguration 的三種模如下式:

  • default:默認(rèn)會話模式(使用的是基于磁盤緩存的持久化策略,通常使用最多的也是這種模式,在default模式下系統(tǒng)會創(chuàng)建一個持久化的緩存并在用戶的鑰匙串中存儲證書)
  • ephemeral:暫時會話模式(該模式不使用磁盤保存任何數(shù)據(jù)。而是保存在 RAM 中,所有內(nèi)容的生命周期都與session相同,因此當(dāng)session會話無效時,這些緩存的數(shù)據(jù)就會被自動清空。)
  • background:后臺會話模式(該模式可以在后臺完成上傳和下載。)

注意:background模式與default模式非常相似,不過background模式會用一個獨立線程來進(jìn)行數(shù)據(jù)傳輸。background模式可以在程序掛起,退出,崩潰的情況下運行task。也可以在APP下次啟動的時候,利用標(biāo)識符來恢復(fù)下載。

下面先來創(chuàng)建一個后臺下載的任務(wù)background session,并且指定一個 identifier

let urlstring = URL(string: "https://dldir1.qq.com/qqfile/QQforMac/QQ_V6.5.5.dmg")!

// 第一步:初始化一個background后臺模式的會話配置configuration
let configuration = URLSessionConfiguration.background(withIdentifier: "com.Henry.cn")
 
// 第二步:根據(jù)配置的configuration,初始化一個session會話
let session = URLSession.init(configuration: configuration, delegate: self, delegateQueue: OperationQueue.main)

// 第三步:傳入URL,創(chuàng)建downloadTask下載任務(wù),開始下載
session.downloadTask(with: url).resume()

接下來實現(xiàn)session的下載代理URLSessionDownloadDelegateURLSessionDelegate的方法:

extension ViewController:URLSessionDownloadDelegate{
    // 下載代理方法,下載結(jié)束
    func urlSession(_ session: URLSession, downloadTask: URLSessionDownloadTask, didFinishDownloadingTo location: URL) {
        // 下載完成 - 開始沙盒遷移
        print("下載完成地址 - \(location)")
        let locationPath = location.path
        //拷貝到用戶目錄
        let documnets = NSHomeDirectory() + "/Documents/" + "com.Henry.cn" + ".dmg"
        print("移動到新地址:\(documnets)")
        //創(chuàng)建文件管理器
        let fileManager = FileManager.default
        try! fileManager.moveItem(atPath: locationPath, toPath: documnets)

    }
    //下載代理方法,監(jiān)聽下載進(jìn)度
    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è)置完這些代碼之后,還不能達(dá)到后臺下載的目的,還需要在AppDelegate中開啟后臺下載的權(quán)限,實現(xiàn)handleEventsForBackgroundURLSession方法:

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

實現(xiàn)到這里已基本實現(xiàn)了后臺下載的功能,在應(yīng)用程序切換到后臺之后,session 會和 ApplicationDelegate 做交互,session 中的task還會繼續(xù)下載,當(dāng)所有的task完成之后(無論下載失敗還是成功),系統(tǒng)都會調(diào)用ApplicationDelegateapplication:handleEventsForBackgroundURLSession:completionHandler:回調(diào),在處理事件之后,在 completionHandler參數(shù)中執(zhí)行 閉包,這樣應(yīng)用程序就可以獲取用戶界面的刷新。

如果我們查看handleEventsForBackgroundURLSession這個api的話,會發(fā)現(xiàn)蘋果文檔要求在實現(xiàn)下載完成后需要實現(xiàn)URLSessionDidFinishEvents的代理,以達(dá)到更新屏幕的目的。

func urlSessionDidFinishEvents(forBackgroundURLSession session: URLSession) {
    print("后臺任務(wù)")
    DispatchQueue.main.async {
        guard let appDelegate = UIApplication.shared.delegate as? AppDelegate, let backgroundHandle = appDelegate.backgroundSessionCompletionHandler else { return }
        backgroundHandle()
    }
}

如果沒有實現(xiàn)此方法的話???:后臺下載的實現(xiàn)是不會有影響的,只是在應(yīng)用程序由后臺切換到前臺的過程中可能會造成卡頓或者掉幀,同時可能在控制臺輸出警告:

Alamofire后臺下載

通過上面的例子??會發(fā)現(xiàn)如果要實現(xiàn)一個后臺下載,需要寫很多的代碼,同時還要注意后臺下載權(quán)限的開啟,完成下載之后回調(diào)的實現(xiàn),漏掉了任何一步,后臺下載都不可能完美的實現(xiàn),下面就來對比一下,在Alamofire中是怎么實現(xiàn)后臺下載的。

首先先創(chuàng)建一個ZHBackgroundManger的后臺下載管理類:

struct ZHBackgroundManger {    
    static let shared = ZHBackgroundManger()

    let manager: SessionManager = {
        let configuration = URLSessionConfiguration.background(withIdentifier: "com.Henry.AlamofireDemo")
        configuration.httpAdditionalHeaders = SessionManager.defaultHTTPHeaders
        configuration.timeoutIntervalForRequest = 10
        configuration.timeoutIntervalForResource = 10
        configuration.sharedContainerIdentifier = "com.Henry.AlamofireDemo"
        return SessionManager(configuration: configuration)
    }()
}

后臺下載的實現(xiàn):

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

并在AppDelegate做統(tǒng)一的處理:

func application(_ application: UIApplication, handleEventsForBackgroundURLSession identifier: String, completionHandler: @escaping () -> Void) {
    ZHBackgroundManger.shared.manager.backgroundCompletionHandler = completionHandler
}

這里可能會有疑問??,為甚么要創(chuàng)建一個ZHBackgroundManger單例類?

那么下面就帶著這個疑問?來探究一下

如果點擊ZHBackgroundManger.shared.manager.download這里的manager會發(fā)現(xiàn)這是SessionManager,那么就跟進(jìn)去SessionManager的源碼來看一下:


可以看到在SessionManagerdefault方法中,是對URLSessionConfiguration做了一些配置,并初始化SessionManager.

那么再來看SessionManager的初始化方法:


SessionManagerinit初始化方法中,可以看到這里把URLSessionConfiguration設(shè)置成default模式,在內(nèi)容的前篇,在創(chuàng)建一個URLSession的后臺下載的時候,我們已經(jīng)知道需要把URLSessionConfiguration設(shè)置成background模式才可以。

在初始化方法里還有一個SessionDelegatedelegate,而且這個delegate被傳入到URLSession中作為其代理,并且session的這個初始化也就使得以后的回調(diào)都將會由 self.delegate 來處理了。也就是SessionManager實例創(chuàng)建一個SessionDelegate對象來處理底層URLSession生成的不同類型的代理回調(diào)。(這又稱為代理移交)。

代理移交之后,在commonInit()的方法中會做另外的一些配置信息:


在這里delegate.sessionManager被設(shè)置為自身 self,而 self其實是持有 delegate 的。而且 delegatesessionManagerweak屬性修飾符。

這里這么寫delegate.sessionManager = self的原因是

  • delegate在處理回調(diào)的時候可以和sessionManager進(jìn)行通信
  • delegate將不屬于自己的回調(diào)處理重新交給sessionManager進(jìn)行再次分發(fā)
  • 減少與其他邏輯內(nèi)容的依賴

而且這里的delegate.sessionDidFinishEventsForBackgroundURLSession閉包,只要后臺任務(wù)下載完成就會回調(diào)到這個閉包內(nèi)部,在閉包內(nèi)部,回調(diào)了主線程,調(diào)用了 backgroundCompletionHandler,這也就是在AppDelegateapplication:handleEventsForBackgroundURLSession方法中的completionHandler。至此,SessionManager的流程大概就是這樣。

對于上面的疑問:

  • 1. 通過源碼我們可以知道SessionManager在設(shè)置URLSessionConfiguration的默認(rèn)的是default模式,因為需要后臺下載的話,就需要把URLSessionConfiguration的模式修改為background模式。包括我們也可以修改URLSessionConfiguration其他的配置
  • 2. 在下載的時候,應(yīng)用程序進(jìn)入到后臺下載,如果對于上面的配置,不做成一個單例的話,或者沒有被持有的情況下,在進(jìn)入后臺后就會被釋放掉,從而會產(chǎn)生錯誤Error Domain=NSURLErrorDomain Code=-999 "cancelled"
  • 3. 而且將SessionManager重新包裝成一個單例后,在AppDelegate中的代理方法中可以直接使用。

總結(jié)

  • 首先在 AppDelegateapplication:handleEventsForBackgroundURLSession的方法里,把回調(diào)閉包completionHandler傳給了 SessionManagerbackgroundCompletionHandler.
  • 在下載完成的時候 SessionDelegateurlSessionDidFinishEvents代理的調(diào)用會觸發(fā) SessionManagersessionDidFinishEventsForBackgroundURLSession代理的調(diào)用
  • 然后sessionDidFinishEventsForBackgroundURLSession 執(zhí)行SessionManagerbackgroundCompletionHandler的閉包.
  • 最后會來到 AppDelegateapplication:handleEventsForBackgroundURLSession的方法里的 completionHandler 的調(diào)用.

關(guān)于Alamofire后臺下載的代碼就分析到這里,其實通過源碼發(fā)現(xiàn),和利用URLSession進(jìn)行后臺下載原理是大致相同的,只不過利用Alamofire使代碼看起來更加簡介,而且Alamofire中會有很多默認(rèn)的配置,我們只需要修改需要的配置項即可。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。