Alamofire源碼解讀系列(一)之概述和使用


盡管Alamofire的github文檔已經做了很詳細的說明,我還是想重新梳理一遍它的各種用法,以及這些方法的一些設計思想

前言

因為之前寫過一個AFNetworking的源碼解讀,所以就已經比較了解iOS平台的網絡框架是怎么一回事了。AlamofireAFNetworking有很多相同的地方,然而,這些相同點在swift和oc兩種不同語言的實現情況下,給人的感覺是完全不同的。

我們看源碼的目的有兩個:一是了解代碼的實現原理,另一個是學習swift的一些高級用法。

下邊的這個表格就是我打算解讀的順序,一共17個文件,其中DispatchQueue+Alamofire.swift就不作為單獨的一篇來解釋了,會在使用到它的地方做一個說明,這一篇文章的主要目的就是解釋Alamofire如何使用,因此一共就需要17篇文章來完成這一系列的源碼解讀。

文件名 描述
1.AFError.swift 對錯誤的封裝,包含了Alamofire中所有可能出現的錯誤,使用enum實現,很有意思
2.Notifications.swift swift中通知的用法,這個跟oc的有區別
3.ParameterEncoding.swift 參數編碼,有些情況需要把參數編碼到URL中,包含了轉義相關的知識
4.Result.swift 對請求結果的封裝
5.TaskDelegate.swift 任務代理
6.NetworkReachabilityManager.swift 網絡狀態管理
7.ServerTrustPolicy.swift 安全策略管理
8.Response.swift 服務器返回的數據的封裝
9.ResponseSerialization.swift 響應序列化管理
10.MultipartFormData.swift 多表單數據處理
11.Timeline.swift 新增的內容,與請求相關的一些時間屬性
12.Request.swift 最核心的請求類
13.Validation.swift 對服務器響應的驗證
14.SessionDelegate.swift 會話代理
15.SessionManager.swift 會話管理,核心內容
16.Alamofire.swift 支持的基本接口

Alamofire的基本用法

1.最簡單的請求

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

這是一個最簡單的請求,這個請求即不需要參數,也不需要接收數據。接下來我們翻看Alamofire這個文件,發現並沒有Alamofire這個類,那么為什么能夠像Alamofire.requeset()這么使用呢?

其實當一個文件作為一個模塊被導入的話,通過文件名就能訪問到模塊內部的數據,比如說通過cocopods導入的框架,就有這樣的特性。如果把Alamofire.swift直接拖進工程中,Alamofire.requeset()就會報錯,但是我們去掉Alamofire,直接用request()就可以了。

2.Response處理

Alamofire.request("https://httpbin.org/get").responseJSON { response in
    print(response.request)  // original URL request
    print(response.response) // HTTP URL response
    print(response.data)     // server data
    print(response.result)   // result of response serialization

    if let JSON = response.result.value {
        print("JSON: \(JSON)")
    }
}

在Alamofire中,對請求的封裝有以下幾種類型:

  • Request
  • DataRequest
  • DownloadRequest
  • UploadRequest
  • StreamRequest

這幾種類型,按照名字我們就能很容易的知道他們的用途是什么,其中StreamRequest在iOS9.0之后才被引入。

request(...)方法返回Request本身或者其子類,那么responseJson就應該是Request本身或者其子類的一個函數,該函數的最后一個參數是一個閉包。這里先不能解釋太多,到了后邊會詳細解釋。

Alamofire對於response提供了5種處理方式:

// Response Handler - Unserialized Response
func response(
    queue: DispatchQueue?,
    completionHandler: @escaping (DefaultDataResponse) -> Void)
    -> Self

// Response Data Handler - Serialized into Data
func responseData(
    queue: DispatchQueue?,
    completionHandler: @escaping (DataResponse<Data>) -> Void)
    -> Self

// Response String Handler - Serialized into String
func responseString(
    queue: DispatchQueue?,
    encoding: String.Encoding?,
    completionHandler: @escaping (DataResponse<String>) -> Void)
    -> Self

// Response JSON Handler - Serialized into Any
func responseJSON(
    queue: DispatchQueue?,
    completionHandler: @escaping (DataResponse<Any>) -> Void)
    -> Self

// Response PropertyList (plist) Handler - Serialized into Any
func responsePropertyList(
    queue: DispatchQueue?,
    completionHandler: @escaping (DataResponse<Any>) -> Void))
    -> Self

我們把這五種歸納一下:

  • response 直接返回HTTPResponse,未序列化
  • responseData 序列化為Data
  • responseString 序列化為Json
  • responseString 序列化為字符串
  • responsePropertyList 序列化為Any

不管被序列成哪一個,結果都會通過閉包的參數response返回,如果是被序列化的數據,就通過resonse中的result.value來獲取數據。

源碼中response閉包函數的返回值是Self,也就是Request,這就讓我們能夠使用鏈式訪問來做一些很有意思的事情,比如:

Alamofire.request("https://httpbin.org/get")
    .responseString { response in
        print("Response String: \(response.result.value)")
    }
    .responseJSON { response in
        print("Response JSON: \(response.result.value)")
    }

上邊的代碼就使用了鏈式訪問,當收到服務器的數據后,先處理responseString再處理responseJSON。那么內部是如何實現類似這種有順序的訪問的呢?

答案就是使用隊列,任務按照順序依次放入到隊列中,就實現了上邊的功能,這里關於隊列在Alamofire中是如何使用的,會在接下來的文章中給出更詳細的解答。我在這里先給出一個粗略的說明:

  1. TaskDelegate中有一個屬性queue,下邊就是這個queue的初始化,這樣的寫法也是通過閉包來實現賦值的,值得注意的是operationQueue的isSuspended被賦值為true,這樣做的目的就是,當一系列的operation被添加到隊列中后,不會立刻執行,直到isSuspended等於false時才會。

       self.queue = {
                 let operationQueue = OperationQueue()
     
                 operationQueue.maxConcurrentOperationCount = 1
                 operationQueue.isSuspended = true
                 operationQueue.qualityOfService = .utility
     
                 return operationQueue
             }()
    
  2. 調用.responseString后放生了什么?其實,很簡單,就是給queue添加了一個操作

     delegate.queue.addOperation {
                 /// 這里就調用了responseSerializer保存的系列化函數,函數調用后會得到result
                 let result = responseSerializer.serializeResponse(
                     self.request,
                     self.response,
                     self.delegate.data,
                     self.delegate.error
                 )
     
                 /// 這里一定要記得,DataResponse是一個結構體,是專門為了純存儲數據的,這里是調用了結構體的初始化方法創建了一個新的DataResponse實例
                 var dataResponse = DataResponse<T.SerializedObject>(
                     request: self.request,
                     response: self.response,
                     data: self.delegate.data,
                     result: result,
                     timeline: self.timeline
                 )
     
                 dataResponse.add(self.delegate.metrics)
     
                 (queue ?? DispatchQueue.main).async { completionHandler(dataResponse) }
             }
    
  3. 當然還有其他的一些操作,比方說上傳完成后要刪除臨時文件等等,但歸根到底,這里用的就是隊列相關的知識

Alamofire中,默認的響應會放在主線程,那么我們該如何自定義響應線程呢?

let utilityQueue = DispatchQueue.global(qos: .utility)

Alamofire.request("https://httpbin.org/get").responseJSON(queue: utilityQueue) { response in
    print("Executing response handler on utility queue")
}

這主要得益於swift函數的參數可以設置默認值,有默認值得函數參數可以忽略。

3.驗證

Alamofire.request("https://httpbin.org/get")
    .validate(statusCode: 200..<300)
    .validate(contentType: ["application/json"])
    .responseData { response in
        switch response.result {
        case .success:
            print("Validation Successful")
        case .failure(let error):
            print(error)
        }
    }

上邊的這些代碼看上去很簡單,其實包含了一個復雜的過程。validate(statusCode: 200..<300)validate(contentType: ["application/json"])都返回的是Self,只有這樣才能夠保證鏈式的調用。那么這兩個驗證的結果要如何來獲取呢?

我們先看一個方法:

  @discardableResult
    public func validate<S: Sequence>(statusCode acceptableStatusCodes: S) -> Self where S.Iterator.Element == Int {
        return validate { [unowned self] _, response, _ in
            return self.validate(statusCode: acceptableStatusCodes, response: response)
        }
    }

這個方法就是validate(statusCode: 200..<300)的內部實現函數,可以看出來,在函數中調用了一個函數得到的返回值,那么這個被調用的函數validate只接受一個參數,這個參數也是一個函數。我們姑且稱這個函數為函數1. 接下來要看看validate函數的實現細節:

 @discardableResult
    public func validate(_ validation: @escaping Validation) -> Self {
        let validationExecution: () -> Void = { [unowned self] in
            if
                let response = self.response,
                self.delegate.error == nil,
                case let .failure(error) = validation(self.request, response, self.delegate.data)
            {
                self.delegate.error = error
            }
        }

        validations.append(validationExecution)

        return self
    }

可以看出,函數內部調用了它的參數,這個參數也就是在上邊傳遞過來的函數1。這個可能比較繞,不太好理解。這個會在ResponseSerialization.swift那篇文章中進行詳細解釋的。

雖然我們可能通過下邊的方法來判斷是不是驗證成功:

switch response.result {
        case .success:
            print("Validation Successful")
        case .failure(let error):
            print(error)
        }

我們仍然可以通過result訪問到序列化后的數據

switch response.result {
        case .success(data):
            print("Validation Successful data:\(data)")
        case .failure(let error):
            print(error)
        }

如果使用自動驗證的話,它會驗證200..<300的狀態嗎和發請求時提供的可接受的ContentType類型。

Alamofire.request("https://httpbin.org/get").validate().responseJSON { response in
    switch response.result {
    case .success:
        print("Validation Successful")
    case .failure(let error):
        print(error)
    }
}

4.HTTP方法

public enum HTTPMethod: String {
    case options = "OPTIONS"
    case get     = "GET"
    case head    = "HEAD"
    case post    = "POST"
    case put     = "PUT"
    case patch   = "PATCH"
    case delete  = "DELETE"
    case trace   = "TRACE"
    case connect = "CONNECT"
}

Alamofire提供了上邊的HTTPMethod,至於每個方法的使用詳情,請參考我寫的這篇文章。那么在請求中是這么使用的:

Alamofire.request("https://httpbin.org/get") // method defaults to `.get`

Alamofire.request("https://httpbin.org/post", method: .post)
Alamofire.request("https://httpbin.org/put", method: .put)
Alamofire.request("https://httpbin.org/delete", method: .delete)

5.Parameter Encoding

Alamofire支持三種參數編碼方式:URLJSONPropertyList。也可以通過實現ParameterEncoding協議來自定義編碼方式。

我們先看URL編碼:

URLEncoding是對URL編碼的封裝,通過一個enum提供3種編碼方式:

 public enum Destination {
        case methodDependent, queryString, httpBody
    }
  • methodDependent 表示根據HTTPMethod來判斷如何編碼,.get, .head, .delete情況下會把參數編入URL之中
  • queryString 表示把參數編入URL之中
  • httpBody 表示把參數編入httpBody之中

當然這些東西現不在這里做過多的解釋了,在開發中也用的不多,詳細的解釋會放到后邊ParameterEncoding.swift這一片文章之中。

JSON

我們把參數以JSON的方式編碼,如果在開發中用到了,需要在請求的header中設置

ContentTypeapplication/json

let parameters: Parameters = [
    "foo": [1,2,3],
    "bar": [
        "baz": "qux"
    ]
]

// Both calls are equivalent
Alamofire.request("https://httpbin.org/post", method: .post, parameters: parameters, encoding: JSONEncoding.default)
Alamofire.request("https://httpbin.org/post", method: .post, parameters: parameters, encoding: JSONEncoding(options: []))

// HTTP body: {"foo": [1, 2, 3], "bar": {"baz": "qux"}}

PropertyList

這個跟JSON很像,如果在開發中用到了,需要在請求的header中設置

ContentTypeapplication/x-plist

如果我們要自定義參數編碼,那該怎么辦呢?下邊是Alamofire的一個例子:

struct JSONStringArrayEncoding: ParameterEncoding {
    private let array: [String]

    init(array: [String]) {
        self.array = array
    }

    func encode(_ urlRequest: URLRequestConvertible, with parameters: Parameters?) throws -> URLRequest {
        var urlRequest = urlRequest.urlRequest

        let data = try JSONSerialization.data(withJSONObject: array, options: [])

        if urlRequest.value(forHTTPHeaderField: "Content-Type") == nil {
            urlRequest.setValue("application/json", forHTTPHeaderField: "Content-Type")
        }

        urlRequest.httpBody = data

        return urlRequest
    }
}

該例子中的JSONStringArrayEncoding實現了ParameterEncoding協議,實現了協議中的方法,這是一個典型的自定義編碼方式,在開發中這么使用:

Alamofire.request("https://xxxxx", method: .get, parameters: nil, encoding: JSONStringArrayEncoding(array: ["abc", "ddd"]), headers: nil)

當然我們也可以把ParameterEncoding當做一個API來使用:

let url = URL(string: "https://httpbin.org/get")!
var urlRequest = URLRequest(url: url)

let parameters: Parameters = ["foo": "bar"]
let encodedURLRequest = try URLEncoding.queryString.encode(urlRequest, with: parameters)

6.請求頭

客戶端每發起一次HTTP請求,請求頭信息是必不可少的。這也是同服務器交流的一種手段,在實際的開發中,也肯定會遇到需要自定義請求頭的需求,那么我們就看看,在Alamofire中如何設置請求頭:

let headers: HTTPHeaders = [
    "Authorization": "Basic QWxhZGRpbjpvcGVuIHNlc2FtZQ==",
    "Accept": "application/json"
]

Alamofire.request("https://httpbin.org/headers", headers: headers).responseJSON { response in
    debugPrint(response)
}

很簡單,在request(...)函數中,存在headers這么一個參數,我們只要傳入提前寫好的字典就行了。當然,使用URLSessionConfiguration來配置全局的屬性更加有優勢,因為上邊的方法只是針對某一個請求的,如果有很多的請求都需要添加請求頭,那么就應該使用URLSessionConfiguration來配置了。

需要說明的是,Alamofire為每一個請求都設置了默認的請求頭,我們簡單介紹一下:

  • Accept-Encoding 表示可接受的編碼方式,值為:gzip;q=1.0, compress;q=0.5
  • Accept-Language 表示可接受的語言,這個在后邊的文章中會詳細說明
  • User-Agent 表示用戶代理信息,比如:iOS Example/1.0 (com.alamofire.iOS-Example; build:1; iOS 10.0.0) Alamofire/4.0.0

默認的情況下,我們通過SessionManager.default來創建SessionManager:

   open static let `default`: SessionManager = {
        let configuration = URLSessionConfiguration.default
        configuration.httpAdditionalHeaders = SessionManager.defaultHTTPHeaders

        return SessionManager(configuration: configuration)
    }()

如果我們想自定義Accept-Encoding Accept-Language User-Agent,那該怎么辦呢? 答案就是使用下邊的這個方法:

  public init(
        configuration: URLSessionConfiguration = URLSessionConfiguration.default,
        delegate: SessionDelegate = SessionDelegate(),
        serverTrustPolicyManager: ServerTrustPolicyManager? = nil)
    {
        self.delegate = delegate
        self.session = URLSession(configuration: configuration, delegate: delegate, delegateQueue: nil)

        commonInit(serverTrustPolicyManager: serverTrustPolicyManager)
    }

通過configuration來設置自定義的請求頭,但需要注意的是,通過這個初始化方法創建的SessionManager不在是一個單利了,要想繼續使用單利,可能需要自己繼承SessionManager,然后手動實現單利。

7.HTTP 基本認證

在Alamofire中有三種使用基本認證的方法:

  • 在request(...)和response之間,拼接authenticate(user: user, password: password)

      let user = "user"
      let password = "password"
      
      Alamofire.request("https://httpbin.org/basic-auth/\(user)/\(password)")
          .authenticate(user: user, password: password)
          .responseJSON { response in
              debugPrint(response)
          }
    
  • 手動生成headers,Request.authorizationHeader(user: user, password: password) 返回一個元組(key: String, value: String)?

      let user = "user"
      let password = "password"
      
      var headers: HTTPHeaders = [:]
      
      if let authorizationHeader = Request.authorizationHeader(user: user, password: password) {
          headers[authorizationHeader.key] = authorizationHeader.value
      }
      
      Alamofire.request("https://httpbin.org/basic-auth/user/password", headers: headers)
          .responseJSON { response in
              debugPrint(response)
          }
    
  • 使用URLCredential

      let user = "user"
      let password = "password"
      
      let credential = URLCredential(user: user, password: password, persistence: .forSession)
      
      Alamofire.request("https://httpbin.org/basic-auth/\(user)/\(password)")
          .authenticate(usingCredential: credential)
          .responseJSON { response in
              debugPrint(response)
          }
    

8.下載文件

Alamofire允許把服務器返回的數據加載到內存或硬盤之中,**凡是以Alamofire.request開頭的請求都是把數據加載進內存,那么為什么還要區分內存和硬盤呢?相對於比較小的數據,加載進內存是高效的,但對於比較大的文件,加載進內存確實災難性的,因為很可能造成內存崩潰。因此,在處理大文件這個問題上,我們應該用Alamofire.download把數據保存到一個臨時的本地文件中。

比如,我們獲取一個圖片:

Alamofire.download("https://httpbin.org/image/png").responseData { response in
    if let data = response.result.value {
        let image = UIImage(data: data)
    }
}

即使APP在后台,download也是支持的。

需要注意的是,Alamofire.download返回的是DownloadRequest,它的response的類型是DownloadResponse,這里邊包含temporaryURLdestinationURL這兩個屬性,也就是說,如果我們沒有指定Destination,那么文件就默認下載到temporaryURL,通過他也可以訪問到文件。

要想自定義指定的目標路徑,我們需要創建一個DownloadFileDestination的閉包,我們先看看這個閉包的原型:

public typealias DownloadFileDestination = (
    _ temporaryURL: URL,
    _ response: HTTPURLResponse)
    -> (destinationURL: URL, options: DownloadOptions)

可以看出,該函數有兩個參數,temporaryURL和response,要求返回一個元組,包含目標路徑和選型,我們在看看這個DownloadOptions:

  • createIntermediateDirectories 表示會根據路徑來創建中間的文件夾
  • removePreviousFile 表示會移除指定路徑上之前的文件

這里指的注意的是DownloadOptions使用掩碼來實現的,這就說明可以同時選中這兩個選項 我們來看個例子:

let destination: DownloadRequest.DownloadFileDestination = { _, _ in
    let documentsURL = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask)[0]
    let fileURL = documentsURL.appendPathComponent("pig.png")

    return (fileURL, [.removePreviousFile, .createIntermediateDirectories])
}

Alamofire.download(urlString, to: destination).response { response in
    print(response)

    if response.error == nil, let imagePath = response.destinationURL?.path {
        let image = UIImage(contentsOfFile: imagePath)
    }
}

另外一種用法就是使用Alamofire建議的路徑,我們先看一個例子:

let destination = DownloadRequest.suggestedDownloadDestination(directory: .documentDirectory)
Alamofire.download("https://httpbin.org/image/png", to: destination)

再來看看suggestedDownloadDestination函數的實現:

 open class func suggestedDownloadDestination(
        for directory: FileManager.SearchPathDirectory = .documentDirectory,
        in domain: FileManager.SearchPathDomainMask = .userDomainMask)
        -> DownloadFileDestination
    {
        return { temporaryURL, response in
            let directoryURLs = FileManager.default.urls(for: directory, in: domain)

            if !directoryURLs.isEmpty {
                return (directoryURLs[0].appendingPathComponent(response.suggestedFilename!), [])
            }

            return (temporaryURL, [])
        }
    }

可以看出來,suggestedDownloadDestination需要指定directory和domain,當然他們也都有默認值,文件名則采用的是response.suggestedFilename!

說道下載,就不得不提下載進度,我們來看看Alamofire是怎么用下載進度的:

Alamofire.download("https://httpbin.org/image/png")
    .downloadProgress { progress in
        print("Download Progress: \(progress.fractionCompleted)")
    }
    .responseData { response in
        if let data = response.result.value {
            let image = UIImage(data: data)
        }
    }

大概說一下監聽進度的基本原理,詳細的實現方法會在后續的文章中提供,當下載文件開始之后,就會有一個數據寫入的代理方法被調用,就是在這個方法中處理進度的。我們看看這個進度函數:

@discardableResult
open func downloadProgress(queue: DispatchQueue = DispatchQueue.main, closure: @escaping ProgressHandler) -> Self {
    dataDelegate.progressHandler = (closure, queue)
    return self
}

可以看出來除了一個閉包參數意外還有另外一個參數,就是隊列,作用就是指定閉包在那個隊列中被調用,我們在開發中,這么使用:

let utilityQueue = DispatchQueue.global(qos: .utility)

Alamofire.download("https://httpbin.org/image/png")
    .downloadProgress(queue: utilityQueue) { progress in
        print("Download Progress: \(progress.fractionCompleted)")
    }
    .responseData { response in
        if let data = response.result.value {
            let image = UIImage(data: data)
        }
    }

還有一種特殊的情況,就是恢復下載數據,當一個下載任務因為一些原因被取消或者中斷后,后返回一個resumeData,我們可以使用這個resumeData重新發起一個請求,具體使用方法如下:

class ImageRequestor {
    private var resumeData: Data?
    private var image: UIImage?

    func fetchImage(completion: (UIImage?) -> Void) {
        guard image == nil else { completion(image) ; return }

        let destination: DownloadRequest.DownloadFileDestination = { _, _ in
            let documentsURL = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask)[0]
            let fileURL = documentsURL.appendPathComponent("pig.png")

            return (fileURL, [.removePreviousFile, .createIntermediateDirectories])
        }

        let request: DownloadRequest

        if let resumeData = resumeData {
            request = Alamofire.download(resumingWith: resumeData)
        } else {
            request = Alamofire.download("https://httpbin.org/image/png")
        }

        request.responseData { response in
            switch response.result {
            case .success(let data):
                self.image = UIImage(data: data)
            case .failure:
                self.resumeData = response.resumeData
            }
        }
    }
}

9.上傳文件

在開發中,當需要上傳的數據很小的時候,我們往往通過JSON或者URL把參數上傳到服務器,但是遇到數據量比較大的情況,在Alamofire中就要采用upload的方式上傳數據。

假設我們有一張圖片要上傳:

let imageData = UIPNGRepresentation(image)!

Alamofire.upload(imageData, to: "https://httpbin.org/post").responseJSON { response in
    debugPrint(response)
}

或者這樣上傳:

let fileURL = Bundle.main.url(forResource: "video", withExtension: "mov")

Alamofire.upload(fileURL, to: "https://httpbin.org/post").responseJSON { response in
    debugPrint(response)
}

在Alamofire中處理上傳數據的方式有以下幾種:

  • Data
  • fileURL
  • inputStream
  • MultipartFormData

前三種用起來比較簡單,我們接下來講講MultipartFormData的使用方法:

Alamofire.upload(
    multipartFormData: { multipartFormData in
        multipartFormData.append(unicornImageURL, withName: "unicorn")
        multipartFormData.append(rainbowImageURL, withName: "rainbow")
    },
    to: "https://httpbin.org/post",
    encodingCompletion: { encodingResult in
        switch encodingResult {
        case .success(let upload, _, _):
            upload.responseJSON { response in
                debugPrint(response)
            }
        case .failure(let encodingError):
            print(encodingError)
        }
    }
)

這段代碼需要注意的有幾個地方。

  • 數據是通過 multipartFormData.append拼接起來的,append需要兩個參數,其中一個參數是獲取數據的方式,另一個是數據名稱,這個名稱一定要給,主要用於給多表單數據的Content-Disposition中的name字段賦值。這個在后續的文章中也會給出詳細解釋。
  • encodingCompletion並不是上傳成功后的回調函數,而是所有要上傳的數據編碼后的回調。那么我們需要對編碼結果做出判斷,這樣做的好處就是,如果數據編碼失敗了,就沒必要發送數據給服務器。
  • encodingResult的結果,如果是成功的,那么它會返回一個UploadRequest,我們就通過這個UploadRequest綁定response事件。

再就是在上傳文件的時候監聽進度了,使用方法:

let fileURL = Bundle.main.url(forResource: "video", withExtension: "mov")

Alamofire.upload(fileURL, to: "https://httpbin.org/post")
    .uploadProgress { progress in // main queue by default
        print("Upload Progress: \(progress.fractionCompleted)")
    }
    .downloadProgress { progress in // main queue by default
        print("Download Progress: \(progress.fractionCompleted)")
    }
    .responseJSON { response in
        debugPrint(response)
    }

10.統計度量

Alamofire提供了一個叫TimeLine的新特性,通過這個特性,我們能夠觀察跟請求相關的一些時間屬性,使用方法如下:

Alamofire.request("https://httpbin.org/get").responseJSON { response in
    print(response.timeline)
}

打印結果如下:

Latency: 0.428 seconds
Request Duration: 0.428 seconds
Serialization Duration: 0.001 seconds
Total Duration: 0.429 seconds

在ios10中,蘋果引入了URLSessionTaskMetrics ,這個APIs能夠提供很多跟請求響應相關的信息,在Alamofire中通過response.metrics來訪問這個屬性:

Alamofire.request("https://httpbin.org/get").responseJSON { response in
    print(response.metrics)
}

在使用的時候,一定要做版本檢測:

Alamofire.request("https://httpbin.org/get").responseJSON { response in
    if #available(iOS 10.0. *) {
        print(response.metrics)
    }
}	

11.打印請求

在開發中,經常做的一件事就是調試接口,如果有一種方案,能夠很容易的打印請求相關的參數,那么就再好不過了。Alamofire中的Request實現了CustomStringConvertibleCustomDebugStringConvertible協議,因此我們就可以通過下邊的方法來打印請求信息:

let request = Alamofire.request("https://httpbin.org/ip")

print(request)
// GET https://httpbin.org/ip (200)

打印調試模式下的信息:

let request = Alamofire.request("https://httpbin.org/get", parameters: ["foo": "bar"])
debugPrint(request)

結果如下:

$ curl -i \
    -H "User-Agent: Alamofire/4.0.0" \
    -H "Accept-Encoding: gzip;q=1.0, compress;q=0.5" \
    -H "Accept-Language: en;q=1.0,fr;q=0.9,de;q=0.8,zh-Hans;q=0.7,zh-Hant;q=0.6,ja;q=0.5" \
    "https://httpbin.org/get?foo=bar"

Alamofire的高級用法

1.Session Manager

Alamofire有一些高級的使用方法,最外層的方法都是通過Alamofire.request來訪問的,其內部是通過Alamofire.SessionManagerURLSessionConfiguration來實現的,因此我們可以通過修改這些屬性,來靈活的使用Request。

先看下邊的兩種請求方式,他們的作用是一樣的:

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

let sessionManager = Alamofire.SessionManager.default
sessionManager.request("https://httpbin.org/get")

通過URLSessionConfiguration我們能夠很靈活的修改網絡配置參數,比如超時時間等等,下邊我們就使用URLSessionConfiguration來創建SessionManager

使用Default Configuration創建SessionManage

let configuration = URLSessionConfiguration.default
let sessionManager = Alamofire.SessionManager(configuration: configuration)

使用Background Configuration創建SessionManage

let configuration = URLSessionConfiguration.background(withIdentifier: "com.example.app.background")
let sessionManager = Alamofire.SessionManager(configuration: configuration)

使用Ephemeral Configuration創建SessionManage

let configuration = URLSessionConfiguration.ephemeral
let sessionManager = Alamofire.SessionManager(configuration: configuration)

修改Configuration

var defaultHeaders = Alamofire.SessionManager.defaultHTTPHeaders
defaultHeaders["DNT"] = "1 (Do Not Track Enabled)"

let configuration = URLSessionConfiguration.default
configuration.httpAdditionalHeaders = defaultHeaders

let sessionManager = Alamofire.SessionManager(configuration: configuration)

對於AuthorizationContent-Type不建議通過Configuration來配置,建議使用Alamofire.request APIs中的headers來配置。

2.Session Delegate

在開發中,會有很多自定義代理事件的需求,Alamofire中提供了很多的閉包來解決這個問題,比如:

/// Overrides default behavior for URLSessionDelegate method `urlSession(_:didReceive:completionHandler:)`.
open var sessionDidReceiveChallenge: ((URLSession, URLAuthenticationChallenge) -> (URLSession.AuthChallengeDisposition, URLCredential?))?

/// Overrides default behavior for URLSessionDelegate method `urlSessionDidFinishEvents(forBackgroundURLSession:)`.
open var sessionDidFinishEventsForBackgroundURLSession: ((URLSession) -> Void)?

/// Overrides default behavior for URLSessionTaskDelegate method `urlSession(_:task:willPerformHTTPRedirection:newRequest:completionHandler:)`.
open var taskWillPerformHTTPRedirection: ((URLSession, URLSessionTask, HTTPURLResponse, URLRequest) -> URLRequest?)?

/// Overrides default behavior for URLSessionDataDelegate method `urlSession(_:dataTask:willCacheResponse:completionHandler:)`.
open var dataTaskWillCacheResponse: ((URLSession, URLSessionDataTask, CachedURLResponse) -> CachedURLResponse?)?

我們有兩種方法來修改Alamofire中默認的代理事件,一種是重寫這些代理函數:

let sessionManager = Alamofire.SessionManager(configuration: URLSessionConfiguration.default)
let delegate: Alamofire.SessionDelegate = sessionManager.delegate

delegate.taskWillPerformHTTPRedirection = { session, task, response, request in
    var finalRequest = request

    if
        let originalRequest = task.originalRequest,
        let urlString = originalRequest.url?.urlString,
        urlString.contains("apple.com")
    {
        finalRequest = originalRequest
    }

    return finalRequest
}

上邊的函數中,我們重新定義了重定向的函數。還有一種方法是繼承代理后,重寫父類的方法:

class LoggingSessionDelegate: SessionDelegate {
    override func urlSession(
        _ session: URLSession,
        task: URLSessionTask,
        willPerformHTTPRedirection response: HTTPURLResponse,
        newRequest request: URLRequest,
        completionHandler: @escaping (URLRequest?) -> Void)
    {
        print("URLSession will perform HTTP redirection to request: \(request)")

        super.urlSession(
            session,
            task: task,
            willPerformHTTPRedirection: response,
            newRequest: request,
            completionHandler: completionHandler
        )
    }
}

3.Request

request, download, upload stream這四個方法的返回值分別為DataRequest, DownloadRequest, UploadRequest StreamRequest,並且他們都繼承自Request.這四個子類有一些方法,比如:authenticate, validate, responseJSON uploadProgress,這些方法的返回值又都是Self,這么做的目的是為了實現鏈式訪問。

每一個請求都可以被暫停,恢復,和取消,分別使用下邊的方法:

  • suspend() 暫停
  • resume() 恢復, 在SessionManager中有一個屬性:startRequestsImmediately。他控制這請求是不是立刻發起,默認的值為true。
  • cancel() 取消 同時該請求的每一個監聽對象都會受到一個錯誤回調

4.路由請求

Alamofire支持通過URLConvertibleURLRequestConvertible這兩個協議來實現路由設計模式,路由的概念就是中轉站的意思,在Alamofire中,String, URL, URLComponents實現了URLConvertible協議。因此我們才能夠這么用:

let urlString = "https://httpbin.org/post"
Alamofire.request(urlString, method: .post)

let url = URL(string: urlString)!
Alamofire.request(url, method: .post)

let urlComponents = URLComponents(url: url, resolvingAgainstBaseURL: true)!
Alamofire.request(urlComponents, method: .post)

當然我們也可以根據實際開發需求,來自定義符合我們需求的路由。在Alamofire的官方演示中,是這么使用的:

extension User: URLConvertible {
    static let baseURLString = "https://example.com"

    func asURL() throws -> URL {
        let urlString = User.baseURLString + "/users/\(username)/"
        return try urlString.asURL()
    }
}

上邊的代碼讓User實現了URLConvertible協議,因此我們就可以直接使用下邊的方式發起請求:

let user = User(username: "mattt")
Alamofire.request(user) // https://example.com/users/mattt

URLRequestConvertible的用法也很神奇,我們直接看例子:

enum Router: URLRequestConvertible {
    case search(query: String, page: Int)

    static let baseURLString = "https://example.com"
    static let perPage = 50

    // MARK: URLRequestConvertible

    func asURLRequest() throws -> URLRequest {
        let result: (path: String, parameters: Parameters) = {
            switch self {
            case let .search(query, page) where page > 0:
                return ("/search", ["q": query, "offset": Router.perPage * page])
            case let .search(query, _):
                return ("/search", ["q": query])
            }
        }()

        let url = try Router.baseURLString.asURL()
        let urlRequest = URLRequest(url: url.appendingPathComponent(result.path))

        return try URLEncoding.default.encode(urlRequest, with: result.parameters)
    }
}

Router實現了URLRequestConvertible協議,因此我們就能夠使用下邊的這種方式請求數據:

Alamofire.request(Router.search(query: "foo bar", page: 1)) // https://example.com/search?q=foo%20bar&offset=50

上邊的Router就實現了根據query和page來生成一個request的過程。大家仔細回味下上邊封裝的Router,很有意思。

在看看下邊的這個封裝:

import Alamofire

enum Router: URLRequestConvertible {
    case createUser(parameters: Parameters)
    case readUser(username: String)
    case updateUser(username: String, parameters: Parameters)
    case destroyUser(username: String)

    static let baseURLString = "https://example.com"

    var method: HTTPMethod {
        switch self {
        case .createUser:
            return .post
        case .readUser:
            return .get
        case .updateUser:
            return .put
        case .destroyUser:
            return .delete
        }
    }

    var path: String {
        switch self {
        case .createUser:
            return "/users"
        case .readUser(let username):
            return "/users/\(username)"
        case .updateUser(let username, _):
            return "/users/\(username)"
        case .destroyUser(let username):
            return "/users/\(username)"
        }
    }

    // MARK: URLRequestConvertible

    func asURLRequest() throws -> URLRequest {
        let url = try Router.baseURLString.asURL()

        var urlRequest = URLRequest(url: url.appendingPathComponent(path))
        urlRequest.httpMethod = method.rawValue

        switch self {
        case .createUser(let parameters):
            urlRequest = try URLEncoding.default.encode(urlRequest, with: parameters)
        case .updateUser(_, let parameters):
            urlRequest = try URLEncoding.default.encode(urlRequest, with: parameters)
        default:
            break
        }

        return urlRequest
    }
}

上邊的代碼把對User的操作進行了封裝,因此我們在操作User的時候,不需要跟底層的數據打交道,按照這種設計寫出的代碼也更簡潔和具有可讀性。

Alamofire.request(Router.readUser("mattt")) // GET https://example.com/users/mattt

5.請求的適配和重試

Alampfire提供了RequestAdapterRequestRetrier這兩個協議來進行請求適配和重試的。

RequestAdapter協議允許開發者改變request,這在實際應用中,會有很多實用場景,比如給請求中添加某個header:

class AccessTokenAdapter: RequestAdapter {
    private let accessToken: String

    init(accessToken: String) {
        self.accessToken = accessToken
    }

    func adapt(_ urlRequest: URLRequest) throws -> URLRequest {
        var urlRequest = urlRequest

        if let urlString = urlRequest.url?.absoluteString, urlString.hasPrefix("https://httpbin.org") {
            urlRequest.setValue("Bearer " + accessToken, forHTTPHeaderField: "Authorization")
        }

        return urlRequest
    }
}

當AccessTokenAdapter成為某個SessionManager的適配者之后,SessionManager的每一個請求都會被這個AccessTokenAdapter適配一遍。具體的代碼實現邏輯會在后續的章節中給出。那么到這里,我們已經掌握了好幾種添加headers得到方法了。AccessTokenAdapter的使用方法:

let sessionManager = SessionManager()
sessionManager.adapter = AccessTokenAdapter(accessToken: "1234")

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

關於RequestAdapterRequestRetrier的綜合運用,Alamofire給出了一個一個這樣的例子:

class OAuth2Handler: RequestAdapter, RequestRetrier {
    private typealias RefreshCompletion = (_ succeeded: Bool, _ accessToken: String?, _ refreshToken: String?) -> Void

    private let sessionManager: SessionManager = {
        let configuration = URLSessionConfiguration.default
        configuration.httpAdditionalHeaders = SessionManager.defaultHTTPHeaders

        return SessionManager(configuration: configuration)
    }()

    private let lock = NSLock()

    private var clientID: String
    private var baseURLString: String
    private var accessToken: String
    private var refreshToken: String

    private var isRefreshing = false
    private var requestsToRetry: [RequestRetryCompletion] = []

    // MARK: - Initialization

    public init(clientID: String, baseURLString: String, accessToken: String, refreshToken: String) {
        self.clientID = clientID
        self.baseURLString = baseURLString
        self.accessToken = accessToken
        self.refreshToken = refreshToken
    }

    // MARK: - RequestAdapter

    func adapt(_ urlRequest: URLRequest) throws -> URLRequest {
        if let urlString = urlRequest.url?.absoluteString, urlString.hasPrefix(baseURLString) {
            var urlRequest = urlRequest
            urlRequest.setValue("Bearer " + accessToken, forHTTPHeaderField: "Authorization")
            return urlRequest
        }

        return urlRequest
    }

    // MARK: - RequestRetrier

    func should(_ manager: SessionManager, retry request: Request, with error: Error, completion: @escaping RequestRetryCompletion) {
        lock.lock() ; defer { lock.unlock() }

        if let response = request.task?.response as? HTTPURLResponse, response.statusCode == 401 {
            requestsToRetry.append(completion)

            if !isRefreshing {
                refreshTokens { [weak self] succeeded, accessToken, refreshToken in
                    guard let strongSelf = self else { return }

                    strongSelf.lock.lock() ; defer { strongSelf.lock.unlock() }

                    if let accessToken = accessToken, let refreshToken = refreshToken {
                        strongSelf.accessToken = accessToken
                        strongSelf.refreshToken = refreshToken
                    }

                    strongSelf.requestsToRetry.forEach { $0(succeeded, 0.0) }
                    strongSelf.requestsToRetry.removeAll()
                }
            }
        } else {
            completion(false, 0.0)
        }
    }

    // MARK: - Private - Refresh Tokens

    private func refreshTokens(completion: @escaping RefreshCompletion) {
        guard !isRefreshing else { return }

        isRefreshing = true

        let urlString = "\(baseURLString)/oauth2/token"

        let parameters: [String: Any] = [
            "access_token": accessToken,
            "refresh_token": refreshToken,
            "client_id": clientID,
            "grant_type": "refresh_token"
        ]

        sessionManager.request(urlString, method: .post, parameters: parameters, encoding: JSONEncoding.default)
            .responseJSON { [weak self] response in
                guard let strongSelf = self else { return }

                if 
                    let json = response.result.value as? [String: Any], 
                    let accessToken = json["access_token"] as? String, 
                    let refreshToken = json["refresh_token"] as? String 
                {
                    completion(true, accessToken, refreshToken)
                } else {
                    completion(false, nil, nil)
                }

                strongSelf.isRefreshing = false
            }
    }
}

我們把上邊的代碼拆解成以下的使用場景:

  • 客戶端發送的每一個請求都要包含一個token,這個token很可能會過期,過期的token不能使用,因此通過adapt方法把token添加到請求的header中
  • 當使用現有的token請求失敗后,如果是token過期導致的請求失敗,那么就通過should方法重新申請一個新的token

使用方法:

let baseURLString = "https://some.domain-behind-oauth2.com"

let oauthHandler = OAuth2Handler(
    clientID: "12345678",
    baseURLString: baseURLString,
    accessToken: "abcd1234",
    refreshToken: "ef56789a"
)

let sessionManager = SessionManager()
sessionManager.adapter = oauthHandler
sessionManager.retrier = oauthHandler

let urlString = "\(baseURLString)/some/endpoint"

sessionManager.request(urlString).validate().responseJSON { response in
    debugPrint(response)
}

6.自定義響應序列者

關於Alamofire中自定義序列響應者。Alamofire已經為我們提供了Data,JSON,strings和property lists的解析。為了演示自定義的功能,我們要完成一下兩件事:

  • 為Alamofire擴展一個XML的解析
  • 直接把服務器返回的數據解析成對象,比方說User

為Alamofire擴展一個XML的解析

在做任何事情事前,都應該先設計好錯誤處理方案:

enum BackendError: Error {
    case network(error: Error) // Capture any underlying Error from the URLSession API
    case dataSerialization(error: Error)
    case jsonSerialization(error: Error)
    case xmlSerialization(error: Error)
    case objectSerialization(reason: String)
}

XML解析:

extension DataRequest {
    static func xmlResponseSerializer() -> DataResponseSerializer<ONOXMLDocument> {
        return DataResponseSerializer { request, response, data, error in
            // Pass through any underlying URLSession error to the .network case.
            guard error == nil else { return .failure(BackendError.network(error: error!)) }

            // Use Alamofire's existing data serializer to extract the data, passing the error as nil, as it has
            // already been handled.
            let result = Request.serializeResponseData(response: response, data: data, error: nil)

            guard case let .success(validData) = result else {
                return .failure(BackendError.dataSerialization(error: result.error! as! AFError))
            }

            do {
                let xml = try ONOXMLDocument(data: validData)
                return .success(xml)
            } catch {
                return .failure(BackendError.xmlSerialization(error: error))
            }
        }
    }

    @discardableResult
    func responseXMLDocument(
        queue: DispatchQueue? = nil,
        completionHandler: @escaping (DataResponse<ONOXMLDocument>) -> Void)
        -> Self
    {
        return response(
            queue: queue,
            responseSerializer: DataRequest.xmlResponseSerializer(),
            completionHandler: completionHandler
        )
    }
}

可以看出,這個解析是在DataRequest基礎上進行擴展的,當然也可以在DownloadRequest上擴展,xmlResponseSerializer函數的返回值是一個函數,這種處理方式在Alamofire中經常出現,完全可以把函數當成一種數據來對待。response函數會把這個閉包函數加入到task代理的隊列中,在請求完成后會被調用,總之,這是一系列的過程,我會在后續的文章中詳細說明。

- 直接把服務器返回的數據解析成對象,比方說User

在開發中,能夠直接把服務器返回的數據轉換成對象還是很有價值的。接下來我們看看用代碼是如何實現的:

protocol ResponseObjectSerializable {
    init?(response: HTTPURLResponse, representation: Any)
}

extension DataRequest {
    func responseObject<T: ResponseObjectSerializable>(
        queue: DispatchQueue? = nil,
        completionHandler: @escaping (DataResponse<T>) -> Void)
        -> Self
    {
        let responseSerializer = DataResponseSerializer<T> { request, response, data, error in
            guard error == nil else { return .failure(BackendError.network(error: error!)) }

            let jsonResponseSerializer = DataRequest.jsonResponseSerializer(options: .allowFragments)
            let result = jsonResponseSerializer.serializeResponse(request, response, data, nil)

            guard case let .success(jsonObject) = result else {
                return .failure(BackendError.jsonSerialization(error: result.error!))
            }

            guard let response = response, let responseObject = T(response: response, representation: jsonObject) else {
                return .failure(BackendError.objectSerialization(reason: "JSON could not be serialized: \(jsonObject)"))
            }

            return .success(responseObject)
        }

        return response(queue: queue, responseSerializer: responseSerializer, completionHandler: completionHandler)
    }
}

ResponseObjectSerializable這個協議是關鍵,這個協議提供了一個初始化方法,方法的參數有兩個,一個是服務器返回的響應,另一個是被轉化后的數據,着這個例子中使用的是JSON。也就是說對象一定要實現這個協議,在這個協議方法中拿到這兩個參數,然后給自己的屬性賦值就可以了 。

User的代碼:

struct User: ResponseObjectSerializable, CustomStringConvertible {
    let username: String
    let name: String

    var description: String {
        return "User: { username: \(username), name: \(name) }"
    }

    init?(response: HTTPURLResponse, representation: Any) {
        guard
            let username = response.url?.lastPathComponent,
            let representation = representation as? [String: Any],
            let name = representation["name"] as? String
        else { return nil }

        self.username = username
        self.name = name
    }
}

使用方法:

Alamofire.request("https://example.com/users/mattt").responseObject { (response: DataResponse<User>) in
    debugPrint(response)

    if let user = response.result.value {
        print("User: { username: \(user.username), name: \(user.name) }")
    }
}

Alamofire的文檔中還掩飾了一個系列成[User]的例子,由於篇幅的原因,在這里就不解釋了。

7.安全

Alamofire中關於安全策略的使用,會放到后邊的文章中介紹。

8.網絡狀態監控

主要用於實時監控當前的網絡情況

let manager = NetworkReachabilityManager(host: "www.apple.com")

manager?.listener = { status in
    print("Network Status Changed: \(status)")
}

manager?.startListening()

有一下幾點值得注意:

  • 不要用該監控來決定是不是發送請求,應該直接發送
  • 當網絡恢復之后,嘗試重新發送請求
  • 狀態嗎可以用來查看網絡問題的原因

總結

以上就是本篇的所有內容,知識大概的講解了Alamofire的使用技巧,真正能夠提高代碼水平的源碼解讀,我會盡量完成。

如果有任何錯誤之處,歡迎提出,多謝了。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM