Alamofire源碼解讀系列(十二)之請求(Request)

本篇是Alamofire中的請求抽象層的講解html

前言

在Alamofire中,圍繞着Request,設計了不少額外的特性,這也偏偏代表,Request是全部請求的基礎部分和發起點。這無疑給咱們一個Request很複雜的想法。但看了Alamofire中Request.swift中的代碼,Request被設計的又是如此的簡單,這就是爲何這些頂級框架如此讓人喜好的緣由。swift

在後續的文章中,我會單獨寫一篇Swift中協議的使用技巧,在Alamofire源碼解讀系列(一)之概述和使用這篇的Alamofire高級用法中,我根據Alamofire官方文檔作了一些補充,其中涉及到了URLConvertible和URLRequestConvertible的高級用法,在本篇中一樣出現了3個協議:安全

  • RequestAdapter 請求適配器,目的是自定義修改請求,一個典型的例子是爲每個請求調價Token請求頭
  • RequestRetrier 請求重試器, 目的是控制請求的重試機制,一個典型的例子是當某個特殊的請求失敗後,是否重試。
  • TaskConvertible task轉換器,目的是把task裝換成特定的類型,在Alamofire中有4中task:Data/Download/Upload/Stream

有一點須要特別說明的是,在使用Alamofire的高級用法時,須要操做SessionManager這個類。服務器

請求過程

明白Alamofire中一個請求的過程,是很是有必要的。先看下邊的代碼:網絡

Alamofire.request("https://httpbin.org/get")

上邊的代碼是最簡單的一個請求,咱們看看Alamofire.request中究竟幹了什麼?session

@discardableResult
public func request(
    _ url: URLConvertible,
    method: HTTPMethod = .get,
    parameters: Parameters? = nil,
    encoding: ParameterEncoding = URLEncoding.default,
    headers: HTTPHeaders? = nil)
    -> DataRequest
{
    return SessionManager.default.request(
        url,
        method: method,
        parameters: parameters,
        encoding: encoding,
        headers: headers
    )
}

該函數內部調用了SessionManager的request方法,這說明請求的第一個發起點來自SessionManager,Alamofire.swift該文件是最上層的封裝,緊鄰其下的就是SessionManager.swift。接下來咱們再看看SessionManager.default.request作了什麼?框架

@discardableResult
    open func request(
        _ url: URLConvertible,
        method: HTTPMethod = .get,
        parameters: Parameters? = nil,
        encoding: ParameterEncoding = URLEncoding.default,
        headers: HTTPHeaders? = nil)
        -> DataRequest
    {
        var originalRequest: URLRequest?
        /// 在這裏計算出可能出現的額錯誤的類型
        /// 1.url 若是不能被轉成URL被拋出一個error
        /// 2.originalRequest不能轉換爲URLRequest會拋出error
        do {
            originalRequest = try URLRequest(url: url, method: method, headers: headers)
            let encodedURLRequest = try encoding.encode(originalRequest!, with: parameters)
            return request(encodedURLRequest)
        } catch {
            return request(originalRequest, failedWith: error)
        }
    }

上邊的函數內部建立了一個Request對象,而後把參數編碼進這個Request中,以後又調用了內部的一個request函數,函數的參數就是上邊的Request對象。咱們就緒看看這個request函數作了什麼?函數

open func request(_ urlRequest: URLRequestConvertible) -> DataRequest {
        var originalRequest: URLRequest?

        do {
            originalRequest = try urlRequest.asURLRequest()
            /// 這裏須要注意的是Requestable並非DataRequest的一個屬性,前邊是沒有加let/var的,因此能夠經過DataRequest.Requestable來操做
            let originalTask = DataRequest.Requestable(urlRequest: originalRequest!)

            let task = try originalTask.task(session: session, adapter: adapter, queue: queue)
            let request = DataRequest(session: session, requestTask: .data(originalTask, task))

            delegate[task] = request

            if startRequestsImmediately { request.resume() }

            return request
        } catch {
            return request(originalRequest, failedWith: error)
        }
    }

注意,上邊的函數是一個open函數,所以可使用SessionManager.request來發起請求,不過參數是_ urlRequest: URLRequestConvertiblepost

URLRequestConvertible協議的目的是對URLRequest進行自定義的轉換,所以,在得到轉換後的URLRequest後,須要用URLRequest生成task,這樣才能發起網絡請求,在Alamofire中,但凡是request開頭的函數,默認的都是DataRequest類型,如今有了URLRequest還不夠,還須要檢測她可否生成與之相對應的task。學習

在上邊的函數中,用到了DataRequest.Requestable,Requestable其實一個結構體,他實現了TaskConvertible協議,所以,它可以用URLRequest生成與之相對應的task。接下來就初始化DataRequest,而後真正的開始發起請求。

咱們總結一下這個過程:

明白了上邊的過程,再回過頭來看Request.swift也就是本篇的內容就簡單多了,就下邊幾個目的:

  • 建立DataRequest/DownloadRequest/UploadRequest/StreamRequest
  • 發起請求

Request

有不少二次封裝的網絡框架中,通常都有這麼一個Request類,用於發送網絡請求,接受response,關聯服務器返回的數據而且管理task。Alamofire中的Request一樣主要實現上邊的任務。

Request做爲DataRequest、DownloadRequest、UploadRequest、StreamRequest的基類,咱們一塊兒來看看它有哪些屬性:

/// The delegate for the underlying task.
    /// 因爲某個屬性是經過另外一個屬性來setter和getter的,所以建議加一個鎖
    open internal(set) var delegate: TaskDelegate {
        get {
            taskDelegateLock.lock() ; defer { taskDelegateLock.unlock() }
            return taskDelegate
        }
        set {
            taskDelegateLock.lock() ; defer { taskDelegateLock.unlock() }
            taskDelegate = newValue
        }
    }

    /// The underlying task.
    open var task: URLSessionTask? { return delegate.task }

    /// The session belonging to the underlying task.
    open let session: URLSession

    /// The request sent or to be sent to the server.
    open var request: URLRequest? { return task?.originalRequest }

    /// The response received from the server, if any.
    open var response: HTTPURLResponse? { return task?.response as? HTTPURLResponse }

    /// The number of times the request has been retried.
    open internal(set) var retryCount: UInt = 0

    let originalTask: TaskConvertible?

    var startTime: CFAbsoluteTime?
    var endTime: CFAbsoluteTime?

    var validations: [() -> Void] = []

    private var taskDelegate: TaskDelegate
    private var taskDelegateLock = NSLock()

這些屬性沒什麼好說的,咱們就略過這些內容,Request的初始化方法,有點意思,咱們先看看代碼:

init(session: URLSession, requestTask: RequestTask, error: Error? = nil) {
        self.session = session

        switch requestTask {
        case .data(let originalTask, let task):
            taskDelegate = DataTaskDelegate(task: task)
            self.originalTask = originalTask
        case .download(let originalTask, let task):
            taskDelegate = DownloadTaskDelegate(task: task)
            self.originalTask = originalTask
        case .upload(let originalTask, let task):
            taskDelegate = UploadTaskDelegate(task: task)
            self.originalTask = originalTask
        case .stream(let originalTask, let task):
            taskDelegate = TaskDelegate(task: task)
            self.originalTask = originalTask
        }

        delegate.error = error
        delegate.queue.addOperation { self.endTime = CFAbsoluteTimeGetCurrent() }
    }

要想發起一個請求,有一個task就足夠了,在上邊的方法中傳遞過來的session主要用於CustomStringConvertibleCustomDebugStringConvertible這兩個協議的實現方法中獲取特定的數據。

這裏有一點小提示,在建立自定義的類的時候,實現上邊這兩個協議,經過打印,可以進行快速的調試。

上邊方法中第二個參數是requestTask,它是一個枚舉類型,咱們看一下:

enum RequestTask {
        case data(TaskConvertible?, URLSessionTask?)
        case download(TaskConvertible?, URLSessionTask?)
        case upload(TaskConvertible?, URLSessionTask?)
        case stream(TaskConvertible?, URLSessionTask?)
    }

在swift中枚舉不只僅用來區分不一樣的選項,更強大的是爲每一個選項綁定的數據。你們仔細想一下,在初始化Request的時候,只須要傳遞requestTask這個枚舉值,咱們就獲得了兩個重要的數據:Request的類型和相對應的task。這一變成手法的運用,大大提升了代碼的質量。

RequestTask枚舉中和選項綁定的數據有兩個,TaskConvertible表示原始的對象,該對象實現了TaskConvertible協議,可以轉換成task。URLSessionTask是原始對象轉換後的task。所以衍生出一種高級使用方法的可能性,能夠自定義一個類,實現TaskConvertible協議,就可以操縱task的轉換過程,很靈活。

delegate.queue.addOperation { self.endTime = CFAbsoluteTimeGetCurrent() }

上邊的這一行代碼。給代理的queue添加了一個操做,隊列是先進先出原則,可是能夠經過isSuspended暫停隊列內部的操做,下邊是一個例子演示:

let queue = { () -> OperationQueue in 
    let operationQueue = OperationQueue()
    
    operationQueue.maxConcurrentOperationCount = 1
    operationQueue.isSuspended = true
    operationQueue.qualityOfService = .utility
    
    return operationQueue
}()

queue.addOperation {
    print("1")
}

queue.addOperation {
    print("2")
}

queue.addOperation {
    print("3")
}

queue.isSuspended = false

打印結果:

1
2
3

隊列提供了強大的功能,瞭解隊列的知識點很是有必要,有很大的一種可能性,也許某個問題卡住了,用隊列可以很輕鬆的解決。有興趣能夠看看我模仿SDWebImage寫的下載器MCDownloader(iOS下載器)說明書

處理網絡請求,就必需要面對安全的問題,爲了解決數據傳輸安全問題,到目前爲止,已經出現了不少種解決方式。想了解這方面的知識,能夠去看<<HTTP權威指南>>

Alamofire源碼解讀系列(一)之概述和使用中的Alamofire高級使用技巧部分。

/// Associates an HTTP Basic credential with the request.
    ///
    /// - parameter user:        The user.
    /// - parameter password:    The password.
    /// - parameter persistence: The URL credential persistence. `.ForSession` by default.
    ///
    /// - returns: The request.
    /// 這裏須要注意一點,persistence表示持久性,能夠點擊去查看詳細說明
    @discardableResult
    open func authenticate(
        user: String,
        password: String,
        persistence: URLCredential.Persistence = .forSession)
        -> Self
    {
        let credential = URLCredential(user: user, password: password, persistence: persistence)
        return authenticate(usingCredential: credential)
    }

    /// Associates a specified credential with the request.
    ///
    /// - parameter credential: The credential.
    ///
    /// - returns: The request.
    @discardableResult
    open func authenticate(usingCredential credential: URLCredential) -> Self {
        delegate.credential = credential
        return self
    }

上邊的這兩個函數可以處理請求中的驗證問題,能夠用來應對用戶密碼和證書驗證。

/// Returns a base64 encoded basic authentication credential as an authorization header tuple.
    ///
    /// - parameter user:     The user.
    /// - parameter password: The password.
    ///
    /// - returns: A tuple with Authorization header and credential value if encoding succeeds, `nil` otherwise.
    open static func authorizationHeader(user: String, password: String) -> (key: String, value: String)? {
        guard let data = "\(user):\(password)".data(using: .utf8) else { return nil }

        let credential = data.base64EncodedString(options: [])

        return (key: "Authorization", value: "Basic \(credential)")
    }

這個方法是一個輔助函數,某些服務器可能須要把用戶名和密碼拼接到請求頭中,那麼可使用這個函數來實現。

咱們對一個請求的操做有下邊3中可能:

  • resume 喚醒該請求,這個很是簡單,函數中作了3件事:記錄開始時間,喚醒task,發通知。

    /// Resumes the request.
          open func resume() {
              guard let task = task else { delegate.queue.isSuspended = false ; return }
    
              if startTime == nil { startTime = CFAbsoluteTimeGetCurrent() }
    
              task.resume()
    
              NotificationCenter.default.post(
                  name: Notification.Name.Task.DidResume,
                  object: self,
                  userInfo: [Notification.Key.Task: task]
              )
          }
  • suspend 暫停

    /// Suspends the request.
          open func suspend() {
              guard let task = task else { return }
    
              task.suspend()
    
              NotificationCenter.default.post(
                  name: Notification.Name.Task.DidSuspend,
                  object: self,
                  userInfo: [Notification.Key.Task: task]
              )
          }
  • cancel 取消

    /// Cancels the request.
          open func cancel() {
              guard let task = task else { return }
    
              task.cancel()
    
              NotificationCenter.default.post(
                  name: Notification.Name.Task.DidCancel,
                  object: self,
                  userInfo: [Notification.Key.Task: task]
              )
          }

Request中對CustomDebugStringConvertible和CustomStringConvertible的實現,咱們就不作太多介紹了,有兩點須要注意:

  1. 相似像urlCredentialStorage, httpCookieStorage這種帶有Storage字段的對象,須要仔細研究一下這種代碼設計的規律。
  2. 下邊這一小段代碼正好提現了swift的優雅之處,須要記住:

    for (field, value) in headerFields where field != "Cookie" {
                     headers[field] = value
                 }

TaskConvertible

TaskConvertible協議給了給了咱們轉換task的能力,任何實現了該協議的對象,都表示可以轉換成一個task。咱們都知道DataRequest,DownloadRequest,UploadRequest,StreamRequest都繼承自Request,最終應該是經過TaskConvertible協議來把一個URLRequest轉換成對應的task。

而Alamofire的Request的設計中,採用struct或者enum來實現這個協議,咱們來看看這些實現;

DataRequest:

struct Requestable: TaskConvertible {
        let urlRequest: URLRequest

        func task(session: URLSession, adapter: RequestAdapter?, queue: DispatchQueue) throws -> URLSessionTask {
            do {
                let urlRequest = try self.urlRequest.adapt(using: adapter)
                return queue.sync { session.dataTask(with: urlRequest) }
            } catch {
                throw AdaptError(error: error)
            }
        }
    }

DownloadRequest:

enum Downloadable: TaskConvertible {
        case request(URLRequest)
        case resumeData(Data)

        func task(session: URLSession, adapter: RequestAdapter?, queue: DispatchQueue) throws -> URLSessionTask {
            do {
                let task: URLSessionTask

                switch self {
                case let .request(urlRequest):
                    let urlRequest = try urlRequest.adapt(using: adapter)
                    task = queue.sync { session.downloadTask(with: urlRequest) }
                case let .resumeData(resumeData):
                    task = queue.sync { session.downloadTask(withResumeData: resumeData) }
                }

                return task
            } catch {
                throw AdaptError(error: error)
            }
        }
    }

若是task的類型是下載,會出現兩種狀況,一種是直接經過URLRequest生成downloadTask,另外一種是根據已有的數據恢復成downloadTask。咱們以前已經講過了,下載失敗後會有resumeData。裏邊保存了下載信息,這裏就不提了。總之,上邊這個enum給咱們提供了兩種不一樣的方式來生成downloadTask。

這種代碼的設計值得學習。

UploadRequest:

enum Uploadable: TaskConvertible {
        case data(Data, URLRequest)
        case file(URL, URLRequest)
        case stream(InputStream, URLRequest)

        func task(session: URLSession, adapter: RequestAdapter?, queue: DispatchQueue) throws -> URLSessionTask {
            do {
                let task: URLSessionTask

                switch self {
                case let .data(data, urlRequest):
                    let urlRequest = try urlRequest.adapt(using: adapter)
                    task = queue.sync { session.uploadTask(with: urlRequest, from: data) }
                case let .file(url, urlRequest):
                    let urlRequest = try urlRequest.adapt(using: adapter)
                    task = queue.sync { session.uploadTask(with: urlRequest, fromFile: url) }
                case let .stream(_, urlRequest):
                    let urlRequest = try urlRequest.adapt(using: adapter)
                    task = queue.sync { session.uploadTask(withStreamedRequest: urlRequest) }
                }

                return task
            } catch {
                throw AdaptError(error: error)
            }
        }
    }

雖然內容與上邊的DownloadRequest不一樣,可是套路卻相同。從代碼中,也能看出,上傳數據有3種介質,分別是:data,file,stream。

StreamRequest:

enum Streamable: TaskConvertible {
        case stream(hostName: String, port: Int)
        case netService(NetService)

        func task(session: URLSession, adapter: RequestAdapter?, queue: DispatchQueue) throws -> URLSessionTask {
            let task: URLSessionTask

            switch self {
            case let .stream(hostName, port):
                task = queue.sync { session.streamTask(withHostName: hostName, port: port) }
            case let .netService(netService):
                task = queue.sync { session.streamTask(with: netService) }
            }

            return task
        }
    }

netService超出了本文的範圍,就不作介紹了,平時用的也少。

咱們對上邊這些struct,enum作一個總結:因爲struct,enum是值拷貝,所以他們比較適合做爲數據的載體。一個方案的邏輯中,若是可能出現多個可能性,就考慮使用enum。還有最重要的一點,儘可能把一個單一的功能的做用域限制的越小越好。功能越單一,結構越簡單的函數越安全。

忽略的內容

在Request.swift的源碼中,還有一個給任務添加進度的方法,在這裏就不作介紹了,原理就是自定義一個函數,傳遞給task的代理。在DownloadRequest中對取消下載任務作了一些額外的處理。還有設置下載後的目錄等等。

DownloadOptions

這個DownloadOptions其實挺有意思的,他實現了OptionSet協議,所以它就有了集合的一些特性。

在OC中,咱們每每經過掩碼來實現多個選項共存這一功能,但DownloadOptions用另外一種方式實現了這一功能:

/// A collection of options to be executed prior to moving a downloaded file from the temporary URL to the
    /// destination URL.
    public struct DownloadOptions: OptionSet {
        /// Returns the raw bitmask value of the option and satisfies the `RawRepresentable` protocol.
        public let rawValue: UInt

        /// A `DownloadOptions` flag that creates intermediate directories for the destination URL if specified.
        public static let createIntermediateDirectories = DownloadOptions(rawValue: 1 << 0)

        /// A `DownloadOptions` flag that removes a previous file from the destination URL if specified.
        public static let removePreviousFile = DownloadOptions(rawValue: 1 << 1)

        /// Creates a `DownloadFileDestinationOptions` instance with the specified raw value.
        ///
        /// - parameter rawValue: The raw bitmask value for the option.
        ///
        /// - returns: A new log level instance.
        public init(rawValue: UInt) {
            self.rawValue = rawValue
        }
    }

上邊的代碼只擴展了兩個默認選項:

  • createIntermediateDirectories
  • removePreviousFile

能夠採用相似的手法,本身擴展更多的選項。看一下下邊的例子就明白了:

var op = DownloadRequest.DownloadOptions(rawValue: 1)
        op.insert(DownloadRequest.DownloadOptions(rawValue: 2))
        if op.contains(.createIntermediateDirectories) {
            print("createIntermediateDirectories")
        }
        if op.contains(.removePreviousFile) {
            print("removePreviousFile")
        }

上邊代碼中,if語句內的打印都會調用。

總結

這一篇文章與上一篇間隔了很長時間,緣由是公司作了一個項目。這個中小型項目結束後,也有一些須要總結的地方,我會把這些感觸寫下來,和你們討論一些項目開發的內容。

讀的越多,愈加以爲Alamofire中的函數的設計很厲害。不是一時半會可以所有串聯的。

因爲知識水平有限,若有錯誤,還望指出

連接

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

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

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

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

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

Alamofire源碼解讀系列(六)之Task代理(TaskDelegate) 簡書-----博客園

Alamofire源碼解讀系列(七)之網絡監控(NetworkReachabilityManager) 簡書-----博客園

Alamofire源碼解讀系列(八)之安全策略(ServerTrustPolicy) 簡書-----博客園

Alamofire源碼解讀系列(九)之響應封裝(Response) 簡書-----博客園

Alamofire源碼解讀系列(十)之序列化(ResponseSerialization) 簡書-----博客園

Alamofire源碼解讀系列(十一)之多表單(MultipartFormData) 簡書-----博客園

Alamofire源碼解讀系列(十二)之時間軸(Timeline) 簡書-----博客園

相關文章
相關標籤/搜索