Alamofire源碼解讀系列(六)之Task代理(TaskDelegate)

本篇介紹Task代理(TaskDelegate.swift)

前言

我相信可能有80%的同學(xué)使用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就完成了這個請求。當(dāng)然這一過程之中還會包含重定向,數(shù)據(jù)上傳邪锌,挑戰(zhàn)勉躺,證書等等一系列的配置信息。

我們再聊聊代理的問題觅丰,不管是在網(wǎng)絡(luò)請求中,還是再其他的地方妨退,代理都類似于一個管理員的身份妇萄。這在業(yè)務(wù)架構(gòu)中是一個很好的主意。假如我把代理想象成一個人咬荷,那么這個人的工作是什么呢冠句?

  1. 提供我所需要的數(shù)據(jù),這一點很重要幸乒。獲取的數(shù)據(jù)也分兩種:加工過的和未加工過的懦底。舉個例子,有一個控制器罕扎,里邊需要處理好幾個不同類型的cell聚唐,每個cell都有屬于自己的適配器來控制樣式和數(shù)據(jù)處理,面對這種情況腔召,可以設(shè)計一個代理杆查,每當(dāng)刷新界面的時候,直接從代理中請求處理過的適配器數(shù)據(jù)臀蛛,那么這種情況下的數(shù)據(jù)就是被加工過的亲桦,如果數(shù)據(jù)加工是一個復(fù)雜的工作,可以設(shè)計一個數(shù)據(jù)加工的代理浊仆。這就涉及了下邊的代理的另外一項工作客峭。

  2. 提供處理業(yè)務(wù)的方法,這往往被用于事件的傳遞抡柿。這是一種狹義上的代理舔琅,是一個協(xié)議。在開發(fā)中,不管是view還是Controller沙绝,都可以利用代理傳遞事件搏明。但我們這里說的不是協(xié)議,協(xié)議必須要求我們實現(xiàn)它的屬性和方法闪檬,這對上邊提到的‘人’是不友好的星著。在真實的理想的場景中,我和代理的交互只有兩種情況:

    1. 給我想要的數(shù)據(jù)
    2. 我知道這個業(yè)務(wù)你比較精通粗悯,當(dāng)有和你相關(guān)的事情的時候虚循,我會通知你

如果對上邊的內(nèi)容不太明白,只需要明白,有的代理是一個協(xié)議横缔,有的代理是一個'人'铺遂,在某些讓你頭疼的復(fù)雜的業(yè)務(wù)中,用代理'人'去處理茎刚。我在想是不是不叫代理襟锐,叫Manager更合適。

URLSessionTask

URLSessionTask是對task最基本的封裝膛锭。按照請求的目的和響應(yīng)的結(jié)果可以分為:

  • 獲取Data
  • 下載
  • 上傳
  • Stream粮坞, 這個在這篇中不做講解

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

我會在下邊詳細(xì)講解這些代理方法初狰。

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)是鏈?zhǔn)降模砭褪前堰@些回調(diào)函數(shù)通過operation添加到隊列中界睁,因此也保證了回調(diào)函數(shù)的訪問順序是正確的
    • 上傳數(shù)據(jù)成功后觉增,刪除臨時文件,這個后續(xù)的文章會解釋的
  • data: Data? 表示服務(wù)器返回的Data翻斟,這個可能為空

  • error: Error?表示該代理生命周期內(nèi)有可能出現(xiàn)的錯誤逾礁,這一點很重要,我們在寫一個代理或者manager的時候访惜,可以添加這么一個錯誤屬性嘹履,專門抓取生命周期內(nèi)的錯誤。

  • task: URLSessionTask? 表示一個task债热,對于本代理而言砾嫉,task是很重要的一個屬性。

  • initialResponseTime: CFAbsoluteTime? 當(dāng)task是URLSessionDataTask時窒篱,表示接收到數(shù)據(jù)的時間焕刮;當(dāng)task是URLSessionDownloadTask時舶沿,表示開始寫數(shù)據(jù)的時間;當(dāng)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è)計的代理添加錯誤處理機(jī)制。

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ù)是一等公民,可以當(dāng)參數(shù)和返回值來使用活尊。同oc的block一樣隶校,我們可以把他們當(dāng)成一個屬性,目的是提前告訴代理當(dāng)遇到指定的事件時應(yīng)該怎么做蛹锰?在YYModel中有一小段代碼就是關(guān)于Block當(dāng)返回值的妙用深胳。我們看看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重定向可用于下列情況:

  1. 永久撤離的資源:資源可能被移動到新的位置权埠,或者被重新命名榨了,有了一個新的URI。Web服務(wù)器返回301 Moved Permanently
  2. 臨時撤離的資源:如果資源被臨時撤離或重命名攘蔽,Web服務(wù)器返回303 See Other 或307 Temproary Redirect
  3. URL增強(qiáng):服務(wù)器通常用重定向來重寫URL龙屉,往往用于嵌入上下文。Web服務(wù)器返回303 See Other 或307 Temproary Redirect
  4. 負(fù)載均衡:如果一個超載的服務(wù)器收到一條請求满俗,服務(wù)器可以將客戶端重新定向到一個負(fù)載不大的服務(wù)器转捕。Web服務(wù)器返回303 See Other 或307 Temproary Redirect
  5. 服務(wù)器關(guān)聯(lián):Web服務(wù)器可能會有某些用戶的本地信息,服務(wù)器可以將客戶端重新定向到包含那個客戶端信息的服務(wù)器上去唆垃。Web服務(wù)器返回303 See Other 或307 Temproary Redirect
  6. 規(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 表示采用默認(rèn)的方式隘擎,這個方式跟服務(wù)器返回authenticationMethod有很大關(guān)系,我簡單列一些常用的method

    • NSURLAuthenticationMethodHTTPBasic 基本認(rèn)證
    • NSURLAuthenticationMethodHTTPDigest 摘要認(rèn)證
    • NSURLAuthenticationMethodClientCertificate 客戶端證書認(rèn)證
    • NSURLAuthenticationMethodServerTrust 表示返回的證書要使用serverTrust創(chuàng)建
  • cancelAuthenticationChallenge 表示取消認(rèn)證

  • rejectProtectionSpace 表示拒絕認(rèn)證

對于驗證而言凉夯,有三種方式:

  1. 客戶端驗證
  2. 服務(wù)器驗證
  3. 雙向驗證

我們先給出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ù)器的證書進(jìn)行驗證尼夺,如果服務(wù)器的證書是證書機(jī)構(gòu)頒發(fā)的話,就可以順利通過驗證炒瘸,否則會報錯淤堵。

另一個很重要的問題是,上邊的方法在何種情況下觸發(fā)顷扩,按照apple的說法URL Session Programming Guide,當(dāng)服務(wù)器響應(yīng)頭中包含WWW-Authenticate或者使用 proxy authentication TLS trust validation時拐邪,上邊的方法就會被觸發(fā),其他情況下都不會觸發(fā)隘截。

Alamofire是雙向驗證的

雙向驗證的過程:

  • 當(dāng)服務(wù)器返回的challenge.protectionSpace.authenticationMethod == NSURLAuthenticationMethodServerTrust時扎阶,服務(wù)器提供了一個服務(wù)器信任的證書汹胃,但這個證書也僅僅是服務(wù)器自己信任的,攻擊者完全可以提供一個證書騙過客戶端东臀,基于這個問題着饥,客戶端需要驗證服務(wù)端的證書
  • Alamofire中通過ServerTrustPolicy.swift這個類來驗證證書,大概過程就是拿本地的證書跟服務(wù)端返回的證書進(jìn)行對比惰赋,如果客戶端存在相同的證書就表示通過宰掉,這個過程我會在ServerTrustPolicy.swift那篇文章中給出詳細(xì)的解答

因此,客戶端和服務(wù)端要建立SSL只需要兩步就行了:

  1. 服務(wù)端返回WWW-Authenticate響應(yīng)頭赁濒,并返回自己信任證書
  2. 客戶端驗證證書轨奄,然后用證書中的公鑰把數(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)
    }

當(dāng)給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ù)做了特殊處理,就是把當(dāng)前已經(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: URLSessionDataTask DataTaskDelegate管理URLSessionDataTask
  • data: Data? 同樣是返回Data户辱,但這里有一點不同,如果定義了dataStream方法的話糙臼,這個data返回為nil
  • progress: Progress 進(jìn)度
  • 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)
    }

當(dāng)收到服務(wù)端的響應(yīng)后揽乱,該方法被觸發(fā)名眉。在這個函數(shù)中,我們能夠獲取到和數(shù)據(jù)相關(guān)的一些參數(shù)凰棉,大家可以想象成響應(yīng)頭损拢。Alamofire中給出了一個函數(shù)dataTaskDidReceiveResponse,我們可以利用這個函數(shù)控制是不是要繼續(xù)獲取數(shù)據(jù)撒犀,默認(rèn)是.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 */
    }

當(dāng)選擇了becomeDownload后荆姆,就會觸發(fā)上邊的第二個函數(shù)蒙幻,在函數(shù)中會提供一個新的downloadTask。這就給我們一個把dataTask轉(zhuǎn)換成downloadTask的機(jī)會

那么我們把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ù)和進(jìn)度的一些處理。

我們看第四個函數(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默認(rèn)是緩存這些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 在上邊我們提到過列疗,當(dāng)請求完成后,如果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ā)。當(dāng)數(shù)據(jù)下載完成后杠览,該函數(shù)被觸發(fā)弯菊。系統(tǒng)會把數(shù)據(jù)下載到一個臨時的locationURL的地方,我們就是通過這個URL拿到數(shù)據(jù)的踱阿。上邊函數(shù)內(nèi)的代碼主要是把數(shù)據(jù)復(fù)制到目標(biāo)路徑中管钳。

但是我有一個疑問?按照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ù)软舌,這樣才不會阻塞當(dāng)前的代理線程才漆,不知道有什么影響?

我們來看第二個函數(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ā)佛点,主要的作用就是提供下載進(jìn)度栽烂。這個比較簡單,我們看看第三個函數(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ù)的,那么當(dāng)下載被取消或者失敗后焰手,系統(tǒng)會返回一個resume?Data對象糟描,這個對象包含了一些跟這個下載task相關(guān)的一些信息,有了它就能重新創(chuàng)建下載task书妻,創(chuàng)建方法有兩個:download?Task(with?Resume?Data:?)download?Task(with?Resume?Data:?completion?Handler:?),當(dāng)task開始后船响,上邊的代理方法就會被觸發(fā)。

UploadTaskDelegate

UploadTaskDelegate繼承自DataTaskDelegate躲履。對于上傳數(shù)據(jù)來說最麻煩的就是多表單數(shù)據(jù)的上傳见间,這個我們會在后續(xù)的MultipartFormData.swift給出詳細(xì)的解釋。

我們先看看它的屬性有哪些工猜?

 // 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ù)主要目的是提供上傳的進(jìn)度史侣,在Alamofire中拴泌,上傳數(shù)據(jù)用的是stream,這個會在后續(xù)文章中給出詳細(xì)的解釋惊橱。

總結(jié)

我個人解讀源碼的方式可能比較特別蚪腐,我喜歡把所有的代碼都寫到文章之中。因為人的記憶都是有問題的税朴,好多東西當(dāng)時記住了回季,過段時間就忘了,為了方便日后查看這些筆記正林,我覺得還是把代碼都弄上來比較好泡一。

同一段代碼,不同的人看會有不同的想法卓囚,這些解讀也可以給別人一些參考瘾杭。我現(xiàn)在越來越覺得代碼的設(shè)計很重要了。

由于知識水平有限哪亿,如有錯誤粥烁,還望指出

鏈接

Alamofire源碼解讀系列(一)之概述和使用 簡書-----博客園

Alamofire源碼解讀系列(二)之錯誤處理(AFError) 簡書-----博客園

Alamofire源碼解讀系列(三)之通知處理(Notification) 簡書-----博客園

Alamofire源碼解讀系列(四)之參數(shù)編碼(ParameterEncoding) 簡書-----博客園

Alamofire源碼解讀系列(五)之結(jié)果封裝(Result) 簡書-----博客園

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市蝇棉,隨后出現(xiàn)的幾起案子讨阻,更是在濱河造成了極大的恐慌,老刑警劉巖篡殷,帶你破解...
    沈念sama閱讀 206,214評論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件钝吮,死亡現(xiàn)場離奇詭異,居然都是意外死亡板辽,警方通過查閱死者的電腦和手機(jī)奇瘦,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,307評論 2 382
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來劲弦,“玉大人耳标,你說我怎么就攤上這事∫毓颍” “怎么了次坡?”我有些...
    開封第一講書人閱讀 152,543評論 0 341
  • 文/不壞的土叔 我叫張陵,是天一觀的道長画畅。 經(jīng)常有香客問我砸琅,道長,這世上最難降的妖魔是什么轴踱? 我笑而不...
    開封第一講書人閱讀 55,221評論 1 279
  • 正文 為了忘掉前任症脂,我火速辦了婚禮,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘摊腋。我一直安慰自己沸版,他們只是感情好,可當(dāng)我...
    茶點故事閱讀 64,224評論 5 371
  • 文/花漫 我一把揭開白布兴蒸。 她就那樣靜靜地躺著视粮,像睡著了一般。 火紅的嫁衣襯著肌膚如雪橙凳。 梳的紋絲不亂的頭發(fā)上蕾殴,一...
    開封第一講書人閱讀 49,007評論 1 284
  • 那天,我揣著相機(jī)與錄音岛啸,去河邊找鬼钓觉。 笑死,一個胖子當(dāng)著我的面吹牛坚踩,可吹牛的內(nèi)容都是我干的荡灾。 我是一名探鬼主播,決...
    沈念sama閱讀 38,313評論 3 399
  • 文/蒼蘭香墨 我猛地睜開眼瞬铸,長吁一口氣:“原來是場噩夢啊……” “哼批幌!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起嗓节,我...
    開封第一講書人閱讀 36,956評論 0 259
  • 序言:老撾萬榮一對情侶失蹤荧缘,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后拦宣,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體截粗,經(jīng)...
    沈念sama閱讀 43,441評論 1 300
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 35,925評論 2 323
  • 正文 我和宋清朗相戀三年鸵隧,在試婚紗的時候發(fā)現(xiàn)自己被綠了凿掂。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片犁苏。...
    茶點故事閱讀 38,018評論 1 333
  • 序言:一個原本活蹦亂跳的男人離奇死亡舀凛,死狀恐怖种蝶,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情靡羡,我是刑警寧澤,帶...
    沈念sama閱讀 33,685評論 4 322
  • 正文 年R本政府宣布俊性,位于F島的核電站略步,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏定页。R本人自食惡果不足惜趟薄,卻給世界環(huán)境...
    茶點故事閱讀 39,234評論 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望典徊。 院中可真熱鬧杭煎,春花似錦恩够、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,240評論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至也切,卻和暖如春扑媚,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背雷恃。 一陣腳步聲響...
    開封第一講書人閱讀 31,464評論 1 261
  • 我被黑心中介騙來泰國打工疆股, 沒想到剛下飛機(jī)就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人倒槐。 一個月前我還...
    沈念sama閱讀 45,467評論 2 352
  • 正文 我出身青樓旬痹,卻偏偏與公主長得像,于是被迫代替她去往敵國和親讨越。 傳聞我的和親對象是個殘疾皇子两残,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 42,762評論 2 345

推薦閱讀更多精彩內(nèi)容