Alamofire 是一款 Swift 寫的 HTTP 網(wǎng)絡(luò)請(qǐng)求庫(kù)
前言
本篇內(nèi)容為 Alamofire 官方 Readme 文件的翻譯,如有翻譯不恰當(dāng)?shù)牡胤?,您也可以給我提交PR,該翻譯github地址。本篇包含如下內(nèi)容:
Alamofire 是 Swift 語(yǔ)言編寫的 HTTP 網(wǎng)絡(luò)庫(kù)。
- 特性
- 組件
- 環(huán)境需求
- 移植指南
- 安裝
-
用法
-
簡(jiǎn)介 - 發(fā)起請(qǐng)求, 響應(yīng)回調(diào), 響應(yīng)驗(yàn)證, 響應(yīng)緩存
- HTTP - [HTTP 方法](#HTTP 方法), 請(qǐng)求參數(shù)編碼, HTTP Headers, 認(rèn)證
- 大量數(shù)據(jù) - 下載數(shù)據(jù)到文件, 上傳數(shù)據(jù)到服務(wù)器
- 工具 - 指標(biāo)統(tǒng)計(jì), [cURL 命令輸出](#cURL 命令輸出)
-
高級(jí)用法
- URL 會(huì)話 - 會(huì)話管理, 會(huì)話代理, 請(qǐng)求
- 請(qǐng)求路由 - 請(qǐng)求路由, Adapting and Retrying Requests
- 模型對(duì)象 - 自定義響應(yīng)序列化器
- 網(wǎng)絡(luò)連接 - 安全性, 網(wǎng)絡(luò)可用性
-
簡(jiǎn)介 - 發(fā)起請(qǐng)求, 響應(yīng)回調(diào), 響應(yīng)驗(yàn)證, 響應(yīng)緩存
- Open Radars
- FAQ
- 致謝
- 捐款
- 開(kāi)源協(xié)議
特性
- [x] 鏈?zhǔn)秸?qǐng)求 / 響應(yīng)方法調(diào)用
- [x] URL / JSON / plist 請(qǐng)求參數(shù)編碼
- [x] 上傳 File / Data / Stream / MultipartFormData
- [x] 文件下載和斷點(diǎn)續(xù)傳
- [x] URLCredential 認(rèn)證方式
- [x] HTTP 響應(yīng)驗(yàn)證
- [x] 上傳下載進(jìn)度
- [x] cURL 命令輸出
- [x] Dynamically Adapt and Retry Requests
- [x] TLS Certificate and Public Key Pinning
- [x] 網(wǎng)絡(luò)可用性
- [x] 測(cè)試單元和集成測(cè)試
組件
為了讓 Alamofire 集中于核心網(wǎng)絡(luò)操作的實(shí)現(xiàn),Alamofire Software Foundation 采用組件的形式為 Alamofire 添加額外的功能。
-
AlamofireImage - 一個(gè)包含圖片響應(yīng)序列化,
UIImage和UIImageView的擴(kuò)展,自定義圖片過(guò)濾器,自動(dòng)清理的內(nèi)存緩存,基于優(yōu)先級(jí)的圖片下載的圖片庫(kù)。 -
AlamofireNetworkActivityIndicator - 控制網(wǎng)絡(luò)活動(dòng)指示器的顯示。允許用戶配置延遲時(shí)間來(lái)延緩活動(dòng)指示器的顯示,同時(shí)也支持獨(dú)立創(chuàng)建的
URLSession實(shí)例對(duì)象。
環(huán)境需求
- iOS 8.0+ / macOS 10.10+ / tvOS 9.0+ / watchOS 2.0+
- Xcode 8.1+
- Swift 3.0+
移植指南
安裝
Cocoapods
CocoaPods 是一款 Cocoa 項(xiàng)目的依賴庫(kù)管理工具。使用下面命令安裝 cocoapods:
$ gem install cocoapods
編譯 Alamofire 4.0.0+ 需要 1.1.0 以上版本的 cocoapods
在 Podfile 中進(jìn)行聲明來(lái)集成 Alamofire:
source 'https://github.com/CocoaPods/Specs.git'
platform :ios, '10.0'
use_frameworks!
target '<Your Target Name>' do
pod 'Alamofire', '~> 4.0'
end
然后運(yùn)行下面的命令進(jìn)行安裝:
$ pod install
Carthage
Carthage 是一款編譯管理依賴庫(kù)的工具,可提供編譯好的 frameworks。
通過(guò) Homebrew 使用一下命令安裝 Carthage:
$ brew update
$ brew install carthage
在 Cartfile 中進(jìn)行聲明來(lái)集成 Alamofire:
github "Alamofire/Alamofire" ~> 4.0
運(yùn)行 carthage update 命令進(jìn)行編譯,并把編譯好的 Alamofire.framework 拖拽到你的 Xcode 項(xiàng)目中。
用法
發(fā)起請(qǐng)求
import Alamofire
Alamofire.request("https://httpbin.org/get")
響應(yīng)回調(diào)
處理請(qǐng)求的響應(yīng)只需在 Request 后面加上處理響應(yīng)的回調(diào)。
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)")
}
}
上面的例子中,responseJSON 回調(diào)拼接在 Request 后面,一旦網(wǎng)絡(luò)請(qǐng)求完成便會(huì)執(zhí)行該 responseJSON。這里沒(méi)有阻塞線程來(lái)等待響應(yīng),而是采用了閉包形式的回調(diào)來(lái)異步接受響應(yīng)。請(qǐng)求的結(jié)果只能在響應(yīng)的閉包中進(jìn)行處理。對(duì)響應(yīng)或從服務(wù)器接收到的數(shù)據(jù)只能在響應(yīng)閉包中處理。
Alamofire 中網(wǎng)絡(luò)請(qǐng)求是異步處理的。對(duì)異步編程相關(guān)的概念不太熟悉的話會(huì)讓人一頭霧水,但有太多的理由讓我們采用異步編程。
Alamofire 默認(rèn)包含五種響應(yīng)回調(diào):
// 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
這些回調(diào)都沒(méi)有對(duì) HTTPURLResponse 進(jìn)行驗(yàn)證。
比如,
400..499和500..599之間的響應(yīng)狀態(tài)碼不會(huì)自動(dòng)觸發(fā)Error。Alamofire 采用 Response Validation 的鏈?zhǔn)椒椒▉?lái)進(jìn)行驗(yàn)證。
Response 回調(diào)
response 不對(duì)響應(yīng)數(shù)據(jù)進(jìn)行任何處理,直接把 URL session 代理中的數(shù)據(jù)交給后面的流程,與使用 cURL 執(zhí)行請(qǐng)求效果一樣。
Alamofire.request("https://httpbin.org/get").response { response in
print("Request: \(response.request)")
print("Response: \(response.response)")
print("Error: \(response.error)")
if let data = response.data, let utf8Text = String(data: data, encoding: .utf8) {
print("Data: \(utf8Text)")
}
}
強(qiáng)烈建議使用其他響應(yīng)序列化器將數(shù)據(jù)變?yōu)楦资褂玫?Response 和 Result 類型
Response Data 回調(diào)
responseData 回調(diào)使用 responseDataSerializer (該對(duì)象用于將從服務(wù)器接收的數(shù)據(jù)序列化為其他類型) 處理服務(wù)器端返回的 Data。如果沒(méi)有錯(cuò)誤就返回 Data,response 的 Result 會(huì)被設(shè)置為 .success value 會(huì)被設(shè)置為 Data 類型。
Alamofire.request("https://httpbin.org/get").responseData { response in
debugPrint("All Response Info: \(response)")
if let data = response.result.value, let utf8Text = String(data: data, encoding: .utf8) {
print("Data: \(utf8Text)")
}
}
Response String 回調(diào)
responseString 回調(diào)使用 responseStringSerializer 根據(jù)指定的編碼方式將從服務(wù)器接收到的 Data 轉(zhuǎn)換為 String 類型。如果沒(méi)有錯(cuò)誤并且數(shù)據(jù)被成功序列化為 String,則 response 的 Result 會(huì)被設(shè)置為 .success,value 會(huì)被設(shè)置為 String 類型。
Alamofire.request("https://httpbin.org/get").responseString { response in
print("Success: \(response.result.isSuccess)")
print("Response String: \(response.result.value)")
}
如果未指定編碼方式,Alamofire 會(huì)根據(jù)從服務(wù)器端接收到的
HTTPURLResponse中的編碼方式進(jìn)行編碼。如果響應(yīng)中也未指定編碼方式,默認(rèn)使用.isoLatin1編碼方式
Response JSON 回調(diào)
responseJSON 使用 responseJSONSerializer 根據(jù)指定的 JSONSerialization.ReadingOptions 將從服務(wù)器接收到的數(shù)據(jù) Data 轉(zhuǎn)換為 Any 類型。如果沒(méi)有錯(cuò)誤并且接收到的數(shù)據(jù)成功的序列化為 JSON 對(duì)象,則 response 的 Result 會(huì)被設(shè)置為 .success 并且 value 會(huì)被設(shè)置為 Any 類型。
Alamofire.request("https://httpbin.org/get").responseJSON { response in
debugPrint(response)
if let json = response.result.value {
print("JSON: \(json)")
}
}
JSON 的序列化由
Foundation框架中的JSONSerialization接口完成。
鏈?zhǔn)巾憫?yīng)回調(diào)
響應(yīng)回調(diào)也可以鏈?zhǔn)秸{(diào)用:
Alamofire.request("https://httpbin.org/get")
.responseString { response in
print("Response String: \(response.result.value)")
}
.responseJSON { response in
print("Response JSON: \(response.result.value)")
}
注意:對(duì)同一個(gè)請(qǐng)求使用多個(gè)響應(yīng)回調(diào)需要服務(wù)器對(duì)數(shù)據(jù)進(jìn)行多次序列化,每次序列化針對(duì)一個(gè)響應(yīng)回調(diào)。
響應(yīng)回調(diào)的操作隊(duì)列
響應(yīng)回調(diào)默認(rèn)是在主派發(fā)隊(duì)列中執(zhí)行。然而可以為響應(yīng)回調(diào)指定自定義的操作隊(duì)列。
let utilityQueue = DispatchQueue.global(qos: .utility)
Alamofire.request("https://httpbin.org/get").responseJSON(queue: utilityQueue) { response in
print("Executing response handler on utility queue")
}
響應(yīng)驗(yàn)證
默認(rèn)情況下 Alamofire 會(huì)忽略響應(yīng)內(nèi)容是否正確,只要請(qǐng)求完成就標(biāo)志著成功。在響應(yīng)回調(diào)調(diào)用之前調(diào)用 validata 時(shí),若響應(yīng)中有錯(cuò)誤的網(wǎng)絡(luò)狀態(tài)碼或錯(cuò)誤的 MIME 格式的數(shù)據(jù)則會(huì)拋出錯(cuò)誤。
手動(dòng)驗(yàn)證
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)
}
}
自動(dòng)驗(yàn)證
自動(dòng)驗(yàn)證會(huì)驗(yàn)證 200...299 之間的狀態(tài)碼并驗(yàn)證響應(yīng)數(shù)據(jù)的 Content-Type 是否和請(qǐng)求頭的指定的 Accept 類型是否匹配。
Alamofire.request("https://httpbin.org/get").validate().responseJSON { response in
switch response.result {
case .success:
print("Validation Successful")
case .failure(let error):
print(error)
}
}
響應(yīng)緩存
響應(yīng)的緩存操作由系統(tǒng)級(jí)框架 URLCache 完成。其同時(shí)提供了內(nèi)存,硬盤兩種緩存方式并且用戶可以設(shè)置可緩存的大小。
Alamofire 默認(rèn)會(huì)使用共享的
URLCache。查看 Session Manager Configurations 進(jìn)行自定義。
HTTP 方法
HTTPMethod 枚舉出了 RFC 7231 §4.3 中定義的 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.request 接口的 method 參數(shù)設(shè)置這些值:
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)
Alamofire.request的 method 參數(shù)默認(rèn)是.get。
請(qǐng)求參數(shù)編碼
Alamofire 默認(rèn)提供了三種參數(shù)編碼方式,包括 URL,JSON,PropertyList。同時(shí)也支持遵循了 ParameterEncoding 協(xié)議的編碼方式。
URL 編碼
URLEncoding 編碼方式創(chuàng)建了 url 編碼的查詢字符串并將其拼接到存在的請(qǐng)求字符串后或者設(shè)置為 URL 請(qǐng)求的 HTTP body。對(duì)于編碼后的查詢字符串,是直接使用,拼接還是設(shè)置為 HTTP body 取決于編碼的 Destination。Destination 包含三種方式:
-
.methodDependent- 若請(qǐng)求方式是GET,HEAD,DELETE,則將編碼的查詢字符串與存在的查詢字符串進(jìn)行拼接,對(duì)于其他請(qǐng)求方式則設(shè)置為請(qǐng)求的 HTTP body。 -
.queryString- 將編碼的查詢字符串與存在的查詢字符串進(jìn)行拼接。 -
.httpBody- 設(shè)置為請(qǐng)求的 HTTP body。
請(qǐng)求頭中的 Content-Type 字段被設(shè)置為 application/x-www-form-urlencoded; charset=utf-8。URL 編碼中并沒(méi)有規(guī)定集合類型該如何進(jìn)行編碼。我們約定,對(duì)數(shù)組類型將[]拼接到 key 后面(foo[]=1&foo[]=2),對(duì)字典類型將中括號(hào)包圍的 key 拼接在請(qǐng)求的鍵后(foo[bar]=baz`)。
獲取使用 URL 編碼參數(shù)的請(qǐng)求
let parameters: Parameters = ["foo": "bar"]
// All three of these calls are equivalent
Alamofire.request("https://httpbin.org/get", parameters: parameters) // encoding defaults to `URLEncoding.default`
Alamofire.request("https://httpbin.org/get", parameters: parameters, encoding: URLEncoding.default)
Alamofire.request("https://httpbin.org/get", parameters: parameters, encoding: URLEncoding(destination: .methodDependent))
// https://httpbin.org/get?foo=bar
發(fā)起使用 URL 編碼參數(shù)的請(qǐng)求
let parameters: Parameters = [
"foo": "bar",
"baz": ["a", 1],
"qux": [
"x": 1,
"y": 2,
"z": 3
]
]
// All three of these calls are equivalent
Alamofire.request("https://httpbin.org/post", parameters: parameters)
Alamofire.request("https://httpbin.org/post", parameters: parameters, encoding: URLEncoding.default)
Alamofire.request("https://httpbin.org/post", parameters: parameters, encoding: URLEncoding.httpBody)
// HTTP body: foo=bar&baz[]=a&baz[]=1&qux[x]=1&qux[y]=2&qux[z]=3
JSON 編碼
JSONEncoding 的編碼方式創(chuàng)建了 JSON 格式的請(qǐng)求參數(shù),并設(shè)置為請(qǐng)求的 HTTP body。HTTP 請(qǐng)求頭的 Content-Type 字段設(shè)置為 applicatioin/json。
發(fā)起使用 JSON 編碼參數(shù)的請(qǐng)求
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"}}
自定義編碼
當(dāng) Alamofire 提供的參數(shù)編碼方式不能滿足需求時(shí),可以創(chuàng)建自定義的編碼方式。下面是一個(gè)自定義的 JSONStringEncoding 編碼方式的例子,該方式將 string 數(shù)組的 JSON 對(duì)象編碼到 Request 中。
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
}
}
下載數(shù)據(jù)到文件
Alamofire 從服務(wù)器接收到的數(shù)據(jù)保存在緩存或硬盤上。到目前為止所有例子中使用 Alamofire.request 接口獲取的數(shù)據(jù)都保存在緩存中。對(duì)于小數(shù)據(jù)這是很高效的,但對(duì)于較大的數(shù)據(jù)量可能會(huì)耗盡緩存。因此需要使用 Alamofire.download 接口將數(shù)據(jù)保存在硬盤的臨時(shí)文件中。
Alamofire.download("https://httpbin.org/image/png").responseData { response in
if let data = response.result.value {
let image = UIImage(data: data)
}
}
當(dāng)需要在后臺(tái)下載數(shù)據(jù)時(shí)也應(yīng)該使用
Alamofire.download接口。更多信息請(qǐng)查看 Session Manager Configurations 章節(jié)
下載路徑
你可以提供一個(gè) DownloadFileDestination 閉包用于把臨時(shí)文件移動(dòng)到指定的路徑下。在移動(dòng)臨時(shí)文件前會(huì)先執(zhí)行閉包中指定的 DownloadOptioins。當(dāng)前支持的兩種 DownloadOptions 分別是:
-
.createIntermediateDirectories- 為指定的路徑創(chuàng)建完整的路徑 -
.removePreviousFile- 移除目標(biāo)路徑下存在的文件
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)
}
}
也可以使用推薦下載路徑 API。
let destination = DownloadRequest.suggestedDownloadDestination(directory: .documentDirectory)
Alamofire.download("https://httpbin.org/image/png", to: destination)
下載進(jìn)度
在下載時(shí)能夠報(bào)告下載進(jìn)度是非常有用的。任何 DownloadRequest 請(qǐng)求可以通過(guò) downloadProgress 接口報(bào)告下載進(jìn)度。
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)
}
}
也可以為 downloadProgress 接口指定下載進(jìn)度閉包執(zhí)行的派發(fā)隊(duì)列。
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)
}
}
喚醒下載
如果一個(gè) DownloadRequest 請(qǐng)求取消或中斷了,URL 會(huì)話可能會(huì)為該請(qǐng)求生成恢復(fù)數(shù)據(jù),該恢復(fù)數(shù)據(jù)可用于 DownloadRequest 請(qǐng)求從中斷的地方恢復(fù)下載?;謴?fù)數(shù)據(jù)可以從下載響應(yīng)中獲取,然后用于恢復(fù)下載。
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
}
}
}
}
上傳數(shù)據(jù)到服務(wù)器
上傳少量的數(shù)據(jù)到服務(wù)器可以采用 JSON 或者 URL 編碼參數(shù)的方式進(jìn)行,這時(shí) Alamofire.request 接口通常很高效。當(dāng)需要上傳的數(shù)據(jù)較大,比如文件或者 InputStream,這時(shí)需要使用 Alamofire.upload 接口。
當(dāng)需要在后臺(tái)上傳數(shù)據(jù)時(shí)也應(yīng)該使用
Alamofire.upload,更多信息請(qǐng)查看 Session Manager Configurations 章節(jié)。
上傳數(shù)據(jù)
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)
}
上傳多格式表單數(shù)據(jù)
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)
}
}
)
上傳進(jìn)度
當(dāng)用戶在上傳時(shí)能夠顯示上傳進(jìn)度是非常友好的。任何 UploadRequest 請(qǐng)求都能通過(guò) uploadProgress 和 doanloadProgress 接口報(bào)告上傳進(jìn)度和下載進(jìn)度。
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)
}
Statistical Metrics
Timeline
Alamofire collects timings throughout the lifecycle of a Request and creates a Timeline object exposed as a property on all response types.
Alamofire.request("https://httpbin.org/get").responseJSON { response in
print(response.timeline)
}
The above reports the following Timeline info:
-
Latency: 0.428 seconds -
Request Duration: 0.428 seconds -
Serialization Duration: 0.001 seconds -
Total Duration: 0.429 seconds
URL Session Task Metrics
In iOS and tvOS 10 and macOS 10.12, Apple introduced the new URLSessionTaskMetrics APIs. The task metrics encapsulate some fantastic statistical information about the request and response execution. The API is very similar to the Timeline, but provides many more statistics that Alamofire doesn't have access to compute. The metrics can be accessed through any response type.
Alamofire.request("https://httpbin.org/get").responseJSON { response in
print(response.metrics)
}
注意,這些接口僅在 iOS,tvOS 10 和 macOS 10.12 三個(gè)平臺(tái)上可用。因此,取決于您的部署環(huán)境,您需要做以下檢測(cè):
Alamofire.request("https://httpbin.org/get").responseJSON { response in
if #available(iOS 10.0. *) {
print(response.metrics)
}
}
cURL 命令輸出
不好的調(diào)試平臺(tái)會(huì)讓工作變得很麻煩. 幸好, Alamofire Request 對(duì)象實(shí)現(xiàn)了 CustomStringConvertible 和 CustomDebugStringConvertible 協(xié)議,這為我們提供了很好的調(diào)試工具。
CustomStringConvertible
let request = Alamofire.request("https://httpbin.org/ip")
print(request)
// GET https://httpbin.org/ip (200)
CustomDebugStringConvertible
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"
高級(jí)用法
Alamofire 建立在 URLSession 和 URL 加載系統(tǒng)上。為了更好的使用該框架,強(qiáng)烈建議要非常熟悉底層網(wǎng)絡(luò)棧的相關(guān)概念
推薦閱讀
會(huì)話管理
頂層的 Alamofire 接口例如 Alamofire.request 使用了默認(rèn)的 Alamofire.SessionManager 會(huì)話管理對(duì)象發(fā)起網(wǎng)絡(luò)請(qǐng)求。該會(huì)話管理對(duì)象默認(rèn)使用了 URLSessionConfiguration 進(jìn)行配置。
因此下面兩段代碼的是等效的:
Alamofire.request("https://httpbin.org/get")
let sessionManager = Alamofire.SessionManager.default
sessionManager.request("https://httpbin.org/get")
您可以為應(yīng)用創(chuàng)建會(huì)后臺(tái)任務(wù)會(huì)話管理對(duì)象,臨時(shí)會(huì)話管理對(duì)象,同時(shí)也可以修改默認(rèn)的會(huì)話配置,比如默認(rèn)的請(qǐng)求頭 (httpAdditionalHeaders) 或者請(qǐng)求超時(shí)時(shí)間 (timeoutIntervalForRequest)。
創(chuàng)建默認(rèn)配置會(huì)話管理對(duì)象
let configuration = URLSessionConfiguration.default
let sessionManager = Alamofire.SessionManager(configuration: configuration)
創(chuàng)建后臺(tái)任務(wù)會(huì)話管理對(duì)象
let configuration = URLSessionConfiguration.background(withIdentifier: "com.example.app.background")
let sessionManager = Alamofire.SessionManager(configuration: configuration)
創(chuàng)建臨時(shí)配置會(huì)話管理對(duì)象
let configuration = URLSessionConfiguration.ephemeral
let sessionManager = Alamofire.SessionManager(configuration: configuration)
修改會(huì)話配置
var defaultHeaders = Alamofire.SessionManager.default.defaultHTTPHeaders
defaultHeaders["DNT"] = "1 (Do Not Track Enabled)"
let configuration = URLSessionConfiguration.default
configuration.httpAdditionalHeaders = defaultHeaders
let sessionManager = Alamofire.SessionManager(configuration: configuration)
不推薦使用這種方式修改
Authorization和Content-Type等請(qǐng)求頭信息。推薦使用Alamofire.request接口中的headers參數(shù),URLRequestConvertible和ParameterEncoding等方式修改請(qǐng)求頭信息。
會(huì)話代理
Alamofire 的會(huì)話管理對(duì)象默認(rèn)創(chuàng)建了一個(gè)會(huì)話代理對(duì)象來(lái)處理 URLSession 產(chǎn)生的各種代理回調(diào)事件。這些代理方法實(shí)現(xiàn)的功能能夠應(yīng)付絕大部分的使用場(chǎng)景并且為隱藏了復(fù)雜的內(nèi)部調(diào)用為用戶提供了簡(jiǎn)單的上層接口。然而,您仍有可能會(huì)因?yàn)楦鞣N各樣的需求而重載這些代理方法的實(shí)現(xiàn)。
重載閉包
第一種自定義 SessionDelegate 行為的方式是重載閉包。通過(guò)閉包您可以重載對(duì)應(yīng)的 SessionDelegate 接口,并且其他接口的實(shí)現(xiàn)將保持不變。這讓實(shí)現(xiàn)一個(gè)自定義的代理方法集合變得很容易。下面是一些可用的可重載的閉包:
/// 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?)?
下面的例子通過(guò)重載 taskWillPerformHTTPRedirection 閉包來(lái)避免請(qǐng)求重定向到apple.com的域名。
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
}
繼承
另一種重載 SessionDelegate 默認(rèn)實(shí)現(xiàn)的方式是繼承。通過(guò)繼承您可以實(shí)現(xiàn)完全的自定義或者仍然使用默認(rèn)實(shí)現(xiàn)僅為接口創(chuàng)建一個(gè)代理。通過(guò)為接口創(chuàng)建代理,您可以在調(diào)用接口默認(rèn)實(shí)現(xiàn)的前后增加日志消息,派發(fā)通知等功能。下面的例子繼承了 SessionDelegate,并且當(dāng)發(fā)生重定向時(shí)打印消息日志。
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
)
}
}
請(qǐng)求
request,download,upload,stream 等方法的返回值 DataRequest, DownloadRequest, UploadRequest 和 StreamRequest 均是繼承于 Request。所有的 Request 實(shí)例都是由自己的會(huì)話管理對(duì)象創(chuàng)建,并且不會(huì)直接初始化。
每個(gè)子類都有一些特殊的方法比如 authenticate, validate, responseJSON 和 uploadProgress,這些方法均返回調(diào)用者以便可以進(jìn)行鏈?zhǔn)秸{(diào)用。
請(qǐng)求可以被掛起,恢復(fù),取消:
-
suspend(): 掛起底層任務(wù)和派發(fā)隊(duì)列。 -
resume(): 恢復(fù)任務(wù)和派發(fā)隊(duì)列。如果會(huì)話管理對(duì)象沒(méi)有設(shè)置startRequestsImmediately為true,那么請(qǐng)求需要調(diào)用resume()才能開(kāi)始。 -
cancel(): 取消任務(wù),產(chǎn)生錯(cuò)誤信息并將錯(cuò)誤信息傳遞到響應(yīng)回調(diào)。
請(qǐng)求路由
隨著 App 變得復(fù)雜,使用通用模式創(chuàng)建你自己的網(wǎng)絡(luò)棧就變得非常重要了。其中一個(gè)重要的設(shè)計(jì)就是如何路由你的請(qǐng)求。遵循 URLConvertible 和 URLRequestConvertible 協(xié)議的 Router 就變得非常有用。
URLConvertible
遵循 URLConvertible 協(xié)議的類可以用來(lái)構(gòu)造 URLs,然后將 URLs 用來(lái)構(gòu)造 URL 請(qǐng)求。String, URL, 和 URLComponents 都遵循了 URLConvertible 協(xié)議,這三個(gè)類的對(duì)象均可以作為 url 參數(shù)傳遞給 request, upload, 和 download 方法:
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(.post, URLComponents)
與 web 服務(wù)器交互時(shí)推薦通過(guò)實(shí)現(xiàn) URLConvertible 協(xié)議來(lái)做域名型模型與服務(wù)器資源的映射。
類型安全路由
extension User: URLConvertible {
static let baseURLString = "https://example.com"
func asURL() throws -> URL {
let urlString = User.baseURLString + "/users/\(username)/"
return try urlString.asURL()
}
}
let user = User(username: "mattt")
Alamofire.request(user) // https://example.com/users/mattt
URLRequestConvertible
實(shí)現(xiàn)了 URLRequestConvertible 協(xié)議的類型可以用來(lái)構(gòu)造 URL 請(qǐng)求。URLRequest 默認(rèn)實(shí)現(xiàn)了 URLRequestConvertible 協(xié)議,這使得 URLRequest 可直接傳遞給 request,upload,download等方法(推薦使用這種方式實(shí)現(xiàn)自定義 HTTP body)
let url = URL(string: "https://httpbin.org/post")!
var urlRequest = URLRequest(url: url)
urlRequest.httpMethod = "POST"
let parameters = ["foo": "bar"]
do {
urlRequest.httpBody = try JSONSerialization.data(withJSONObject: parameters, options: [])
} catch {
// No-op
}
urlRequest.setValue("application/json", forHTTPHeaderField: "Content-Type")
Alamofire.request(urlRequest)
與 web 服務(wù)器交互時(shí)推薦通過(guò)實(shí)現(xiàn) URLRequestConvertible 協(xié)議以確保請(qǐng)求端點(diǎn)的一致性。這種方法可以用于抽象出服務(wù)器端不一致并提供類型安全路由,以及管理認(rèn)證憑證和其他狀態(tài)
API 抽象參數(shù)
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)
}
}
Alamofire.request(Router.search(query: "foo bar", page: 1)) // https://example.com/search?q=foo%20bar&offset=50
CRUD & Authorization
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
}
}
Alamofire.request(Router.readUser("mattt")) // GET https://example.com/users/mattt
Adapting and Retrying Requests
當(dāng)今的很多 web 服務(wù)都可以通過(guò)授權(quán)系統(tǒng)進(jìn)行訪問(wèn)。其中最常用的是 OAuth。OAuth 會(huì)生成一個(gè)訪問(wèn)令牌來(lái)授權(quán)你的應(yīng)用訪問(wèn)權(quán)限內(nèi)的 web 服務(wù)。創(chuàng)建令牌可能會(huì)很麻煩,令牌過(guò)期需要考慮很多線程安全的問(wèn)題,這會(huì)讓情況變得更復(fù)雜。
RequestAdapter 和 RequestRetrier 協(xié)議讓創(chuàng)建線程安全的授權(quán)系統(tǒng)變得容易。
RequestAdapter
RequestAdapter 協(xié)議允許 SessionManager 在創(chuàng)建 Request 前為 Request 做額外的檢查和適配工作。比較常用的應(yīng)用場(chǎng)景是為請(qǐng)求拼接授權(quán)參數(shù)。
class AccessTokenAdapter: RequestAdapter {
private let accessToken: String
init(accessToken: String) {
self.accessToken = accessToken
}
func adapt(_ urlRequest: URLRequest) throws -> URLRequest {
var urlRequest = urlRequest
if urlRequest.urlString.hasPrefix("https://httpbin.org") {
urlRequest.setValue("Bearer " + accessToken, forHTTPHeaderField: "Authorization")
}
return urlRequest
}
}
let sessionManager = SessionManager()
sessionManager.adapter = AccessTokenAdapter(accessToken: "1234")
sessionManager.request("https://httpbin.org/get")
RequestRetrier
RequestRetrier 協(xié)議允許網(wǎng)絡(luò)請(qǐng)求發(fā)生錯(cuò)誤時(shí)重新發(fā)起請(qǐng)求。通過(guò)同時(shí)實(shí)現(xiàn) RequestAdapter 和 RequestRetrier 協(xié)議,您可以為 OAuth1,OAuth2,基本授權(quán),重試策略創(chuàng)建一個(gè)證書(shū)刷新系統(tǒng)。您能實(shí)現(xiàn)的功能不局限于此。下面的例子展示了 OAuth2 令牌的刷新流程。
免責(zé)聲明: 這不是一個(gè)全局的
OAuth2解決方案。下面的代碼僅作為簡(jiǎn)單示例展示了如何通過(guò)RequestAdapter和RequestRetrier協(xié)議來(lái)實(shí)現(xiàn)線程安全的刷新系統(tǒng)。
重申,不要拷貝下面的示例代碼到您的產(chǎn)品中。該代碼片段僅能作為示例。每一個(gè)授權(quán)系統(tǒng)應(yīng)該基于平臺(tái)和授權(quán)類型做相應(yīng)的修改。
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 url = urlRequest.url, url.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
}
}
}
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)
}
SessionManager 的 adapter 和 retrier 被設(shè)置為 OAuth2Handler后,當(dāng)令牌失效時(shí),便會(huì)自動(dòng)刷新令牌并嘗試按失敗的順序重新發(fā)起請(qǐng)求。
如果您想按創(chuàng)建網(wǎng)絡(luò)請(qǐng)求的順序重新發(fā)起請(qǐng)求,您可以通過(guò)網(wǎng)絡(luò)請(qǐng)求任務(wù)的 id 進(jìn)行排序。
該示例僅檢查了響應(yīng)的 401 狀態(tài)碼,作為檢測(cè)失效令牌的例子這已經(jīng)足夠。O在實(shí)際產(chǎn)品中,您應(yīng)該還要檢測(cè)響應(yīng)頭中的 reaml 和 www-authenticate 等字段。
還需要注意的是該授權(quán)系統(tǒng)可以在多個(gè)會(huì)話管理對(duì)象間共享。比如,您可以為同一個(gè) web 服務(wù)集同時(shí)使用 default 和 ephemeral 會(huì)話配置。上面的例子允許 oauthHandler 實(shí)例對(duì)象在多個(gè)會(huì)話管理對(duì)象間共享并管理各自的刷新流程。
自定義響應(yīng)序列化器
錯(cuò)誤處理
過(guò)去在實(shí)現(xiàn)自定義響應(yīng)序列化器或?qū)ο笮蛄谢椒〞r(shí)著重考慮的是錯(cuò)誤信息的處理。這里有兩個(gè)可選項(xiàng):對(duì)錯(cuò)誤信息不做任何處理直接向下傳遞,由用戶在響應(yīng)回調(diào)處處理;或者為您的應(yīng)用定義一個(gè)包含所有錯(cuò)誤類型的 Error 枚舉類。
下面的 BackendError 枚舉類在后面的例子中也會(huì)出現(xiàn):
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)
}
自定義響應(yīng)序列化器
Alamofire 為 strings,JSON,property lsits 提供了內(nèi)置的響應(yīng)序列化器,您也可以為 Alamofire.DataRequest 和 Alamofire.DownloadRequest 進(jìn)行擴(kuò)展。
下面的例子展示了響應(yīng)序列化器使用 Ono 的實(shí)現(xiàn)方式:
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
)
}
}
通用響應(yīng)對(duì)象序列化
通用序列化可以進(jìn)行自動(dòng),類型安全的對(duì)象序列化。
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)
}
}
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) }")
}
}
相同的方法也可以用于處理終端返回的對(duì)象集合:
protocol ResponseCollectionSerializable {
static func collection(from response: HTTPURLResponse, withRepresentation representation: Any) -> [Self]
}
extension ResponseCollectionSerializable where Self: ResponseObjectSerializable {
static func collection(from response: HTTPURLResponse, withRepresentation representation: Any) -> [Self] {
var collection: [Self] = []
if let representation = representation as? [[String: Any]] {
for itemRepresentation in representation {
if let item = Self(response: response, representation: itemRepresentation) {
collection.append(item)
}
}
}
return collection
}
}
extension DataRequest {
@discardableResult
func responseCollection<T: ResponseCollectionSerializable>(
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 jsonSerializer = DataRequest.jsonResponseSerializer(options: .allowFragments)
let result = jsonSerializer.serializeResponse(request, response, data, nil)
guard case let .success(jsonObject) = result else {
return .failure(BackendError.jsonSerialization(error: result.error!))
}
guard let response = response else {
let reason = "Response collection could not be serialized due to nil response."
return .failure(BackendError.objectSerialization(reason: reason))
}
return .success(T.collection(from: response, withRepresentation: jsonObject))
}
return response(responseSerializer: responseSerializer, completionHandler: completionHandler)
}
}
struct User: ResponseObjectSerializable, ResponseCollectionSerializable, 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").responseCollection { (response: DataResponse<[User]>) in
debugPrint(response)
if let users = response.result.value {
users.forEach { print("- \($0)") }
}
}
請(qǐng)求適配器
安全性
在與 web 服務(wù)器交互傳輸敏感數(shù)據(jù)時(shí)應(yīng)該使用安全的 HTTPS 連接。默認(rèn)情況下,Alamofire 會(huì)使用蘋果提供的 Security 框架對(duì)服務(wù)器提供的證書(shū)串進(jìn)行驗(yàn)證。這樣僅僅能確保服務(wù)器端證書(shū)是否有效,并不能防止中間人攻擊 man-in-the-middle(MITM) 或其他潛在的漏洞。為了降低遭受中間人攻擊的可能性,應(yīng)用在處理敏感用戶的數(shù)據(jù)或金融信息時(shí)應(yīng)該配合使用證書(shū)或 ServerTrustPolicy 提供的公鑰鎖定
ServerTrustPolicy
Server Trust Policy Manager
繼承 Server Trust Policy Manager
驗(yàn)證主機(jī)
驗(yàn)證證書(shū)串
App Transport Security
<dict>
<key>NSAppTransportSecurity</key>
<dict>
<key>NSExceptionDomains</key>
<dict>
<key>example.com</key>
<dict>
<key>NSExceptionAllowsInsecureHTTPLoads</key>
<true/>
<key>NSExceptionRequiresForwardSecrecy</key>
<false/>
<key>NSIncludesSubdomains</key>
<true/>
<!-- Optional: Specify minimum TLS version -->
<key>NSTemporaryExceptionMinimumTLSVersion</key>
<string>TLSv1.2</string>
</dict>
</dict>
</dict>
</dict>
網(wǎng)絡(luò)可用性
NetworkReachabilityManager 可用于監(jiān)聽(tīng) WWAN 和 WiFi 網(wǎng)絡(luò)到指定主機(jī)或 IP 地址的連接狀態(tài)。
let manager = NetworkReachabilityManager(host: "www.apple.com")
manager?.listener = { status in
print("Network Status Changed: \(status)")
}
manager?.startListening()
請(qǐng)確保對(duì)
網(wǎng)絡(luò)狀態(tài)監(jiān)聽(tīng)對(duì)象有強(qiáng)引用,否則不會(huì)監(jiān)聽(tīng)到任何網(wǎng)絡(luò)狀態(tài)。
在監(jiān)聽(tīng)網(wǎng)絡(luò)狀態(tài)時(shí)需要注意以下幾點(diǎn):
-
不要根據(jù)網(wǎng)絡(luò)狀態(tài)來(lái)決定是否發(fā)送網(wǎng)絡(luò)請(qǐng)求。
- 只管發(fā)送就行
- 當(dāng)網(wǎng)絡(luò)恢復(fù)連接,對(duì)失敗的網(wǎng)絡(luò)請(qǐng)求重新發(fā)起請(qǐng)求。
- 盡管重新發(fā)起請(qǐng)求仍有可能失敗,但您仍應(yīng)該嘗試。
- 網(wǎng)絡(luò)狀態(tài)有助于分析出請(qǐng)求失敗原因。
- 如果網(wǎng)絡(luò)請(qǐng)求失敗,提示用戶網(wǎng)絡(luò)處于離線狀態(tài)要比更具體的錯(cuò)誤信息比如"請(qǐng)求超時(shí)"等更友好。
更多信息請(qǐng)參考 WWDC 2012 Session 706, "Networking Best Practices" for more info.
Open Radars
The following radars have some effect on the current implementation of Alamofire.
-
rdar://21349340- Compiler throwing warning due to toll-free bridging issue in test case -
rdar://26761490- Swift string interpolation causing memory leak with common usage -
rdar://26870455- Background URL Session Configurations do not work in the simulator -
rdar://26849668- Some URLProtocol APIs do not properly handleURLRequest
FAQ
Alamofire 名字由來(lái)
Alamofire 花,矢車菊的一種,是德克薩斯州的官方州花。
請(qǐng)求路由和請(qǐng)求適配器的區(qū)別
資源路徑,請(qǐng)求參數(shù),公共請(qǐng)求頭這些靜態(tài)數(shù)據(jù)屬于 路由 范疇。認(rèn)證 頭這類會(huì)隨著認(rèn)證系統(tǒng)發(fā)生變化的動(dòng)態(tài)數(shù)據(jù)屬于 請(qǐng)求適配器 范疇。
致謝
Alamofire 由 Alamofire 軟件基金會(huì) 所有并維護(hù)。您可以通過(guò)關(guān)注我們的 Twitter 官方賬號(hào) @AlamofireSF 來(lái)獲取最新的更新發(fā)布消息。
捐款
開(kāi)源協(xié)議
Alamofire 在 MIT 開(kāi)源協(xié)議下發(fā)布。更多信息請(qǐng)查看 LICENSE 文件。
歡迎關(guān)注我的簡(jiǎn)書(shū),我會(huì)定期做一些技術(shù)分享:)