盡管Alamofire的github文檔已經做了很詳細的說明,我還是想重新梳理一遍它的各種用法,以及這些方法的一些設計思想
前言
因為之前寫過一個AFNetworking的源碼解讀,所以就已經比較了解iOS平台的網絡框架是怎么一回事了。Alamofire
和AFNetworking
有很多相同的地方,然而,這些相同點在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中是如何使用的,會在接下來的文章中給出更詳細的解答。我在這里先給出一個粗略的說明:
-
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 }()
-
調用.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) } }
-
當然還有其他的一些操作,比方說上傳完成后要刪除臨時文件等等,但歸根到底,這里用的就是隊列相關的知識
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支持三種參數編碼方式:URL
,JSON
和PropertyList
。也可以通過實現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中設置
ContentType
為application/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中設置
ContentType
為application/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,這里邊包含temporaryURL
和destinationURL
這兩個屬性,也就是說,如果我們沒有指定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實現了CustomStringConvertible
和CustomDebugStringConvertible
協議,因此我們就可以通過下邊的方法來打印請求信息:
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.SessionManager
和URLSessionConfiguration
來實現的,因此我們可以通過修改這些屬性,來靈活的使用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)
對於
Authorization
和Content-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支持通過URLConvertible
和URLRequestConvertible
這兩個協議來實現路由設計模式,路由的概念就是中轉站的意思,在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提供了RequestAdapter
和RequestRetrier
這兩個協議來進行請求適配和重試的。
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")
關於RequestAdapter
和RequestRetrier
的綜合運用,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的使用技巧,真正能夠提高代碼水平的源碼解讀,我會盡量完成。
如果有任何錯誤之處,歡迎提出,多謝了。