本篇介紹Task代理(TaskDelegate.swift)
前言
我相信可能有80%的同學使用AFNetworking或者Alamofire處理網(wǎng)絡(luò)事件,并且這兩個框架都提供了豐富的功能,我也相信很多人都做了二次封裝,但事實上,這個二次封裝卻又異常簡單或者是簡陋。這篇文章的內(nèi)容是Task代理,是一篇很獨立的文章,大家可以通過這篇文章了解iOS中網(wǎng)絡(luò)開發(fā)是怎么一回事。
那么一條最普通的網(wǎng)絡(luò)請求,究竟是怎樣的一個過程?首先我們根據(jù)一個URL和若干個參數(shù)生成Request,然后根據(jù)Request生成一個會話Session,再根據(jù)這個Session生成Task,我們開啟Task就完成了這個請求。當然這一過程之中還會包含重定向,數(shù)據(jù)上傳,挑戰(zhàn),證書等等一系列的配置信息。
我們再聊聊代理的問題,不管是在網(wǎng)絡(luò)請求中,還是再其他的地方,代理都類似于一個管理員的身份。這在業(yè)務(wù)架構(gòu)中是一個很好的主意。假如我把代理想象成一個人,那么這個人的工作是什么呢?
提供我所需要的數(shù)據(jù),這一點很重要。獲取的數(shù)據(jù)也分兩種:加工過的和未加工過的。舉個例子,有一個控制器,里邊需要處理好幾個不同類型的cell,每個cell都有屬于自己的適配器來控制樣式和數(shù)據(jù)處理,面對這種情況,可以設(shè)計一個代理,每當刷新界面的時候,直接從代理中請求處理過的適配器數(shù)據(jù),那么這種情況下的數(shù)據(jù)就是被加工過的,如果數(shù)據(jù)加工是一個復(fù)雜的工作,可以設(shè)計一個數(shù)據(jù)加工的代理。這就涉及了下邊的代理的另外一項工作。
-
提供處理業(yè)務(wù)的方法,這往往被用于事件的傳遞。這是一種狹義上的代理,是一個協(xié)議。在開發(fā)中,不管是view還是Controller,都可以利用代理傳遞事件。但我們這里說的不是協(xié)議,協(xié)議必須要求我們實現(xiàn)它的屬性和方法,這對上邊提到的‘人’是不友好的。在真實的理想的場景中,我和代理的交互只有兩種情況:
- 給我想要的數(shù)據(jù)
- 我知道這個業(yè)務(wù)你比較精通,當有和你相關(guān)的事情的時候,我會通知你
如果對上邊的內(nèi)容不太明白,只需要明白,有的代理是一個協(xié)議,有的代理是一個'人',在某些讓你頭疼的復(fù)雜的業(yè)務(wù)中,用代理'人'去處理。我在想是不是不叫代理,叫Manager更合適。
URLSessionTask
URLSessionTask是對task最基本的封裝。按照請求的目的和響應(yīng)的結(jié)果可以分為:
- 獲取Data
- 下載
- 上傳
- Stream, 這個在這篇中不做講解

上邊圖中表示了一種繼承關(guān)系,與之相對應(yīng)的代理如下圖:




我會在下邊詳細講解這些代理方法。
TaskDelegate
TaskDelegate位于繼承鏈的最底層,因此它提供了一些最基礎(chǔ)的東西,這些東西也是其他Delegate共享的,我們先看看屬性:
// MARK: Properties
/// The serial operation queue used to execute all operations after the task completes.
open let queue: OperationQueue
/// The data returned by the server.
public var data: Data? { return nil }
/// The error generated throughout the lifecyle of the task.
public var error: Error?
var task: URLSessionTask? {
didSet { reset() }
}
var initialResponseTime: CFAbsoluteTime?
var credential: URLCredential?
var metrics: AnyObject? // URLSessionTaskMetrics
我們來分析下這些屬性:
-
queue: OperationQueue很明顯這是一個隊列,隊列中可以添加很多operation。隊列是可以被暫停的,通過把isSuspended設(shè)置為true就可以讓隊列中的所有operation暫停,直到isSuspended設(shè)置為false后,operation才會開始執(zhí)行。在Alamofire中,放入該隊列的operation有一下幾種情況:- 注意:該隊列會在任務(wù)完成之后把
isSuspended設(shè)置為false - 一個Request的endTime,在任務(wù)完成后調(diào)用,就可以為Request設(shè)置請求結(jié)束時間
- response處理,Alamofire中的響應(yīng)回調(diào)是鏈式的,原理就是把這些回調(diào)函數(shù)通過operation添加到隊列中,因此也保證了回調(diào)函數(shù)的訪問順序是正確的
- 上傳數(shù)據(jù)成功后,刪除臨時文件,這個后續(xù)的文章會解釋的
- 注意:該隊列會在任務(wù)完成之后把
data: Data?表示服務(wù)器返回的Data,這個可能為空error: Error?表示該代理生命周期內(nèi)有可能出現(xiàn)的錯誤,這一點很重要,我們在寫一個代理或者manager的時候,可以添加這么一個錯誤屬性,專門抓取生命周期內(nèi)的錯誤。task: URLSessionTask?表示一個task,對于本代理而言,task是很重要的一個屬性。initialResponseTime: CFAbsoluteTime?當task是URLSessionDataTask時,表示接收到數(shù)據(jù)的時間;當task是URLSessionDownloadTask時,表示開始寫數(shù)據(jù)的時間;當task是URLSessionUploadTask時,表示上傳數(shù)據(jù)的時間;credential: URLCredential?表示證書,如果給該代理設(shè)置了這個屬性,在它里邊的證書驗證方法中會備用到metrics: AnyObject?apple提供了一個統(tǒng)計task信息的類URLSessionTaskMetrics,可以統(tǒng)計跟task相關(guān)的一些信息,包括和task相關(guān)的所有事務(wù),task的開始和結(jié)束時間,task的重定向的次數(shù)。
上邊的這些屬性中,可以留意一下queue的使用方法,盡量為設(shè)計的代理添加錯誤處理機制。
Alamofire使用類似Properties,Lifecycle等等關(guān)鍵字來分割文件的,看完了上邊的屬性,我們在看看Lifecycle。
// MARK: Lifecycle
init(task: URLSessionTask?) {
self.task = task
self.queue = {
let operationQueue = OperationQueue()
operationQueue.maxConcurrentOperationCount = 1
operationQueue.isSuspended = true
operationQueue.qualityOfService = .utility
return operationQueue
}()
}
func reset() {
error = nil
initialResponseTime = nil
}
reset函數(shù)把error和initialResponseTime都置為nil,這個沒什么好說的,在init函數(shù)中隊列的創(chuàng)建很有意思。在swift中,如果我們要創(chuàng)建一個對象,不管是view還是別的,都可以采用這樣的方式:創(chuàng)建一個函數(shù),然后立刻調(diào)用,這很像JavaScript的用法。
lazy var label: UILabel = {
let view = UILabel()
view.backgroundColor = .clear
view.textAlignment = .center
view.textColor = .white
view.font = .boldSystemFont(ofSize: 18)
self.contentView.addSubview(view)
return view
}()
operationQueue.isSuspended = true可以保證隊列中的operation都是暫停狀態(tài),正常情況下,operation在被加入到隊列中后,會盡快執(zhí)行。
在swift中函數(shù)是一等公民,可以當參數(shù)和返回值來使用。同oc的block一樣,我們可以把他們當成一個屬性,目的是提前告訴代理當遇到指定的事件時應(yīng)該怎么做?在YYModel中有一小段代碼就是關(guān)于Block當返回值的妙用。我們看看TaskDelegate下的四個相關(guān)函數(shù):
// MARK: URLSessionTaskDelegate
var taskWillPerformHTTPRedirection: ((URLSession, URLSessionTask, HTTPURLResponse, URLRequest) -> URLRequest?)?
var taskDidReceiveChallenge: ((URLSession, URLSessionTask, URLAuthenticationChallenge) -> (URLSession.AuthChallengeDisposition, URLCredential?))?
var taskNeedNewBodyStream: ((URLSession, URLSessionTask) -> InputStream?)?
var taskDidCompleteWithError: ((URLSession, URLSessionTask, Error?) -> Void)?
我們先看第一個函數(shù),這個函數(shù)對應(yīng)的代理方法如下:
@objc(URLSession:task:willPerformHTTPRedirection:newRequest:completionHandler:)
func urlSession(
_ session: URLSession,
task: URLSessionTask,
willPerformHTTPRedirection response: HTTPURLResponse,
newRequest request: URLRequest,
completionHandler: @escaping (URLRequest?) -> Void)
{
var redirectRequest: URLRequest? = request
if let taskWillPerformHTTPRedirection = taskWillPerformHTTPRedirection {
redirectRequest = taskWillPerformHTTPRedirection(session, task, response, request)
}
completionHandler(redirectRequest)
}
上邊的函數(shù)處理的問題是請求重定向問題,我們大概講一下重定向是怎么一回事:Web服務(wù)器有時會返回重定向響應(yīng)而不是成功的報文。Web服務(wù)器可以將瀏覽器重定向到其他地方來執(zhí)行請求。重定向響應(yīng)由返回碼3XX說明。Location響應(yīng)首部包含了內(nèi)容的新地址或優(yōu)選地址的URI重定向可用于下列情況:
- 永久撤離的資源:資源可能被移動到新的位置,或者被重新命名,有了一個新的URI。Web服務(wù)器返回301 Moved Permanently
- 臨時撤離的資源:如果資源被臨時撤離或重命名,Web服務(wù)器返回303 See Other 或307 Temproary Redirect
- URL增強:服務(wù)器通常用重定向來重寫URL,往往用于嵌入上下文。Web服務(wù)器返回303 See Other 或307 Temproary Redirect
- 負載均衡:如果一個超載的服務(wù)器收到一條請求,服務(wù)器可以將客戶端重新定向到一個負載不大的服務(wù)器。Web服務(wù)器返回303 See Other 或307 Temproary Redirect
- 服務(wù)器關(guān)聯(lián):Web服務(wù)器可能會有某些用戶的本地信息,服務(wù)器可以將客戶端重新定向到包含那個客戶端信息的服務(wù)器上去。Web服務(wù)器返回303 See Other 或307 Temproary Redirect
- 規(guī)范目錄名稱:客戶端請求的URI是一個不帶尾部斜線的目錄名時,大多數(shù)服務(wù)器都會將客戶端重定向到Hige加了尾部斜線的URI上。
上邊的重定向函數(shù)要求返回一個redirectRequest,就是重定向的Request,Alamofire的處理方法就是,如果給代理的重定向處理函數(shù)賦值了,就返回代理函數(shù)的返回值,否則返回服務(wù)器返回的Request。
第二個函數(shù)用于處理驗證相關(guān)的事務(wù)。我們先講講disposition,他的類型是URLSession.AuthChallengeDisposition,其實就是一個枚舉:
@available(iOS 7.0, *)
public enum AuthChallengeDisposition : Int {
case useCredential
case performDefaultHandling
case cancelAuthenticationChallenge
case rejectProtectionSpace
}
這個授權(quán)配置一共有四個選項:
useCredential表示使用證書-
performDefaultHandling表示采用默認的方式,這個方式跟服務(wù)器返回authenticationMethod有很大關(guān)系,我簡單列一些常用的method-
NSURLAuthenticationMethodHTTPBasic基本認證 -
NSURLAuthenticationMethodHTTPDigest摘要認證 -
NSURLAuthenticationMethodClientCertificate客戶端證書認證 -
NSURLAuthenticationMethodServerTrust表示返回的證書要使用serverTrust創(chuàng)建
-
cancelAuthenticationChallenge表示取消認證rejectProtectionSpace表示拒絕認證
對于驗證而言,有三種方式:
- 客戶端驗證
- 服務(wù)器驗證
- 雙向驗證
我們先給出Alamofire中的函數(shù),然后在分析:
@objc(URLSession:task:didReceiveChallenge:completionHandler:)
func urlSession(
_ session: URLSession,
task: URLSessionTask,
didReceive challenge: URLAuthenticationChallenge,
completionHandler: @escaping (URLSession.AuthChallengeDisposition, URLCredential?) -> Void)
{
var disposition: URLSession.AuthChallengeDisposition = .performDefaultHandling
var credential: URLCredential?
if let taskDidReceiveChallenge = taskDidReceiveChallenge {
(disposition, credential) = taskDidReceiveChallenge(session, task, challenge)
} else if challenge.protectionSpace.authenticationMethod == NSURLAuthenticationMethodServerTrust {
let host = challenge.protectionSpace.host
if
let serverTrustPolicy = session.serverTrustPolicyManager?.serverTrustPolicy(forHost: host),
let serverTrust = challenge.protectionSpace.serverTrust
{
if serverTrustPolicy.evaluate(serverTrust, forHost: host) {
disposition = .useCredential
credential = URLCredential(trust: serverTrust)
} else {
disposition = .cancelAuthenticationChallenge
}
}
} else {
if challenge.previousFailureCount > 0 {
disposition = .rejectProtectionSpace
} else {
credential = self.credential ?? session.configuration.urlCredentialStorage?.defaultCredential(for: challenge.protectionSpace)
if credential != nil {
disposition = .useCredential
}
}
}
completionHandler(disposition, credential)
}
如果服務(wù)器需要驗證客戶端的,我們只需要給TaskDelegate的 var taskDidReceiveChallenge: ((URLSession, URLSessionTask, URLAuthenticationChallenge) -> (URLSession.AuthChallengeDisposition, URLCredential?))?賦值就行了。
這里有一個很重要的問題,HTTPS并不會觸發(fā)上邊的回調(diào)函數(shù),原因就是NSURLSession內(nèi)部有一個根證書,內(nèi)部會跟服務(wù)器的證書進行驗證,如果服務(wù)器的證書是證書機構(gòu)頒發(fā)的話,就可以順利通過驗證,否則會報錯。
另一個很重要的問題是,上邊的方法在何種情況下觸發(fā),按照apple的說法URL Session Programming Guide,當服務(wù)器響應(yīng)頭中包含WWW-Authenticate或者使用 proxy authentication TLS trust validation時,上邊的方法就會被觸發(fā),其他情況下都不會觸發(fā)。
Alamofire是雙向驗證的
雙向驗證的過程:
- 當服務(wù)器返回的
challenge.protectionSpace.authenticationMethod == NSURLAuthenticationMethodServerTrust時,服務(wù)器提供了一個服務(wù)器信任的證書,但這個證書也僅僅是服務(wù)器自己信任的,攻擊者完全可以提供一個證書騙過客戶端,基于這個問題,客戶端需要驗證服務(wù)端的證書 - Alamofire中通過
ServerTrustPolicy.swift這個類來驗證證書,大概過程就是拿本地的證書跟服務(wù)端返回的證書進行對比,如果客戶端存在相同的證書就表示通過,這個過程我會在ServerTrustPolicy.swift那篇文章中給出詳細的解答
因此,客戶端和服務(wù)端要建立SSL只需要兩步就行了:
- 服務(wù)端返回
WWW-Authenticate響應(yīng)頭,并返回自己信任證書 - 客戶端驗證證書,然后用證書中的公鑰把數(shù)據(jù)加密后發(fā)送給服務(wù)端
第三個函數(shù):
///This delegate method is called under two circumstances:
///To provide the initial request body stream if the task was created with uploadTask(withStreamedRequest:)
///To provide a replacement request body stream if the task needs to resend a request that has a body stream because of an authentication challenge or other recoverable server error.
///Note
///You do not need to implement this if your code provides the request body using a file URL or an NSData object.
@objc(URLSession:task:needNewBodyStream:)
func urlSession(
_ session: URLSession,
task: URLSessionTask,
needNewBodyStream completionHandler: @escaping (InputStream?) -> Void)
{
var bodyStream: InputStream?
if let taskNeedNewBodyStream = taskNeedNewBodyStream {
bodyStream = taskNeedNewBodyStream(session, task)
}
completionHandler(bodyStream)
}
當給task的Request提供一個body stream時才會調(diào)用,我們不需要關(guān)心這個方法,即使我們通過fileURL或者NSData上傳數(shù)據(jù)時,該函數(shù)也不會被調(diào)用,使用場景很少。
第四個函數(shù):
@objc(URLSession:task:didCompleteWithError:)
func urlSession(_ session: URLSession, task: URLSessionTask, didCompleteWithError error: Error?) {
if let taskDidCompleteWithError = taskDidCompleteWithError {
taskDidCompleteWithError(session, task, error)
} else {
if let error = error {
if self.error == nil { self.error = error }
if
let downloadDelegate = self as? DownloadTaskDelegate,
let resumeData = (error as NSError).userInfo[NSURLSessionDownloadTaskResumeData] as? Data
{
downloadDelegate.resumeData = resumeData
}
}
queue.isSuspended = false
}
}
該函數(shù)在請求完成后被調(diào)用,值得注意的是error不為nil的情況,除了給自身的error屬性賦值外,針對下載任務(wù)做了特殊處理,就是把當前已經(jīng)下載的數(shù)據(jù)保存在downloadDelegate.resumeData中,有點像斷點下載。
DataTaskDelegate
DataTaskDelegate繼承自TaskDelegate,實現(xiàn)了URLSessionDataDelegate協(xié)議。因此下邊我們也會講解URLSessionDataDelegate協(xié)議的方法。我們還是先看這里邊的屬性:
// MARK: Properties
var dataTask: URLSessionDataTask { return task as! URLSessionDataTask }
override var data: Data? {
if dataStream != nil {
return nil
} else {
return mutableData
}
}
var progress: Progress
var progressHandler: (closure: Request.ProgressHandler, queue: DispatchQueue)?
var dataStream: ((_ data: Data) -> Void)?
private var totalBytesReceived: Int64 = 0
private var mutableData: Data
private var expectedContentLength: Int64?
我們對這些屬性給出一定的解釋:
-
dataTask: URLSessionDataTaskDataTaskDelegate管理URLSessionDataTask -
data: Data?同樣是返回Data,但這里有一點不同,如果定義了dataStream方法的話,這個data返回為nil -
progress: Progress進度 -
progressHandler這不是函數(shù),是一個元組,什么時候調(diào)用,在下邊的方法中給出說明 -
dataStream自定義的數(shù)據(jù)處理函數(shù) -
totalBytesReceived已經(jīng)接受的數(shù)據(jù) -
mutableData保存數(shù)據(jù)的容器 -
expectedContentLength需要接受的數(shù)據(jù)的總大小
DataTaskDelegate的生命周期:
// MARK: Lifecycle
override init(task: URLSessionTask?) {
mutableData = Data()
progress = Progress(totalUnitCount: 0)
super.init(task: task)
}
override func reset() {
super.reset()
progress = Progress(totalUnitCount: 0)
totalBytesReceived = 0
mutableData = Data()
expectedContentLength = nil
}
這些沒什么好說的,我們在看看有哪些函數(shù):
// MARK: URLSessionDataDelegate
var dataTaskDidReceiveResponse: ((URLSession, URLSessionDataTask, URLResponse) -> URLSession.ResponseDisposition)?
var dataTaskDidBecomeDownloadTask: ((URLSession, URLSessionDataTask, URLSessionDownloadTask) -> Void)?
var dataTaskDidReceiveData: ((URLSession, URLSessionDataTask, Data) -> Void)?
var dataTaskWillCacheResponse: ((URLSession, URLSessionDataTask, CachedURLResponse) -> CachedURLResponse?)?
URLSessionDataDelegate有四個函數(shù),我們先看第一個函數(shù):
func urlSession(
_ session: URLSession,
dataTask: URLSessionDataTask,
didReceive response: URLResponse,
completionHandler: @escaping (URLSession.ResponseDisposition) -> Void)
{
var disposition: URLSession.ResponseDisposition = .allow
expectedContentLength = response.expectedContentLength
if let dataTaskDidReceiveResponse = dataTaskDidReceiveResponse {
disposition = dataTaskDidReceiveResponse(session, dataTask, response)
}
completionHandler(disposition)
}
當收到服務(wù)端的響應(yīng)后,該方法被觸發(fā)。在這個函數(shù)中,我們能夠獲取到和數(shù)據(jù)相關(guān)的一些參數(shù),大家可以想象成響應(yīng)頭。Alamofire中給出了一個函數(shù)dataTaskDidReceiveResponse,我們可以利用這個函數(shù)控制是不是要繼續(xù)獲取數(shù)據(jù),默認是.allow。
我們看第二個函數(shù):
func urlSession(
_ session: URLSession,
dataTask: URLSessionDataTask,
didBecome downloadTask: URLSessionDownloadTask)
{
dataTaskDidBecomeDownloadTask?(session, dataTask, downloadTask)
}
在上邊的disposition配置中,disposition的類型是URLSession.ResponseDisposition,我們看看這個枚舉:
public enum ResponseDisposition : Int {
case cancel /* Cancel the load, this is the same as -[task cancel] */
case allow /* Allow the load to continue */
case becomeDownload /* Turn this request into a download */
@available(iOS 9.0, *)
case becomeStream /* Turn this task into a stream task */
}
當選擇了becomeDownload后,就會觸發(fā)上邊的第二個函數(shù),在函數(shù)中會提供一個新的downloadTask。這就給我們一個把dataTask轉(zhuǎn)換成downloadTask的機會
那么我們把dataTask轉(zhuǎn)換成downloadTask究竟有什么用呢?我想到了一個使用場景,假如給定一個URL,返回的數(shù)據(jù)是Data,其實我想把這些數(shù)據(jù)下載下來,那么就可以使用上邊的這種技術(shù)了。舉個例子,https://baidu.com打開這個url會直接顯示網(wǎng)頁,使用上邊的技術(shù),打開這個url會直接下載網(wǎng)頁。我并沒有驗證上邊的想法。
我們繼續(xù)看第三個函數(shù):
func urlSession(_ session: URLSession, dataTask: URLSessionDataTask, didReceive data: Data) {
if initialResponseTime == nil { initialResponseTime = CFAbsoluteTimeGetCurrent() }
if let dataTaskDidReceiveData = dataTaskDidReceiveData {
dataTaskDidReceiveData(session, dataTask, data)
} else {
if let dataStream = dataStream {
dataStream(data)
} else {
mutableData.append(data)
}
let bytesReceived = Int64(data.count)
totalBytesReceived += bytesReceived
let totalBytesExpected = dataTask.response?.expectedContentLength ?? NSURLSessionTransferSizeUnknown
progress.totalUnitCount = totalBytesExpected
progress.completedUnitCount = totalBytesReceived
if let progressHandler = progressHandler {
progressHandler.queue.async { progressHandler.closure(self.progress) }
}
}
}
這個方法算是核心方法,我在MCDownloadManager中實現(xiàn)下載的核心方法就是這個方法,不同之處是,Alamofire把數(shù)據(jù)放入對象中,而我把數(shù)據(jù)寫入本地文件中。對這個函數(shù)內(nèi)部就不做解釋了,主要就是對自定義函數(shù)和進度的一些處理。
我們看第四個函數(shù):
func urlSession(
_ session: URLSession,
dataTask: URLSessionDataTask,
willCacheResponse proposedResponse: CachedURLResponse,
completionHandler: @escaping (CachedURLResponse?) -> Void)
{
var cachedResponse: CachedURLResponse? = proposedResponse
if let dataTaskWillCacheResponse = dataTaskWillCacheResponse {
cachedResponse = dataTaskWillCacheResponse(session, dataTask, proposedResponse)
}
completionHandler(cachedResponse)
}
其實,往往這樣的函數(shù)才是我們應(yīng)該注意的,最常見的接受響應(yīng),處理數(shù)據(jù),請求完成都是我們很熟悉的方法,因此更應(yīng)該多多注意這些不太熟悉的方法。
該函數(shù)用于處理是否需要緩存響應(yīng),Alamofire默認是緩存這些response的,但是每次發(fā)請求,它不會再緩存中讀取。
DownloadTaskDelegate
DownloadTaskDelegate繼承自TaskDelegate,實現(xiàn)了URLSessionDownloadDelegate協(xié)議。因此下邊我們也會講解URLSessionDownloadDelegate協(xié)議的方法。我們還是先看這里邊的屬性:
// MARK: Properties
var downloadTask: URLSessionDownloadTask { return task as! URLSessionDownloadTask }
var progress: Progress
var progressHandler: (closure: Request.ProgressHandler, queue: DispatchQueue)?
var resumeData: Data?
override var data: Data? { return resumeData }
var destination: DownloadRequest.DownloadFileDestination?
var temporaryURL: URL?
var destinationURL: URL?
var fileURL: URL? { return destination != nil ? destinationURL : temporaryURL }
這些屬性中有和上邊介紹的屬性重復(fù)的部分,我們只對不重復(fù)的部分給出說明:
-
downloadTask和URLSessionDownloadDelegate相對應(yīng)的URLSessionDownloadTask -
resumeData在上邊我們提到過,當請求完成后,如果error不為nil,如果是DownloadTaskDelegate,就會給這個屬性賦值 -
data返回resumeData -
destination通過這個函數(shù)可以自定義文件保存目錄和保存方式,這個保存方式分兩種,為URl創(chuàng)建文件夾,刪除已經(jīng)下載且存在的文件,這個會在后續(xù)的文章中提到 -
temporaryURL臨時的URL -
destinationURL數(shù)據(jù)存儲URL -
fileURL返回文件的路徑,如果destination不為nil,就返回destinationURL,否則返回temporaryURL
生命周期:
// MARK: Lifecycle
override init(task: URLSessionTask?) {
progress = Progress(totalUnitCount: 0)
super.init(task: task)
}
override func reset() {
super.reset()
progress = Progress(totalUnitCount: 0)
resumeData = nil
}
和下載相關(guān)的代理函數(shù)有三個:
// MARK: URLSessionDownloadDelegate
var downloadTaskDidFinishDownloadingToURL: ((URLSession, URLSessionDownloadTask, URL) -> URL)?
var downloadTaskDidWriteData: ((URLSession, URLSessionDownloadTask, Int64, Int64, Int64) -> Void)?
var downloadTaskDidResumeAtOffset: ((URLSession, URLSessionDownloadTask, Int64, Int64) -> Void)?
我們先看看第一個函數(shù):
func urlSession(
_ session: URLSession,
downloadTask: URLSessionDownloadTask,
didFinishDownloadingTo location: URL)
{
temporaryURL = location
guard
let destination = destination,
let response = downloadTask.response as? HTTPURLResponse
else { return }
let result = destination(location, response)
let destinationURL = result.destinationURL
let options = result.options
self.destinationURL = destinationURL
/// 說明在編碼過程中,對于存在可能出現(xiàn)錯誤的地方,一定要做error處理
do {
if options.contains(.removePreviousFile), FileManager.default.fileExists(atPath: destinationURL.path) {
try FileManager.default.removeItem(at: destinationURL)
}
if options.contains(.createIntermediateDirectories) {
let directory = destinationURL.deletingLastPathComponent()
try FileManager.default.createDirectory(at: directory, withIntermediateDirectories: true)
}
try FileManager.default.moveItem(at: location, to: destinationURL)
} catch {
self.error = error
}
}
對于這樣的代理方法,我們首先要做的就是弄明白在什么情況下它會被觸發(fā)。當數(shù)據(jù)下載完成后,該函數(shù)被觸發(fā)。系統(tǒng)會把數(shù)據(jù)下載到一個臨時的locationURL的地方,我們就是通過這個URL拿到數(shù)據(jù)的。上邊函數(shù)內(nèi)的代碼主要是把數(shù)據(jù)復(fù)制到目標路徑中。
但是我有一個疑問?按照apple文檔的內(nèi)容:If you choose to open the file for reading, you should do the actual reading in another thread to avoid blocking the delegate queue.應(yīng)該在另一個線程來讀取數(shù)據(jù),這樣才不會阻塞當前的代理線程,不知道有什么影響?
我們來看第二個函數(shù):
func urlSession(
_ session: URLSession,
downloadTask: URLSessionDownloadTask,
didWriteData bytesWritten: Int64,
totalBytesWritten: Int64,
totalBytesExpectedToWrite: Int64)
{
if initialResponseTime == nil { initialResponseTime = CFAbsoluteTimeGetCurrent() }
if let downloadTaskDidWriteData = downloadTaskDidWriteData {
downloadTaskDidWriteData(
session,
downloadTask,
bytesWritten,
totalBytesWritten,
totalBytesExpectedToWrite
)
} else {
progress.totalUnitCount = totalBytesExpectedToWrite
progress.completedUnitCount = totalBytesWritten
if let progressHandler = progressHandler {
progressHandler.queue.async { progressHandler.closure(self.progress) }
}
}
}
該代理方法在數(shù)據(jù)下載過程中被觸發(fā),主要的作用就是提供下載進度。這個比較簡單,我們看看第三個函數(shù):
func urlSession(
_ session: URLSession,
downloadTask: URLSessionDownloadTask,
didResumeAtOffset fileOffset: Int64,
expectedTotalBytes: Int64)
{
if let downloadTaskDidResumeAtOffset = downloadTaskDidResumeAtOffset {
downloadTaskDidResumeAtOffset(session, downloadTask, fileOffset, expectedTotalBytes)
} else {
progress.totalUnitCount = expectedTotalBytes
progress.completedUnitCount = fileOffset
}
}
是這樣的,如果一個下載的task是可以恢復(fù)的,那么當下載被取消或者失敗后,系統(tǒng)會返回一個resume?Data對象,這個對象包含了一些跟這個下載task相關(guān)的一些信息,有了它就能重新創(chuàng)建下載task,創(chuàng)建方法有兩個:download?Task(with?Resume?Data:?)和download?Task(with?Resume?Data:?completion?Handler:?),當task開始后,上邊的代理方法就會被觸發(fā)。
UploadTaskDelegate
UploadTaskDelegate繼承自DataTaskDelegate。對于上傳數(shù)據(jù)來說最麻煩的就是多表單數(shù)據(jù)的上傳,這個我們會在后續(xù)的MultipartFormData.swift給出詳細的解釋。
我們先看看它的屬性有哪些?
// MARK: Properties
var uploadTask: URLSessionUploadTask { return task as! URLSessionUploadTask }
var uploadProgress: Progress
var uploadProgressHandler: (closure: Request.ProgressHandler, queue: DispatchQueue)?
這些和上邊出現(xiàn)的內(nèi)容有重疊,在這里就不多做解釋了,我們再看看生命周期:
// MARK: Lifecycle
override init(task: URLSessionTask?) {
uploadProgress = Progress(totalUnitCount: 0)
super.init(task: task)
}
override func reset() {
super.reset()
uploadProgress = Progress(totalUnitCount: 0)
}
也沒什么好說的,再看看函數(shù):
// MARK: URLSessionTaskDelegate
var taskDidSendBodyData: ((URLSession, URLSessionTask, Int64, Int64, Int64) -> Void)?
func URLSession(
_ session: URLSession,
task: URLSessionTask,
didSendBodyData bytesSent: Int64,
totalBytesSent: Int64,
totalBytesExpectedToSend: Int64)
{
if initialResponseTime == nil { initialResponseTime = CFAbsoluteTimeGetCurrent() }
if let taskDidSendBodyData = taskDidSendBodyData {
taskDidSendBodyData(session, task, bytesSent, totalBytesSent, totalBytesExpectedToSend)
} else {
uploadProgress.totalUnitCount = totalBytesExpectedToSend
uploadProgress.completedUnitCount = totalBytesSent
if let uploadProgressHandler = uploadProgressHandler {
uploadProgressHandler.queue.async { uploadProgressHandler.closure(self.uploadProgress) }
}
}
}
該函數(shù)主要目的是提供上傳的進度,在Alamofire中,上傳數(shù)據(jù)用的是stream,這個會在后續(xù)文章中給出詳細的解釋。
總結(jié)
我個人解讀源碼的方式可能比較特別,我喜歡把所有的代碼都寫到文章之中。因為人的記憶都是有問題的,好多東西當時記住了,過段時間就忘了,為了方便日后查看這些筆記,我覺得還是把代碼都弄上來比較好。
同一段代碼,不同的人看會有不同的想法,這些解讀也可以給別人一些參考。我現(xiàn)在越來越覺得代碼的設(shè)計很重要了。
由于知識水平有限,如有錯誤,還望指出
鏈接
Alamofire源碼解讀系列(一)之概述和使用 簡書-----博客園
Alamofire源碼解讀系列(二)之錯誤處理(AFError) 簡書-----博客園
Alamofire源碼解讀系列(三)之通知處理(Notification) 簡書-----博客園