@TOC
(一) Alamofire框架功能簡(jiǎn)介
- 前面寫(xiě)了三篇關(guān)于Alamofire框架的博客颊亮,基本是循序漸進(jìn)的方式,先講解了必須了解的網(wǎng)絡(luò)原理,協(xié)議等知識(shí),然后講解了蘋(píng)果自帶框架api UISession的一些知識(shí)橱脸。而Alamofire就是基于這些實(shí)現(xiàn)的網(wǎng)絡(luò)框架,專注于網(wǎng)絡(luò)相關(guān)的api分苇。
- 為了更好的理解Alamofire框架的實(shí)現(xiàn)原理慰技,很有必要先了解Alamofire框架是什么?它提供了一些什么功能组砚?如果正確高效的使用它吻商?
-
帶著這三個(gè)問(wèn)題,本篇博客將圍繞這三個(gè)方向來(lái)闡述糟红。
Alamofire框架 - (1) Alamofire框架是什么艾帐?
Alamofire就是牛逼的框架AFNetwork開(kāi)發(fā)者的母公司開(kāi)發(fā)的一套基于swift實(shí)現(xiàn)的網(wǎng)絡(luò)框架。Alamofire專注于核心網(wǎng)絡(luò)的實(shí)現(xiàn)盆偿,Alamofire生態(tài)系統(tǒng)還有另外兩個(gè)庫(kù):AlamofireImage柒爸,AlamofireNetworkActivityIndicator
- AlamofireImage:
一個(gè)圖片庫(kù),包括圖像響應(yīng)序列化器事扭、UIImage和UIImageView的擴(kuò)展捎稚、自定義圖像濾鏡、內(nèi)存中自動(dòng)清除和基于優(yōu)先級(jí)的圖像下載系統(tǒng)求橄。
- AlamofireNetworkActivityIndicator:
控制iOS應(yīng)用的網(wǎng)絡(luò)活動(dòng)指示器今野。包含可配置的延遲計(jì)時(shí)器來(lái)幫助減少閃光,并且支持不受Alamofire管理的URLSession實(shí)例罐农。
- (2) Alamofire框架提供了什么功能条霜?
- 鏈?zhǔn)秸?qǐng)求 / 響應(yīng)方法
- URL / JSON / plist參數(shù)編碼
- 上傳文件 / 數(shù)據(jù) / 流 / 多表單數(shù)據(jù)
- 使用請(qǐng)求或者斷點(diǎn)下載來(lái)下載文件
- 使用URL憑據(jù)進(jìn)行身份認(rèn)證
- HTTP響應(yīng)驗(yàn)證
- 包含進(jìn)度的上傳和下載閉包
- cURL命令的輸出
- 動(dòng)態(tài)適配和重試請(qǐng)求
- TLS證書(shū)和Public Key Pinning
- 網(wǎng)絡(luò)可達(dá)性
- 全面的單元和集成測(cè)試覆蓋率
- (3) Alamofire框架如何使用?
- 要求的使用環(huán)境:
iOS 10.0+ / macOS 10.12+ / tvOS 10.0+ / watchOS 3.0+
Xcode 10.2+
Swift 5+
- 安裝方法:
source 'https://github.com/CocoaPods/Specs.git'
platform :ios, '10.0'
use_frameworks!
target '項(xiàng)目名稱' do
pod 'Alamofire', '~> 5.0.0-beta.5'
end
- api使用涵亏,下面將詳細(xì)講述
(二)Alamofire api使用
1. 發(fā)請(qǐng)求
Alamofire.request("http://qq.com/")
2. 響應(yīng)處理
- 直接在請(qǐng)求后面用點(diǎn)語(yǔ)法鏈接響應(yīng)處理:
實(shí)例1:
Alamofire.request("https://httpbin.org/get").responseJSON { response in
print(response.request) // 原始的URL請(qǐng)求
print(response.response) // HTTP URL響應(yīng)
print(response.data) // 服務(wù)器返回的數(shù)據(jù)
print(response.result) // 響應(yīng)序列化結(jié)果宰睡,在這個(gè)閉包里蒲凶,存儲(chǔ)的是JSON數(shù)據(jù)
if let JSON = response.result.value {
print("JSON: \(JSON)")
}
}
在上面的例子中,responseJSON handler直接拼接到請(qǐng)求后面拆内,當(dāng)請(qǐng)求完成后被調(diào)用旋圆。這個(gè)閉包一旦收到響應(yīng)后,就會(huì)處理這個(gè)響應(yīng)麸恍,并不會(huì)因?yàn)榈却?wù)器的響應(yīng)而造成阻塞執(zhí)行灵巧。請(qǐng)求的結(jié)果僅在響應(yīng)閉包的范圍內(nèi)可用。其他任何與服務(wù)器返回的響應(yīng)或者數(shù)據(jù)相關(guān)的操作或南,都必須在這個(gè)閉包內(nèi)執(zhí)行孩等。
2.1 五種不同的響應(yīng)handler
實(shí)例2:
- 所有的響應(yīng)handler都不會(huì)對(duì)響應(yīng)進(jìn)行驗(yàn)證。也就是說(shuō)響應(yīng)狀態(tài)碼在400..<500和500..<600范圍內(nèi)采够,都不會(huì)觸發(fā)錯(cuò)誤肄方。
// 響應(yīng) Handler - 未序列化的響應(yīng)
func response(
queue: DispatchQueue?,
completionHandler: @escaping (DefaultDataResponse) -> Void)
-> Self
// 響應(yīng)數(shù)據(jù) Handler - 序列化成數(shù)據(jù)類型
func responseData(
queue: DispatchQueue?,
completionHandler: @escaping (DataResponse<Data>) -> Void)
-> Self
// 響應(yīng)字符串 Handler - 序列化成字符串類型
func responseString(
queue: DispatchQueue?,
encoding: String.Encoding?,
completionHandler: @escaping (DataResponse<String>) -> Void)
-> Self
// 響應(yīng) JSON Handler - 序列化成Any類型
func responseJSON(
queue: DispatchQueue?,
completionHandler: @escaping (DataResponse<Any>) -> Void)
-> Self
// 響應(yīng) PropertyList (plist) Handler - 序列化成Any類型
func responsePropertyList(
queue: DispatchQueue?,
completionHandler: @escaping (DataResponse<Any>) -> Void))
-> Self
2.1.1 響應(yīng) Handler
- response handler不處理任何響應(yīng)數(shù)據(jù)。它僅僅是從URL session delegate中轉(zhuǎn)發(fā)信息蹬癌。
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)")
}
}
- 一般情況下不建議使用這種沒(méi)有響應(yīng)序列化器的handler权她,而應(yīng)該使用下面有特定序列化器的handler。
2.1.2 響應(yīng)數(shù)據(jù) Handler
-
responseData handler使用
responseDataSerializer
(這個(gè)對(duì)象把服務(wù)器的數(shù)據(jù)序列化成其他類型)來(lái)提取服務(wù)器返回的數(shù)據(jù)逝薪。如果沒(méi)有返回錯(cuò)誤并且有數(shù)據(jù)返回隅要,那么響應(yīng)Result
將會(huì)是.success
,value
是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)")
}
}
2.1.3 響應(yīng)字符串 Handler
-
responseString handler使用
responseStringSerializer
對(duì)象根據(jù)指定的編碼格式把服務(wù)器返回的數(shù)據(jù)轉(zhuǎn)換成String步清。如果沒(méi)有返回錯(cuò)誤并且服務(wù)器的數(shù)據(jù)成功地轉(zhuǎn)換為String,那么響應(yīng)Result
將會(huì)是.success虏肾,value是String類型廓啊。
Alamofire.request("https://httpbin.org/get").responseString { response in
print("Success: \(response.result.isSuccess)")
print("Response String: \(response.result.value)")
}
- 如果沒(méi)有指定編碼格式,將會(huì)使用服務(wù)器的
HTTPURLResponse
指定的格式封豪。如果服務(wù)器無(wú)法確定編碼格式谴轮,那么默認(rèn)使用.isoLatin1
。
2.1.4 響應(yīng) JSON Handler
-
responseJSON handler使用
responseJSONSerializer
根據(jù)指定的JSONSerialization.ReadingOptions
把服務(wù)器返回的數(shù)據(jù)轉(zhuǎn)換成Any類型吹埠。如果沒(méi)有返回錯(cuò)誤并且服務(wù)器的數(shù)據(jù)成功地轉(zhuǎn)換為JSON對(duì)象第步,那么響應(yīng)Result將會(huì)是.success,value是Any類型缘琅。
Alamofire.request("https://httpbin.org/get").responseJSON { response in
debugPrint(response)
if let json = response.result.value {
print("JSON: \(json)")
}
}
- 所有JSON的序列化粘都,都是使用JSONSerialization完成的。
2.1.5 鏈?zhǔn)巾憫?yīng)handler
- 響應(yīng)handler可以鏈接在一起:
Alamofire.request("https://httpbin.org/get")
.responseString { response in
print("Response String: \(response.result.value)")
}
.responseJSON { response in
print("Response JSON: \(response.result.value)")
}
- 在同一個(gè)請(qǐng)求中使用多個(gè)響應(yīng)handler胯杭,要求服務(wù)器的數(shù)據(jù)會(huì)被序列化多次驯杜,每次對(duì)應(yīng)一個(gè)handler。
2.2 響應(yīng)handler隊(duì)列
- 默認(rèn)情況下鸽心,響應(yīng)handler是在主隊(duì)列執(zhí)行的。但是我們也可以自定義隊(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")
}
2.3 響應(yīng)驗(yàn)證
默認(rèn)情況下居暖,Alamofire把所有完成的請(qǐng)求當(dāng)做是成功的請(qǐng)求顽频,無(wú)論響應(yīng)的內(nèi)容是什么。如果響應(yīng)有一個(gè)不能被接受的狀態(tài)碼或者M(jìn)IME類型太闺,在響應(yīng)handler之前調(diào)用validate將會(huì)產(chǎn)生錯(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)證在200…299范圍內(nèi)的狀態(tài)碼;如果請(qǐng)求頭中有指定Accept省骂,那么也會(huì)驗(yàn)證響應(yīng)頭的與請(qǐng)求頭Accept一樣的Content-Type蟀淮。
Alamofire.request("https://httpbin.org/get").validate().responseJSON { response in
switch response.result {
case .success:
print("Validation Successful")
case .failure(let error):
print(error)
}
}
2.4 響應(yīng)緩存
- 響應(yīng)緩存是使用系統(tǒng)的框架URLCache來(lái)處理的。它提供了內(nèi)存和磁盤(pán)上的緩存钞澳,并允許我們控制內(nèi)存和磁盤(pán)的大小怠惶。
- 默認(rèn)情況下,Alamofire利用共享的URLCache轧粟。
2.5 HTTP協(xié)議相關(guān)
2.5.1 HTTP方法
-
HTTPMethod
列舉了下面的這些方法:
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時(shí)策治,可以傳入方法參數(shù):
Alamofire.request("https://httpbin.org/get") // 默認(rèn)是get請(qǐng)求
Alamofire.request("https://httpbin.org/post", method: .post)
Alamofire.request("https://httpbin.org/put", method: .put)
Alamofire.request("https://httpbin.org/delete", method: .delete)
2.5.2 HTTP請(qǐng)求頭
- 可以直接在請(qǐng)求方法添加自定義HTTP請(qǐng)求頭,這有利于我們?cè)谡?qǐng)求中添加請(qǐng)求頭兰吟。
let headers: HTTPHeaders = [
"Authorization": "Basic QWxhZGRpbjpvcGVuIHNlc2FtZQ==",
"Accept": "application/json"
]
Alamofire.request("https://httpbin.org/headers", headers: headers).responseJSON { response in
debugPrint(response)
}
- 對(duì)于那些不變的請(qǐng)求頭通惫,建議在
URLSessionConfiguration
設(shè)置,這樣就可以自動(dòng)被用于任何URLSession
創(chuàng)建的URLSessionTask
混蔼。 - 默認(rèn)的Alamofire SessionManager為每一個(gè)請(qǐng)求提供了一個(gè)默認(rèn)的請(qǐng)求頭集合履腋,包括:
Accept-Encoding
,默認(rèn)是gzip;q=1.0, compress;q=0.5惭嚣。
Accept-Language
遵湖,默認(rèn)是系統(tǒng)的前6個(gè)偏好語(yǔ)言,格式類似于en;q=1.0料按。
User-Agent
奄侠,包含當(dāng)前應(yīng)用程序的版本信息。例如iOS Example/1.0 (com.alamofire.iOS-Example; build:1; iOS 10.0.0) Alamofire/4.0.0载矿。
- 如果要自定義這些請(qǐng)求頭集合垄潮,我們必須創(chuàng)建一個(gè)自定義的
URLSessionConfiguration
,defaultHTTPHeaders
屬性將會(huì)被更新闷盔,并且自定義的會(huì)話配置也會(huì)應(yīng)用到新的SessionManager
實(shí)例弯洗。
2.5.3 認(rèn)證
- 認(rèn)證是使用系統(tǒng)框架
URLCredential
和URLAuthenticationChallenge
實(shí)現(xiàn)的。 - 支持的認(rèn)證方案:
HTTP Basic
HTTP Digest
Kerberos
NTLM
2.5.3.1 HTTP Basic認(rèn)證
- 在合適的時(shí)候逢勾,在一個(gè)請(qǐng)求的authenticate方法會(huì)自動(dòng)提供一個(gè)URLCredential給URLAuthenticationChallenge:
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)
}
- 根據(jù)服務(wù)器實(shí)現(xiàn)牡整,Authorization header也可能是適合的:
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)
}
2.5.3.2 使用URLCredential認(rèn)證
- 使用URLCredential來(lái)做認(rèn)證,如果服務(wù)器發(fā)出一個(gè)challenge溺拱,底層的URLSession實(shí)際上最終會(huì)發(fā)兩次請(qǐng)求逃贝。第一次請(qǐng)求不會(huì)包含credential谣辞,并且可能會(huì)觸發(fā)服務(wù)器發(fā)出一個(gè)challenge。這個(gè)challenge會(huì)被Alamofire接收沐扳,credential會(huì)被添加泥从,然后URLSessin會(huì)重試請(qǐng)求。
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)
}
2.6 編碼相關(guān)
2.6.1 參數(shù)編碼
- Alamofire支持三種參數(shù)編碼:
URL
沪摄、JSON
和PropertyList
躯嫉。還支持遵循了ParameterEncoding
協(xié)議的自定義編碼。
2.6.1.1 URL編碼
-
URLEncoding
類型創(chuàng)建了一個(gè)URL編碼的查詢字符串來(lái)設(shè)置或者添加到一個(gè)現(xiàn)有的URL查詢字符串杨拐,或者設(shè)置URL請(qǐng)求的請(qǐng)求體祈餐。查詢字符串是否被設(shè)置或者添加到現(xiàn)有的URL查詢字符串,或者被作為HTTP請(qǐng)求體哄陶,決定于編碼的Destination
帆阳。編碼的Destination有三個(gè)case:.methodDependent
:為GET、HEAD和DELETE請(qǐng)求使用編碼查詢字符串來(lái)設(shè)置或者添加到現(xiàn)有查詢字符串奕筐,并且使用其他HTTP方法來(lái)設(shè)置請(qǐng)求體舱痘。.queryString
:設(shè)置或者添加編碼查詢字符串到現(xiàn)有查詢字符串.httpBody
:把編碼查詢字符串作為URL請(qǐng)求的請(qǐng)求體
一個(gè)編碼請(qǐng)求的請(qǐng)求體的
Content-Type
字段被設(shè)置為application/x-www-form-urlencoded; charset=utf-8
。因?yàn)闆](méi)有公開(kāi)的標(biāo)準(zhǔn)說(shuō)明如何編碼集合類型离赫,所以按照慣例在key
后面添加[]
來(lái)表示數(shù)組的值(foo[]=1&foo[]=2
)芭逝,在key外面包一個(gè)中括號(hào)來(lái)表示字典的值(foo[bar]=baz
)。使用URL編碼參數(shù)的GET請(qǐng)求
let parameters: Parameters = ["foo": "bar"]
// 下面這三種寫(xiě)法是等價(jià)的
Alamofire.request("https://httpbin.org/get", parameters: parameters) // encoding 默認(rèn)是`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
- 使用URL編碼參數(shù)的POST請(qǐng)求
let parameters: Parameters = [
"foo": "bar",
"baz": ["a", 1],
"qux": [
"x": 1,
"y": 2,
"z": 3
]
]
// 下面這三種寫(xiě)法是等價(jià)的
Alamofire.request("https://httpbin.org/post", method: .post, parameters: parameters)
Alamofire.request("https://httpbin.org/post", method: .post, parameters: parameters, encoding: URLEncoding.default)
Alamofire.request("https://httpbin.org/post", method: .post, parameters: parameters, encoding: URLEncoding.httpBody)
// HTTP body: foo=bar&baz[]=a&baz[]=1&qux[x]=1&qux[y]=2&qux[z]=3
設(shè)置Bool類型參數(shù)的編碼
- URLEncoding.BoolEncoding提供了兩種編碼方式:
.numeric
:把true編碼為1渊胸,false編碼為0
.literal
:把true編碼為true旬盯,false編碼為false
- 默認(rèn)情況下:Alamofire使用.numeric。
- 可以使用下面的初始化函數(shù)來(lái)創(chuàng)建URLEncoding翎猛,指定Bool編碼的類型:
let encoding = URLEncoding(boolEncoding: .literal)
設(shè)置Array類型參數(shù)編碼
- URLEncoding.ArrayEncoding提供了兩種編碼方式:
.brackets
: 在每個(gè)元素值的key后面加上一個(gè)[]胖翰,如foo=[1,2]編碼成foo[]=1&foo[]=2
.noBrackets
:不添加[]盔腔,例如foo=[1,2]編碼成``foo=1&foo=2`
- 默認(rèn)情況下江耀,Alamofire使用.brackets。
- 可以使用下面的初始化函數(shù)來(lái)創(chuàng)建URLEncoding巨税,指定Array編碼的類型:
let encoding = URLEncoding(arrayEncoding: .noBrackets)
2.6.1.2 JSON編碼
-
JSONEncoding
類型創(chuàng)建了一個(gè)JOSN對(duì)象疫稿,并作為請(qǐng)求體培他。編碼請(qǐng)求的請(qǐng)求頭的Content-Type請(qǐng)求字段被設(shè)置為application/json
。 - 使用JSON編碼參數(shù)的POST請(qǐng)求
let parameters: Parameters = [
"foo": [1,2,3],
"bar": [
"baz": "qux"
]
]
// 下面這兩種寫(xiě)法是等價(jià)的
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"}}
2.6.1.3 屬性列表編碼
-
PropertyListEncoding
根據(jù)關(guān)聯(lián)格式和寫(xiě)選項(xiàng)值遗座,使用PropertyListSerialization
來(lái)創(chuàng)建一個(gè)屬性列表對(duì)象舀凛,并作為請(qǐng)求體。編碼請(qǐng)求的請(qǐng)求頭的Content-Type
請(qǐng)求字段被設(shè)置為application/x-plist
途蒋。
2.6.1.4 自定義編碼
- 如果提供的
ParameterEncoding
類型不能滿足我們的要求猛遍,可以創(chuàng)建自定義編碼。下面演示如何快速自定義一個(gè)JSONStringArrayEncoding
類型把JSON字符串?dāng)?shù)組編碼到請(qǐng)求中。
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
}
}
2.6.1.5 手動(dòng)URL請(qǐng)求參數(shù)編碼
-
ParameterEncodingAPI
可以在創(chuàng)建網(wǎng)絡(luò)請(qǐng)求外面使用懊烤。
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)
2.7 下載
2.7.1 將數(shù)據(jù)下載到文件
Alamofire可以把服務(wù)器的數(shù)據(jù)下載到內(nèi)存(in-memory)或者硬盤(pán)(on-disk)中梯醒。所有Alamofire.requestAPI下載的數(shù)據(jù)都是存儲(chǔ)在內(nèi)存中。這比較適合小文件奸晴,更高效冤馏;但是不適合大文件日麸,因?yàn)榇笪募?huì)把內(nèi)存耗盡寄啼。我們要使用Alamofire.downloadAPI把服務(wù)器的數(shù)據(jù)下載到硬盤(pán)中。
下面這個(gè)方法只適用于macOS代箭。因?yàn)樵谄渌脚_(tái)不允許在應(yīng)用沙盒外訪問(wèn)文件系統(tǒng)墩划。下面會(huì)講到如何在其他平臺(tái)下載文件。
Alamofire.download("https://httpbin.org/image/png").responseData { response in
if let data = response.result.value {
let image = UIImage(data: data)
}
}
2.7.2 下載文件存儲(chǔ)位置
- 我們可以提供一個(gè)
DownloadFileDestination
閉包把臨時(shí)文件夾的文件移動(dòng)到一個(gè)目標(biāo)文件夾嗡综。在臨時(shí)文件真正移動(dòng)到destinationURL
之前乙帮,閉包內(nèi)部指定的DownloadOptions
將會(huì)被執(zhí)行。目前支持的DownloadOptions
有下面兩個(gè):-
.createIntermediateDirectories
:如果指定了目標(biāo)URL极景,將會(huì)創(chuàng)建中間目錄察净。 -
.removePreviousFile
:如果指定了目標(biāo)URL,將會(huì)移除之前的文件
-
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)
}
}
- 也可以直接使用建議的下載目標(biāo)API:
let destination = DownloadRequest.suggestedDownloadDestination(directory: .documentDirectory)
Alamofire.download("https://httpbin.org/image/png", to: destination)
2.7.3 下載進(jìn)度
- 所有的
DownloadRequest
都可以使用downloadProgressAPI
來(lái)反饋下載進(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)
}
}
-
downloadProgressAPI
還可以接受一個(gè)queue參數(shù)來(lái)指定下載進(jìn)度閉包在哪個(gè)DispatchQueue
中執(zhí)行氢卡。
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)
}
}
2.7.4 恢復(fù)下載
- 如果一個(gè)
DownloadRequest
被取消或中斷,底層的URL會(huì)話會(huì)生成一個(gè)恢復(fù)數(shù)據(jù)晨缴∫肭兀恢復(fù)數(shù)據(jù)可以被重新利用并在中斷的位置繼續(xù)下載』魍耄恢復(fù)數(shù)據(jù)可以通過(guò)下載響應(yīng)訪問(wèn)筑悴,然后在重新開(kāi)始請(qǐng)求的時(shí)候被利用。 - 在iOS 10 - 10.2, macOS 10.12 - 10.12.2, tvOS 10 - 10.1, watchOS 3 - 3.1.1中稍途,resumeData會(huì)被后臺(tái)URL會(huì)話配置破壞阁吝。因?yàn)樵趓esumeData的生成邏輯有一個(gè)底層的bug,不能恢復(fù)下載械拍。具體情況可以到Stack Overflow看看突勇。
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
}
}
}
}
2.8 上傳
2.8.1 上傳數(shù)據(jù)到服務(wù)器
- 使用JOSN或者URL編碼參數(shù)上傳一些小數(shù)據(jù)到服務(wù)器,使用Alamofire.request API就已經(jīng)足夠了殊者。如果需要發(fā)送很大的數(shù)據(jù)与境,需要使用Alamofire.upload API。當(dāng)我們需要在后臺(tái)上傳數(shù)據(jù)時(shí)猖吴,也可以使用Alamofire.upload摔刁。
2.8.2 上傳數(shù)據(jù)
let imageData = UIPNGRepresentation(image)!
Alamofire.upload(imageData, to: "https://httpbin.org/post").responseJSON { response in
debugPrint(response)
}
2.8.3 上傳文件
let fileURL = Bundle.main.url(forResource: "video", withExtension: "mov")
Alamofire.upload(fileURL, to: "https://httpbin.org/post").responseJSON { response in
debugPrint(response)
}
2.8.4 上傳多部分表單數(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)
}
}
)
2.8.5 上傳進(jìn)度
- 所有的UploadRequest都可以使用uploadProgress和downloadProgress APIs來(lái)反饋上傳和下載進(jìn)度。
let fileURL = Bundle.main.url(forResource: "video", withExtension: "mov")
Alamofire.upload(fileURL, to: "https://httpbin.org/post")
.uploadProgress { progress in // 默認(rèn)在主線程中執(zhí)行
print("Upload Progress: \(progress.fractionCompleted)")
}
.downloadProgress { progress in // 默認(rèn)在主線程中執(zhí)行
print("Download Progress: \(progress.fractionCompleted)")
}
.responseJSON { response in
debugPrint(response)
}
2.9 統(tǒng)計(jì)指標(biāo)
2.9.1 時(shí)間表
- Alamofire在一個(gè)請(qǐng)求周期內(nèi)收集時(shí)間海蔽,并創(chuàng)建一個(gè)Tineline對(duì)象共屈,它是響應(yīng)類型的一個(gè)屬性绑谣。
Alamofire.request("https://httpbin.org/get").responseJSON { response in
print(response.timeline)
}
上面的Timeline信息包括:
Latency: 0.428 seconds (延遲)
Request Duration: 0.428 seconds (請(qǐng)求時(shí)間)
Serialization Duration: 0.001 seconds (序列化時(shí)間)
Total Duration: 0.429 seconds (總時(shí)間)
2.9.2 URL會(huì)話任務(wù)指標(biāo)
- 在iOS和tvOS 10和macOS 10.12中,蘋(píng)果發(fā)布了新的URLSessionTaskMetrics APIs拗引。這個(gè)任務(wù)指標(biāo)封裝了關(guān)于請(qǐng)求和響應(yīng)執(zhí)行的神奇統(tǒng)計(jì)信息借宵。這個(gè)API和Timeline非常相似,但是提供了很多Alamofire沒(méi)有提供的統(tǒng)計(jì)信息矾削。這些指標(biāo)可以通過(guò)任何響應(yīng)去訪問(wèn)壤玫。
Alamofire.request("https://httpbin.org/get").responseJSON { response in
print(response.metrics)
}
- 這些API只能在iOS和tvOS 10和macOS 10.12中使用。所以哼凯,根據(jù)部署目標(biāo)欲间,可能需要加入版本判斷:
Alamofire.request("https://httpbin.org/get").responseJSON { response in
if #available(iOS 10.0. *) {
print(response.metrics)
}
}
2.9.3 cURL命令輸出
- 調(diào)試平臺(tái)問(wèn)題很讓人厭煩。慶幸的是断部,Alamofire的Request對(duì)象遵循了
CustomStringConvertible
和CustomDebugStringConvertible
協(xié)議來(lái)提供一些非常有用的調(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)
輸出結(jié)果:
$ 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不同場(chǎng)景api使用
1. Session Manager
- 我們可以自己創(chuàng)建后臺(tái)會(huì)話和短暫會(huì)話的session manager,還可以自定義默認(rèn)的會(huì)話配置來(lái)創(chuàng)建新的session manager蝴光,例如修改默認(rèn)的header httpAdditionalHeaders和timeoutIntervalForRequest她渴。
Alamofire.request("https://httpbin.org/get")
let sessionManager = Alamofire.SessionManager.default
sessionManager.request("https://httpbin.org/get")
- 用默認(rèn)的會(huì)話配置創(chuàng)建一個(gè)Session Manager
let configuration = URLSessionConfiguration.default
let sessionManager = Alamofire.SessionManager(configuration: configuration)
- 用后臺(tái)會(huì)話配置創(chuàng)建一個(gè)Session Manager
let configuration = URLSessionConfiguration.background(withIdentifier: "com.example.app.background")
let sessionManager = Alamofire.SessionManager(configuration: configuration)
- 用默短暫會(huì)話配置創(chuàng)建一個(gè)Session Manager
let configuration = URLSessionConfiguration.ephemeral
let sessionManager = Alamofire.SessionManager(configuration: configuration)
- 修改會(huì)話配置
//不推薦在Authorization或者Content-Type header使用。而應(yīng)該使用Alamofire.requestAPI蔑祟、URLRequestConvertible和ParameterEncoding的headers參數(shù)趁耗。
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)
1.1 會(huì)話代理 SessionDelegate
默認(rèn)情況下,一個(gè)
SessionManager
實(shí)例創(chuàng)建一個(gè)SessionDelegate
對(duì)象來(lái)處理底層URLSession
生成的不同類型的代理回調(diào)做瞪。每個(gè)代理方法的實(shí)現(xiàn)處理常見(jiàn)的情況对粪。然后,高級(jí)用戶可能由于各種原因需要重寫(xiě)默認(rèn)功能装蓬。-
有兩種方式實(shí)現(xiàn)
SessionDelegate
:方式一:自定義
SessionDelegate
的方法是通過(guò)重寫(xiě)閉包著拭。我們可以在每個(gè)閉包重寫(xiě)SessionDelegate
API對(duì)應(yīng)的實(shí)現(xiàn)。方式二:重寫(xiě)
SessionDelegate
的實(shí)現(xiàn)的方法是把它子類化牍帚。通過(guò)子類化儡遮,我們可以完全自定義他的行為,或者為這個(gè)API創(chuàng)建一個(gè)代理并且仍然使用它的默認(rèn)實(shí)現(xiàn)暗赶。通過(guò)創(chuàng)建代理鄙币,我們可以跟蹤日志事件、發(fā)通知蹂随、提供前后實(shí)現(xiàn)十嘿。
實(shí)現(xiàn)
SessionDelegate
代碼實(shí)例:
- 重寫(xiě)閉包的示例:
/// 重寫(xiě)URLSessionDelegate的`urlSession(_:didReceive:completionHandler:)`方法
open var sessionDidReceiveChallenge: ((URLSession, URLAuthenticationChallenge) -> (URLSession.AuthChallengeDisposition, URLCredential?))?
/// 重寫(xiě)URLSessionDelegate的`urlSessionDidFinishEvents(forBackgroundURLSession:)`方法
open var sessionDidFinishEventsForBackgroundURLSession: ((URLSession) -> Void)?
/// 重寫(xiě)URLSessionTaskDelegate的`urlSession(_:task:willPerformHTTPRedirection:newRequest:completionHandler:)`方法
open var taskWillPerformHTTPRedirection: ((URLSession, URLSessionTask, HTTPURLResponse, URLRequest) -> URLRequest?)?
/// 重寫(xiě)URLSessionDataDelegate的`urlSession(_:dataTask:willCacheResponse:completionHandler:)`方法
open var dataTaskWillCacheResponse: ((URLSession, URLSessionDataTask, CachedURLResponse) -> CachedURLResponse?)?
- 使用taskWillPerformHTTPRedirection來(lái)避免回調(diào)到任何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
}
- 下面這個(gè)例子演示了如何子類化SessionDelegate岳锁,并且有回調(diào)的時(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
)
}
}
- 總的來(lái)說(shuō)绩衷,無(wú)論是默認(rèn)實(shí)現(xiàn)還是重寫(xiě)閉包,都應(yīng)該提供必要的功能。子類化應(yīng)該作為最后的選擇咳燕。
2. 請(qǐng)求 Request
request
勿决、download
、upload
和stream
方法的結(jié)果是DataRequest
招盲、DownloadRequest
低缩、UploadRequest
和StreamRequest
,并且所有請(qǐng)求都繼承自Request
曹货。所有的Request
并不是直接創(chuàng)建的咆繁,而是由session manager
創(chuàng)建的。每個(gè)子類都有特定的方法控乾,例如
authenticate
么介、validate
、responseJSON
和uploadProgress
蜕衡,都返回一個(gè)實(shí)例,以便方法鏈接(也就是用點(diǎn)語(yǔ)法連續(xù)調(diào)用方法)设拟。請(qǐng)求可以被暫停慨仿、恢復(fù)和取消:
suspend()
:暫停底層的任務(wù)和調(diào)度隊(duì)列
resume()
:恢復(fù)底層的任務(wù)和調(diào)度隊(duì)列。如果manager的startRequestsImmediately不是true纳胧,那么必須調(diào)用resume()來(lái)開(kāi)始請(qǐng)求镰吆。
cancel()
:取消底層的任務(wù),并產(chǎn)生一個(gè)error跑慕,error被傳入任何已經(jīng)注冊(cè)的響應(yīng)handlers万皿。
- 隨著應(yīng)用的不多增大,當(dāng)我們建立網(wǎng)絡(luò)棧的時(shí)候要使用通用的模式核行。在通用模式的設(shè)計(jì)中牢硅,一個(gè)很重要的部分就是如何傳送請(qǐng)求。遵循
Router
設(shè)計(jì)模式的URLConvertible
和URLRequestConvertible
協(xié)議可以幫助我們
2.1 DataRequest
2.2 DownloadRequest
2.3 UploadRequest
2.4 StreamRequest
2.5 URLConvertible
- 遵循了
URLConvertible
協(xié)議的類型可以被用來(lái)構(gòu)建URL芝雪,然后用來(lái)創(chuàng)建URL請(qǐng)求减余。String、URL和URLComponent默認(rèn)是遵循URLConvertible協(xié)議的惩系。它們都可以作為url參數(shù)傳入request
位岔、upload
和download
方法. - 以一種有意義的方式和web應(yīng)用程序交互的應(yīng)用,都鼓勵(lì)使用自定義的遵循URLConvertible協(xié)議的類型將特定領(lǐng)域模型映射到服務(wù)器資源堡牡,因?yàn)檫@樣比較方便抒抬。
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)
- 類型安全傳送
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
2.6 URLRequestConvertible
- 遵循
URLRequestConvertible
協(xié)議的類型可以被用來(lái)構(gòu)建URL請(qǐng)求。URLRequest
默認(rèn)遵循了URLRequestConvertible
晤柄,允許被直接傳入request
擦剑、upload
和download
(推薦用這種方法為單個(gè)請(qǐng)求自定義請(qǐng)求頭) - 以一種有意義的方式和web應(yīng)用程序交互的應(yīng)用,都鼓勵(lì)使用自定義的遵循
URLRequestConvertible
協(xié)議的類型來(lái)保證請(qǐng)求端點(diǎn)的一致性。這種方法可以用來(lái)抽象服務(wù)器端的不一致性抓于,并提供類型安全傳送做粤,以及管理身份驗(yàn)證憑據(jù)和其他狀態(tài)。
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)
- 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
2.7 CRUD和授權(quán)
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
2.8 適配和重試請(qǐng)求
現(xiàn)在的大多數(shù)Web服務(wù)捉撮,都需要身份認(rèn)證∨缕罚現(xiàn)在比較常見(jiàn)的是
OAuth
。通常是需要一個(gè)access token
來(lái)授權(quán)應(yīng)用或者用戶巾遭,然后才可以使用各種支持的Web服務(wù)肉康。創(chuàng)建這些access token
是比較麻煩的,當(dāng)access token
過(guò)期之后就比較麻煩了灼舍,我們需要重新創(chuàng)建一個(gè)新的吼和。有許多線程安全問(wèn)題要考慮。RequestAdapter
和RequestRetrier
協(xié)議可以讓我們更容易地為特定的Web服務(wù)創(chuàng)建一個(gè)線程安全的認(rèn)證系統(tǒng)骑素。
2.8.1 RequestAdapter
- RequestAdapter協(xié)議允許每一個(gè)SessionManager的Request在創(chuàng)建之前被檢查和適配炫乓。一個(gè)非常特別的使用適配器方法是,在一個(gè)特定的認(rèn)證類型献丑,把Authorization header拼接到請(qǐng)求末捣。
- 創(chuàng)建一個(gè)AccessTokenAdapter類繼承RequestAdapter
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
}
}
- 創(chuàng)建SessionManager,并將AccessTokenAdapter賦值給sessionManager
let sessionManager = SessionManager()
sessionManager.adapter = AccessTokenAdapter(accessToken: "1234")
sessionManager.request("https://httpbin.org/get")
2.8.2 RequestRetrier
-
RequestRetrier
協(xié)議允許一個(gè)在執(zhí)行過(guò)程中遇到error的請(qǐng)求被重試创橄。當(dāng)一起使用RequestAdapter
和RequestRetrier
協(xié)議時(shí)箩做,我們可以為OAuth1
、OAuth2
妥畏、Basic Auth
(每次請(qǐng)求API都要提供用戶名和密碼)甚至是exponential backoff
重試策略創(chuàng)建資格恢復(fù)系統(tǒng)邦邦。下面的例子演示了如何實(shí)現(xiàn)一個(gè)OAuth2 access token
的恢復(fù)流程。
實(shí)例282:
注意:下面代碼不是一個(gè)全面的OAuth2解決方案醉蚁。這僅僅是演示如何把RequestAdapter和RequestRetrier協(xié)議結(jié)合起來(lái)創(chuàng)建一個(gè)線程安全的恢復(fù)系統(tǒng)燃辖。
重申: 不要把這個(gè)例子復(fù)制到實(shí)際的開(kāi)發(fā)應(yīng)用中,這僅僅是一個(gè)例子馍管。每個(gè)認(rèn)證系統(tǒng)必須為每個(gè)特定的平臺(tái)和認(rèn)證類型重新定制郭赐。
- 創(chuàng)建一個(gè)類OAuth2Handler,同時(shí)繼承:RequestAdapter, RequestRetrier 兩個(gè)協(xié)議
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
}
}
}
- 創(chuàng)建sessionManager對(duì)象确沸,并將sessionManager.adapter捌锭,sessionManager.retrier 都同時(shí)指向OAuth2Handler對(duì)象。
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)
}
一旦
OAuth2Handler
為SessionManager
被應(yīng)用與adapter
和retrier
罗捎,他將會(huì)通過(guò)自動(dòng)恢復(fù)access token
來(lái)處理一個(gè)非法的access token error
观谦,并且根據(jù)失敗的順序來(lái)重試所有失敗的請(qǐng)求。(如果需要讓他們按照創(chuàng)建的時(shí)間順序來(lái)執(zhí)行桨菜,可以使用他們的task identifier
來(lái)排序)上面這個(gè)例子僅僅檢查了
401
響應(yīng)碼豁状,不是演示如何檢查一個(gè)非法的access token error
捉偏。在實(shí)際開(kāi)發(fā)應(yīng)用中,我們想要檢查realm
和www-authenticate header
響應(yīng)泻红,雖然這取決于OAuth2
的實(shí)現(xiàn)夭禽。還有一個(gè)要重點(diǎn)注意的是,這個(gè)認(rèn)證系統(tǒng)可以在多個(gè)
session manager
之間共享谊路。例如讹躯,可以在同一個(gè)Web服務(wù)集合使用default
和ephemeral
會(huì)話配置。上面這個(gè)例子可以在多個(gè)session manager
間共享一個(gè)oauthHandler
實(shí)例缠劝,來(lái)管理一個(gè)恢復(fù)流程潮梯。
2.9
3. 序列化
3.1 自定義響應(yīng)序列化
- Alamofire為
data
、strings
惨恭、JSON
和Property List
提供了內(nèi)置的響應(yīng)序列化:
Alamofire.request(...).responseData { (resp: DataResponse<Data>) in ... }
Alamofire.request(...).responseString { (resp: DataResponse<String>) in ... }
Alamofire.request(...).responseJSON { (resp: DataResponse<Any>) in ... }
Alamofire.request(...).responsePropertyList { resp: DataResponse<Any>) in ... }
- 這些響應(yīng)包裝了反序列化的值(
Data
,String
,Any
)或者error (network
,validation
errors
)秉馏,以及元數(shù)據(jù) (URL Request, HTTP headers, status code, metrics, ...)。 - 我們可以有多個(gè)方法來(lái)自定義所有響應(yīng)元素:
- 響應(yīng)映射
- 處理錯(cuò)誤
- 創(chuàng)建一個(gè)自定義的響應(yīng)序列化器
- 泛型響應(yīng)對(duì)象序列化
3.1.1 響應(yīng)映射
- 響應(yīng)映射是自定義響應(yīng)最簡(jiǎn)單的方式脱羡。它轉(zhuǎn)換響應(yīng)的值萝究,同時(shí)保留最終錯(cuò)誤和元數(shù)據(jù)。例如轻黑,我們可以把一個(gè)json響應(yīng)DataResponse<Any>轉(zhuǎn)換為一個(gè)保存應(yīng)用模型的的響應(yīng)糊肤,例如DataResponse<User>。使用DataResponse.map來(lái)進(jìn)行響應(yīng)映射:
Alamofire.request("https://example.com/users/mattt").responseJSON { (response: DataResponse<Any>) in
let userResponse = response.map { json in
// We assume an existing User(json: Any) initializer
return User(json: json)
}
// Process userResponse, of type DataResponse<User>:
if let user = userResponse.value {
print("User: { username: \(user.username), name: \(user.name) }")
}
}
- 當(dāng)轉(zhuǎn)換可能會(huì)拋出錯(cuò)誤時(shí)氓鄙,使用flatMap方法:
Alamofire.request("https://example.com/users/mattt").responseJSON { response in
let userResponse = response.flatMap { json in
try User(json: json)
}
}
- 響應(yīng)映射非常適合自定義completion handler:
@discardableResult
func loadUser(completionHandler: @escaping (DataResponse<User>) -> Void) -> Alamofire.DataRequest {
return Alamofire.request("https://example.com/users/mattt").responseJSON { response in
let userResponse = response.flatMap { json in
try User(json: json)
}
completionHandler(userResponse)
}
}
loadUser { response in
if let user = userResponse.value {
print("User: { username: \(user.username), name: \(user.name) }")
}
}
- 上面代碼中l(wèi)oadUser方法被@discardableResult標(biāo)記,意思是調(diào)用loadUser方法可以不接收它的返回值业舍;也可以用_來(lái)忽略返回值抖拦。
- 當(dāng) map/flatMap 閉包會(huì)產(chǎn)生比較大的數(shù)據(jù)量時(shí),要保證這個(gè)閉包在子線程中執(zhí)行:
@discardableResult
func loadUser(completionHandler: @escaping (DataResponse<User>) -> Void) -> Alamofire.DataRequest {
let utilityQueue = DispatchQueue.global(qos: .utility)
return Alamofire.request("https://example.com/users/mattt").responseJSON(queue: utilityQueue) { response in
let userResponse = response.flatMap { json in
try User(json: json)
}
DispatchQueue.main.async {
completionHandler(userResponse)
}
}
}
- map和flatMap也可以用于下載響應(yīng)舷暮。
3.1.2 處理錯(cuò)誤
- 在實(shí)現(xiàn)自定義響應(yīng)序列化器或者對(duì)象序列化方法前态罪,思考如何處理所有可能出現(xiàn)的錯(cuò)誤是非常重要的。有兩個(gè)方法:1)傳遞未修改的錯(cuò)誤下面,在響應(yīng)時(shí)間處理复颈;2)把所有的錯(cuò)誤封裝在一個(gè)Error類型中。
- 例如沥割,下面是等會(huì)要用用到的后端錯(cuò)誤:
enum BackendError: Error {
case network(error: Error) // 捕獲任何從URLSession API產(chǎn)生的錯(cuò)誤
case dataSerialization(error: Error)
case jsonSerialization(error: Error)
case xmlSerialization(error: Error)
case objectSerialization(reason: String)
}
3.1.3 創(chuàng)建一個(gè)自定義的響應(yīng)序列化器
- Alamofire為
strings
耗啦、JSON
和Property List
提供了內(nèi)置的響應(yīng)序列化,但是我們可以通過(guò)擴(kuò)展Alamofire.DataRequest
或者Alamofire.DownloadRequest
來(lái)添加其他序列化机杜。 - 例如帜讲,下面這個(gè)例子是一個(gè)使用Ono (一個(gè)實(shí)用的處理iOS和macOS平臺(tái)的XML和HTML的方式)的響應(yīng)handler的實(shí)現(xiàn):
extension DataRequest {
static func xmlResponseSerializer() -> DataResponseSerializer<ONOXMLDocument> {
return DataResponseSerializer { request, response, data, error in
// 把任何底層的URLSession error傳遞給 .network case
guard error == nil else { return .failure(BackendError.network(error: error!)) }
// 使用Alamofire已有的數(shù)據(jù)序列化器來(lái)提取數(shù)據(jù),error為nil椒拗,因?yàn)樯弦恍写a已經(jīng)把不是nil的error過(guò)濾了
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
)
}
}
3.1.4 泛型響應(yīng)對(duì)象序列化
泛型可以用來(lái)提供自動(dòng)的似将、類型安全的響應(yīng)對(duì)象序列化获黔。
代碼1
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)
}
}
- 代碼2
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
}
}
- 代碼3
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) }")
}
}
- 代碼4
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
}
}
- 代碼5
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)
}
}
- 代碼6
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
}
}
- 代碼7
Alamofire.request("https://example.com/users").responseCollection { (response: DataResponse<[User]>) in
debugPrint(response)
if let users = response.result.value {
users.forEach { print("- \($0)") }
}
}
4. 安全
- 對(duì)于安全敏感的數(shù)據(jù)來(lái)說(shuō),在與服務(wù)器和web服務(wù)交互時(shí)使用安全的HTTPS連接是非常重要的一步在验。默認(rèn)情況下玷氏,Alamofire會(huì)使用蘋(píng)果安全框架內(nèi)置的驗(yàn)證方法來(lái)評(píng)估服務(wù)器提供的證書(shū)鏈。雖然保證了證書(shū)鏈?zhǔn)怯行У囊干啵遣荒芊乐?code>man-in-the-middle (
MITM
)攻擊或者其他潛在的漏洞盏触。為了減少MITM
攻擊,處理用戶的敏感數(shù)據(jù)或財(cái)務(wù)信息的應(yīng)用侦厚,應(yīng)該使用ServerTrustPolicy
提供的certificate
或者public key pinning
耻陕。
4. 1 ServerTrustPolicy
- 在通過(guò)HTTPS安全連接連接到服務(wù)器時(shí),ServerTrustPolicy枚舉通常會(huì)評(píng)估URLAuthenticationChallenge提供的server trust刨沦。
let serverTrustPolicy = ServerTrustPolicy.pinCertificates(
certificates: ServerTrustPolicy.certificates(),
validateCertificateChain: true,
validateHost: true
)
- 在驗(yàn)證的過(guò)程中诗宣,有多種方法可以讓我們完全控制server trust的評(píng)估:
屬性 | 作用 |
---|---|
performDefaultEvaluation | 使用默認(rèn)的server trust評(píng)估,允許我們控制是否驗(yàn)證challenge提供的host想诅。 |
pinCertificates | 使用pinned certificates來(lái)驗(yàn)證server trust召庞。如果pinned certificates匹配其中一個(gè)服務(wù)器證書(shū),那么認(rèn)為server trust是有效的来破。 |
pinPublicKeys | 使用pinned public keys來(lái)驗(yàn)證server trust篮灼。如果pinned public keys匹配其中一個(gè)服務(wù)器證書(shū)公鑰,那么認(rèn)為server trust是有效的徘禁。 |
disableEvaluation | 禁用所有評(píng)估诅诱,總是認(rèn)為server trust是有效的。 |
customEvaluation | 使用相關(guān)的閉包來(lái)評(píng)估server trust的有效性送朱,我們可以完全控制整個(gè)驗(yàn)證過(guò)程娘荡。但是要謹(jǐn)慎使用。 |
4. 2 ServerTrustPolicyManager(服務(wù)器信任策略管理者 )
- ServerTrustPolicyManager負(fù)責(zé)存儲(chǔ)一個(gè)內(nèi)部的服務(wù)器信任策略到特定主機(jī)的映射驶沼。這樣Alamofire就可以評(píng)估每個(gè)主機(jī)不同服務(wù)器信任策略炮沐。
let serverTrustPolicies: [String: ServerTrustPolicy] = [
"test.example.com": .pinCertificates(
certificates: ServerTrustPolicy.certificates(),
validateCertificateChain: true,
validateHost: true
),
"insecure.expired-apis.com": .disableEvaluation
]
let sessionManager = SessionManager(
serverTrustPolicyManager: ServerTrustPolicyManager(policies: serverTrustPolicies)
)
- 要確保有一個(gè)強(qiáng)引用引用著SessionManager實(shí)例,否則當(dāng)sessionManager被銷毀時(shí)回怜,請(qǐng)求將會(huì)取消大年。
- 這些服務(wù)器信任策略將會(huì)形成下面的結(jié)果:
test.example.com
:始終使用證書(shū)鏈固定的證書(shū)和啟用主機(jī)驗(yàn)證,因此需要以下條件才能是TLS握手成功:
(1) 證書(shū)鏈必須是有效的玉雾。
(2) 證書(shū)鏈必須包含一個(gè)已經(jīng)固定的證書(shū)翔试。
(3) Challenge主機(jī)必須匹配主機(jī)證書(shū)鏈的子證書(shū)。insecure.expired-apis.com
:將從不評(píng)估證書(shū)鏈抹凳,并且總是允許TLS握手成功遏餐。- 其他主機(jī)將會(huì)默認(rèn)使用蘋(píng)果提供的驗(yàn)證。
4. 3 子類化服務(wù)器信任策略管理者
- 如果我們需要一個(gè)更靈活的服務(wù)器信任策略來(lái)匹配其他行為(例如通配符域名)赢底,可以子類化
ServerTrustPolicyManager
失都,并且重寫(xiě)serverTrustPolicyForHost
方法柏蘑。
class CustomServerTrustPolicyManager: ServerTrustPolicyManager {
override func serverTrustPolicy(forHost host: String) -> ServerTrustPolicy? {
var policy: ServerTrustPolicy?
// Implement your custom domain matching behavior...
return policy
}
}
4. 4 驗(yàn)證主機(jī)
-
.performDefaultEvaluation
、.pinCertificates
和.pinPublicKeys
這三個(gè)服務(wù)器信任策略都帶有一個(gè)validateHost
參數(shù)粹庞。把這個(gè)值設(shè)為true
咳焚,服務(wù)器信任評(píng)估就會(huì)驗(yàn)證與challenge
主機(jī)名字匹配的在證書(shū)里面的主機(jī)名字。如果他們不匹配庞溜,驗(yàn)證失敗革半。如果設(shè)置為false
,仍然會(huì)評(píng)估整個(gè)證書(shū)鏈流码,但是不會(huì)驗(yàn)證子證書(shū)的主機(jī)名字又官。 - 建議在實(shí)際開(kāi)發(fā)中,把
validateHost
設(shè)置為true
漫试。
4. 5 驗(yàn)證證書(shū)鏈
-
Pinning certificate
和public keys
都可以通過(guò)validateCertificateChain
參數(shù)擁有驗(yàn)證證書(shū)鏈的選項(xiàng)六敬。把它設(shè)置為true
,除了對(duì)Pinning certificate
和public keys
進(jìn)行字節(jié)相等檢查外驾荣,還將會(huì)驗(yàn)證整個(gè)證書(shū)鏈外构。如果是false
,將會(huì)跳過(guò)證書(shū)鏈驗(yàn)證播掷,但還會(huì)進(jìn)行字節(jié)相等檢查审编。 - 還有很多情況會(huì)導(dǎo)致禁用證書(shū)鏈認(rèn)證。最常用的方式就是自簽名和過(guò)期的證書(shū)歧匈。在這些情況下垒酬,驗(yàn)證始終會(huì)失敗。但是字節(jié)相等檢查會(huì)保證我們從服務(wù)器接收到證書(shū)件炉。
- 建議在實(shí)際開(kāi)發(fā)中伤溉,把
validateCertificateChain
設(shè)置為true
。
4. 6 ATS 應(yīng)用傳輸安全 (App Transport Security)
- 從iOS9開(kāi)始妻率,就添加了App Transport Security (ATS),使用
ServerTrustPolicyManager
和多個(gè)ServerTrustPolicy
對(duì)象可能沒(méi)什么影響板祝。如果我們不斷看到CFNetwork
SSLHandshake failed (-9806)錯(cuò)誤
宫静,我們可能遇到了這個(gè)問(wèn)題。蘋(píng)果的ATS
系統(tǒng)重寫(xiě)了整個(gè)challenge系統(tǒng)券时,除非我們?cè)?code>plist文件中配置ATS
設(shè)置來(lái)允許應(yīng)用評(píng)估服務(wù)器信任孤里。 - plist文件設(shè)置如下:
<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/>
<!-- 可選的: 指定TLS的最小版本 -->
<key>NSTemporaryExceptionMinimumTLSVersion</key>
<string>TLSv1.2</string>
</dict>
</dict>
</dict>
</dict>
- 是否需要把
NSExceptionRequiresForwardSecrecy
設(shè)置為NO
取決于TLS連接是否使用一個(gè)允許的密碼套件。在某些情況下橘洞,它需要設(shè)置為NO捌袜。NSExceptionAllowsInsecureHTTPLoads
必須設(shè)置為YES
,然后SessionDelegate
才能接收到challenge
回調(diào)炸枣。一旦challenge
回調(diào)被調(diào)用虏等,ServerTrustPolicyManager
將接管服務(wù)器信任評(píng)估弄唧。如果我們要連接到一個(gè)僅支持小于1.2版本的TSL主機(jī),那么還要指定NSTemporaryExceptionMinimumTLSVersion
霍衫。 - 在實(shí)際開(kāi)發(fā)中候引,建議始終使用有效的證書(shū)。
5. 網(wǎng)絡(luò)可達(dá)性
5. 1 Network Reachability
- NetworkReachabilityManager監(jiān)聽(tīng)WWAN和WiFi網(wǎng)絡(luò)接口和主機(jī)地址的可達(dá)性變化敦跌。
let manager = NetworkReachabilityManager(host: "www.apple.com")
manager?.listener = { status in
print("Network Status Changed: \(status)")
}
manager?.startListening()
- 要確保manager被強(qiáng)引用澄干,否則會(huì)接收不到狀態(tài)變化。另外柠傍,在主機(jī)字符串中不要包含scheme麸俘,也就是說(shuō)要把https://去掉,否則無(wú)法監(jiān)聽(tīng)惧笛。
- 當(dāng)使用網(wǎng)絡(luò)可達(dá)性來(lái)決定接下來(lái)要做什么時(shí)从媚,有以下幾點(diǎn)需要重點(diǎn)注意的:
- 不要使用Reachability來(lái)決定是否發(fā)送一個(gè)網(wǎng)絡(luò)請(qǐng)求,我們必須要發(fā)送請(qǐng)求徐紧。
- 當(dāng)Reachability恢復(fù)了静檬,要重試網(wǎng)絡(luò)請(qǐng)求。即使網(wǎng)絡(luò)請(qǐng)求失敗并级,在這個(gè)時(shí)候也非常適合重試請(qǐng)求拂檩。
- 網(wǎng)絡(luò)可達(dá)性的狀態(tài)非常適合用來(lái)決定為什么網(wǎng)絡(luò)請(qǐng)求會(huì)失敗。如果一個(gè)請(qǐng)求失敗嘲碧,應(yīng)該告訴用戶是離線導(dǎo)致請(qǐng)求失敗的稻励,而不是技術(shù)錯(cuò)誤,例如請(qǐng)求超時(shí)愈涩。