Swift--URLsession後臺下載

前言

URLSession是一個能夠響應發送或者接受HTTP請求的關鍵類。首先使用全局的 URLSession.shareddownloadTask 來建立一個簡單的下載任務:api

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 鍵、或者切到其它應用程序上),當前的下載任務便會馬上中止,這個樣話對於一些較大的文件,下載過程當中用戶沒法切換到後臺,對用戶來講是一種不太友好的體驗。下面來看一下在後臺下載的具體實現:緩存

URLsession後臺下載

咱們能夠經過URLSessionConfiguration類新建URLSession實例,而URLSessionConfiguration這個類是有三種模式的: bash

URLSessionConfiguration 的三種模以下式:session

  • default:默認會話模式(使用的是基於磁盤緩存的持久化策略,一般使用最多的也是這種模式,在default模式下系統會建立一個持久化的緩存並在用戶的鑰匙串中存儲證書)
  • ephemeral:暫時會話模式(該模式不使用磁盤保存任何數據。而是保存在 RAM 中,全部內容的生命週期都與session相同,所以當session會話無效時,這些緩存的數據就會被自動清空。)
  • background:後臺會話模式(該模式能夠在後臺完成上傳和下載。)

注意:background模式與default模式很是類似,不過background模式會用一個獨立線程來進行數據傳輸。background模式能夠在程序掛起,退出,崩潰的狀況下運行task。也能夠在APP下次啓動的時候,利用標識符來恢復下載。閉包

下面先來建立一個後臺下載的任務background session,而且指定一個 identifierapp

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")
 
// 第二步:根據配置的configuration,初始化一個session會話
let session = URLSession.init(configuration: configuration, delegate: self, delegateQueue: OperationQueue.main)

// 第三步:傳入URL,建立downloadTask下載任務,開始下載
session.downloadTask(with: url).resume()
複製代碼

接下來實現session的下載代理URLSessionDownloadDelegateURLSessionDelegate的方法:async

extension ViewController:URLSessionDownloadDelegate{
    // 下載代理方法,下載結束
    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)")
        //建立文件管理器
        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("下載進度: \(Double(totalBytesWritten)/Double(totalBytesExpectedToWrite))\n")
    }
}
複製代碼

設置完這些代碼以後,還不能達到後臺下載的目的,還須要在AppDelegate中開啓後臺下載的權限,實現handleEventsForBackgroundURLSession方法:ide

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

實現到這裏已基本實現了後臺下載的功能,在應用程序切換到後臺以後,session 會和 ApplicationDelegate 作交互,session 中的task還會繼續下載,當全部的task完成以後(不管下載失敗仍是成功),系統都會調用ApplicationDelegateapplication:handleEventsForBackgroundURLSession:completionHandler:回調,在處理事件以後,在 completionHandler參數中執行 閉包,這樣應用程序就能夠獲取用戶界面的刷新。ui

若是咱們查看handleEventsForBackgroundURLSession這個api的話,會發現蘋果文檔要求在實現下載完成後須要實現URLSessionDidFinishEvents的代理,以達到更新屏幕的目的。url

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

若是沒有實現此方法的話⚠️️:後臺下載的實現是不會有影響的,只是在應用程序由後臺切換到前臺的過程當中可能會形成卡頓或者掉幀,同時可能在控制檯輸出警告:

Alamofire後臺下載

經過上面的例子🌰會發現若是要實現一個後臺下載,須要寫不少的代碼,同時還要注意後臺下載權限的開啓,完成下載以後回調的實現,漏掉了任何一步,後臺下載都不可能完美的實現,下面就來對比一下,在Alamofire中是怎麼實現後臺下載的。

首先先建立一個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)
    }()
}
複製代碼

後臺下載的實現:

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("下載回調信息: \(downloadResponse)")
    }
    .downloadProgress { (progress) in
        print("下載進度 : \(progress)")
}
複製代碼

並在AppDelegate作統一的處理:

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

這裏可能會有疑問🤔,爲甚麼要建立一個ZHBackgroundManger單例類?

那麼下面就帶着這個疑問❓來探究一下

若是點擊ZHBackgroundManger.shared.manager.download這裏的manager會發現這是SessionManager,那麼就跟進去SessionManager的源碼來看一下:

能夠看到在 SessionManagerdefault方法中,是對 URLSessionConfiguration作了一些配置,並初始化 SessionManager.

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

SessionManagerinit初始化方法中,能夠看到這裏把 URLSessionConfiguration設置成 default模式, 在內容的前篇,在建立一個URLSession的後臺下載的時候,咱們已經知道須要把URLSessionConfiguration設置成background模式才能夠。

在初始化方法裏還有一個SessionDelegatedelegate,並且這個delegate被傳入到URLSession中做爲其代理,而且session的這個初始化也就使得之後的回調都將會由 self.delegate 來處理了。也就是SessionManager實例建立一個SessionDelegate對象來處理底層URLSession生成的不一樣類型的代理回調。(這又稱爲代理移交)。

代理移交以後,在commonInit()的方法中會作另外的一些配置信息:

在這裏 delegate.sessionManager被設置爲自身 self,而 self實際上是持有 delegate 的。並且 delegatesessionManagerweak屬性修飾符。

這裏這麼寫delegate.sessionManager = self的緣由是

  • delegate在處理回調的時候能夠和sessionManager進行通訊
  • delegate將不屬於本身的回調處理從新交給sessionManager進行再次分發
  • 減小與其餘邏輯內容的依賴

並且這裏的delegate.sessionDidFinishEventsForBackgroundURLSession閉包,只要後臺任務下載完成就會回調到這個閉包內部,在閉包內部,回調了主線程,調用了 backgroundCompletionHandler,這也就是在AppDelegateapplication:handleEventsForBackgroundURLSession方法中的completionHandler。至此,SessionManager的流程大概就是這樣。

對於上面的疑問:

  • 1. 經過源碼咱們能夠知道SessionManager在設置URLSessionConfiguration的默認的是default模式,由於須要後臺下載的話,就須要把URLSessionConfiguration的模式修改成background模式。包括咱們也能夠修改URLSessionConfiguration其餘的配置
  • 2. 在下載的時候,應用程序進入到後臺下載,若是對於上面的配置,不作成一個單例的話,或者沒有被持有的狀況下,在進入後臺後就會被釋放掉,從而會產生錯誤Error Domain=NSURLErrorDomain Code=-999 "cancelled"
  • 3. 並且將SessionManager從新包裝成一個單例後,在AppDelegate中的代理方法中能夠直接使用。

總結

  • 首先在 AppDelegateapplication:handleEventsForBackgroundURLSession的方法裏,把回調閉包completionHandler傳給了 SessionManagerbackgroundCompletionHandler.
  • 在下載完成的時候 SessionDelegateurlSessionDidFinishEvents代理的調用會觸發 SessionManagersessionDidFinishEventsForBackgroundURLSession代理的調用
  • 而後sessionDidFinishEventsForBackgroundURLSession 執行SessionManagerbackgroundCompletionHandler的閉包.
  • 最後會來到 AppDelegateapplication:handleEventsForBackgroundURLSession的方法裏的 completionHandler 的調用.

關於Alamofire後臺下載的代碼就分析到這裏,其實經過源碼發現,和利用URLSession進行後臺下載原理是大體相同的,只不過利用Alamofire使代碼看起來更加簡介,並且Alamofire中會有不少默認的配置,咱們只須要修改須要的配置項便可。

相關文章
相關標籤/搜索