前言
URLSession
是一個可以響應(yīng)發(fā)送或者接受HTTP請求的關(guān)鍵類。首先使用全局的 URLSession.shared
和 downloadTask
來創(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
的下載代理URLSessionDownloadDelegate
,URLSessionDelegate
的方法:
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)用ApplicationDelegate
的application: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
的源碼來看一下:
可以看到在
SessionManager
的default
方法中,是對URLSessionConfiguration
做了一些配置,并初始化SessionManager
.
那么再來看SessionManager
的初始化方法:
在
SessionManager
的init
初始化方法中,可以看到這里把URLSessionConfiguration
設(shè)置成default
模式,在內(nèi)容的前篇,在創(chuàng)建一個URLSession
的后臺下載的時候,我們已經(jīng)知道需要把URLSessionConfiguration
設(shè)置成background
模式才可以。
在初始化方法里還有一個SessionDelegate
的delegate
,而且這個delegate
被傳入到URLSession
中作為其代理,并且session
的這個初始化也就使得以后的回調(diào)都將會由 self.delegate
來處理了。也就是SessionManager
實例創(chuàng)建一個SessionDelegate
對象來處理底層URLSession
生成的不同類型的代理回調(diào)。(這又稱為代理移交)。
代理移交之后,在commonInit()
的方法中會做另外的一些配置信息:
在這里
delegate.sessionManager
被設(shè)置為自身 self
,而 self
其實是持有 delegate
的。而且 delegate
的 sessionManager
是weak
屬性修飾符。
這里這么寫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
,這也就是在AppDelegate
中application: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é)
- 首先在
AppDelegate
的application:handleEventsForBackgroundURLSession
的方法里,把回調(diào)閉包completionHandler
傳給了SessionManager
的backgroundCompletionHandler
. - 在下載完成的時候
SessionDelegate
的urlSessionDidFinishEvents
代理的調(diào)用會觸發(fā)SessionManager
的sessionDidFinishEventsForBackgroundURLSession
代理的調(diào)用 - 然后
sessionDidFinishEventsForBackgroundURLSession
執(zhí)行SessionManager
的backgroundCompletionHandler
的閉包. - 最后會來到
AppDelegate
的application:handleEventsForBackgroundURLSession
的方法里的completionHandler
的調(diào)用.
關(guān)于Alamofire
后臺下載的代碼就分析到這里,其實通過源碼發(fā)現(xiàn),和利用URLSession
進(jìn)行后臺下載原理是大致相同的,只不過利用Alamofire
使代碼看起來更加簡介,而且Alamofire
中會有很多默認(rèn)的配置,我們只需要修改需要的配置項即可。