Alamofire(四)怎么合理使用Alamofire

@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

  1. AlamofireImage
    一個(gè)圖片庫(kù),包括圖像響應(yīng)序列化器事扭、UIImage和UIImageView的擴(kuò)展捎稚、自定義圖像濾鏡、內(nèi)存中自動(dòng)清除和基于優(yōu)先級(jí)的圖像下載系統(tǒng)求橄。
  1. AlamofireNetworkActivityIndicator
    控制iOS應(yīng)用的網(wǎng)絡(luò)活動(dòng)指示器今野。包含可配置的延遲計(jì)時(shí)器來(lái)幫助減少閃光,并且支持不受Alamofire管理的URLSession實(shí)例罐农。
  • (2) Alamofire框架提供了什么功能条霜?
  1. 鏈?zhǔn)秸?qǐng)求 / 響應(yīng)方法
  2. URL / JSON / plist參數(shù)編碼
  3. 上傳文件 / 數(shù)據(jù) / 流 / 多表單數(shù)據(jù)
  4. 使用請(qǐng)求或者斷點(diǎn)下載來(lái)下載文件
  5. 使用URL憑據(jù)進(jìn)行身份認(rèn)證
  6. HTTP響應(yīng)驗(yàn)證
  7. 包含進(jìn)度的上傳和下載閉包
  8. cURL命令的輸出
  9. 動(dòng)態(tài)適配和重試請(qǐng)求
  10. TLS證書(shū)和Public Key Pinning
  11. 網(wǎng)絡(luò)可達(dá)性
  12. 全面的單元和集成測(cè)試覆蓋率
  1. 要求的使用環(huán)境:

iOS 10.0+ / macOS 10.12+ / tvOS 10.0+ / watchOS 3.0+
Xcode 10.2+
Swift 5+

  1. 安裝方法:
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
  1. 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ì)是.successvalueData類型董济。
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è)自定義的URLSessionConfigurationdefaultHTTPHeaders屬性將會(huì)被更新闷盔,并且自定義的會(huì)話配置也會(huì)應(yīng)用到新的SessionManager實(shí)例弯洗。

2.5.3 認(rèn)證

  • 認(rèn)證是使用系統(tǒng)框架URLCredentialURLAuthenticationChallenge實(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沪摄、JSONPropertyList躯嫉。還支持遵循了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ì)象遵循了CustomStringConvertibleCustomDebugStringConvertible協(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í)例:

  1. 重寫(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?)?
  1. 使用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
}
  1. 下面這個(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勿决、downloaduploadstream方法的結(jié)果是DataRequest招盲、DownloadRequest低缩、UploadRequestStreamRequest,并且所有請(qǐng)求都繼承自Request曹货。所有的Request并不是直接創(chuàng)建的咆繁,而是由session manager創(chuàng)建的。

  • 每個(gè)子類都有特定的方法控乾,例如authenticate么介、validateresponseJSONuploadProgress蜕衡,都返回一個(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ì)模式的URLConvertibleURLRequestConvertible協(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位岔、uploaddownload方法.
  • 以一種有意義的方式和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擦剑、uploaddownload(推薦用這種方法為單個(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)題要考慮。

  • RequestAdapterRequestRetrier協(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)求末捣。
  1. 創(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
    }

}
  1. 創(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)一起使用RequestAdapterRequestRetrier協(xié)議時(shí)箩做,我們可以為OAuth1OAuth2妥畏、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)證類型重新定制郭赐。

  1. 創(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
            }
    }
}
  1. 創(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)
}
  • 一旦OAuth2HandlerSessionManager被應(yīng)用與adapterretrier罗捎,他將會(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)用中,我們想要檢查realmwww-authenticate header響應(yīng)泻红,雖然這取決于OAuth2的實(shí)現(xiàn)夭禽。

  • 還有一個(gè)要重點(diǎn)注意的是,這個(gè)認(rèn)證系統(tǒng)可以在多個(gè)session manager之間共享谊路。例如讹躯,可以在同一個(gè)Web服務(wù)集合使用defaultephemeral會(huì)話配置。上面這個(gè)例子可以在多個(gè)session manager間共享一個(gè)oauthHandler實(shí)例缠劝,來(lái)管理一個(gè)恢復(fù)流程潮梯。

2.9

3. 序列化

3.1 自定義響應(yīng)序列化

  • Alamofire為datastrings惨恭、JSONProperty 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耗啦、JSONProperty 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é)果:
  1. test.example.com:始終使用證書(shū)鏈固定的證書(shū)和啟用主機(jī)驗(yàn)證,因此需要以下條件才能是TLS握手成功:
    (1) 證書(shū)鏈必須是有效的玉雾。
    (2) 證書(shū)鏈必須包含一個(gè)已經(jīng)固定的證書(shū)翔试。
    (3) Challenge主機(jī)必須匹配主機(jī)證書(shū)鏈的子證書(shū)。
  2. insecure.expired-apis.com:將從不評(píng)估證書(shū)鏈抹凳,并且總是允許TLS握手成功遏餐。
  3. 其他主機(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 certificatepublic keys 都可以通過(guò)validateCertificateChain參數(shù)擁有驗(yàn)證證書(shū)鏈的選項(xiàng)六敬。把它設(shè)置為true,除了對(duì)Pinning certificatepublic 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í)愈涩。
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末望抽,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子履婉,更是在濱河造成了極大的恐慌煤篙,老刑警劉巖,帶你破解...
    沈念sama閱讀 218,755評(píng)論 6 507
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件毁腿,死亡現(xiàn)場(chǎng)離奇詭異辑奈,居然都是意外死亡,警方通過(guò)查閱死者的電腦和手機(jī)已烤,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,305評(píng)論 3 395
  • 文/潘曉璐 我一進(jìn)店門(mén)鸠窗,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái),“玉大人胯究,你說(shuō)我怎么就攤上這事稍计。” “怎么了裕循?”我有些...
    開(kāi)封第一講書(shū)人閱讀 165,138評(píng)論 0 355
  • 文/不壞的土叔 我叫張陵臣嚣,是天一觀的道長(zhǎng)净刮。 經(jīng)常有香客問(wèn)我,道長(zhǎng)茧球,這世上最難降的妖魔是什么庭瑰? 我笑而不...
    開(kāi)封第一講書(shū)人閱讀 58,791評(píng)論 1 295
  • 正文 為了忘掉前任,我火速辦了婚禮抢埋,結(jié)果婚禮上弹灭,老公的妹妹穿的比我還像新娘。我一直安慰自己揪垄,他們只是感情好穷吮,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,794評(píng)論 6 392
  • 文/花漫 我一把揭開(kāi)白布取刃。 她就那樣靜靜地躺著京办,像睡著了一般。 火紅的嫁衣襯著肌膚如雪毫深。 梳的紋絲不亂的頭發(fā)上酷愧,一...
    開(kāi)封第一講書(shū)人閱讀 51,631評(píng)論 1 305
  • 那天驾诈,我揣著相機(jī)與錄音,去河邊找鬼溶浴。 笑死乍迄,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的士败。 我是一名探鬼主播闯两,決...
    沈念sama閱讀 40,362評(píng)論 3 418
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼谅将!你這毒婦竟也來(lái)了漾狼?” 一聲冷哼從身側(cè)響起,我...
    開(kāi)封第一講書(shū)人閱讀 39,264評(píng)論 0 276
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤饥臂,失蹤者是張志新(化名)和其女友劉穎逊躁,沒(méi)想到半個(gè)月后,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體隅熙,經(jīng)...
    沈念sama閱讀 45,724評(píng)論 1 315
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡志衣,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,900評(píng)論 3 336
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了猛们。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 40,040評(píng)論 1 350
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡狞洋,死狀恐怖弯淘,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情吉懊,我是刑警寧澤庐橙,帶...
    沈念sama閱讀 35,742評(píng)論 5 346
  • 正文 年R本政府宣布假勿,位于F島的核電站,受9級(jí)特大地震影響态鳖,放射性物質(zhì)發(fā)生泄漏转培。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,364評(píng)論 3 330
  • 文/蒙蒙 一浆竭、第九天 我趴在偏房一處隱蔽的房頂上張望浸须。 院中可真熱鬧,春花似錦邦泄、人聲如沸删窒。這莊子的主人今日做“春日...
    開(kāi)封第一講書(shū)人閱讀 31,944評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)肌索。三九已至,卻和暖如春特碳,著一層夾襖步出監(jiān)牢的瞬間诚亚,已是汗流浹背。 一陣腳步聲響...
    開(kāi)封第一講書(shū)人閱讀 33,060評(píng)論 1 270
  • 我被黑心中介騙來(lái)泰國(guó)打工午乓, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留站宗,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 48,247評(píng)論 3 371
  • 正文 我出身青樓硅瞧,卻偏偏與公主長(zhǎng)得像份乒,于是被迫代替她去往敵國(guó)和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子腕唧,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,979評(píng)論 2 355

推薦閱讀更多精彩內(nèi)容

  • 這篇文章是 Alamofire 5.0 以前的文檔或辖,最新文檔請(qǐng)查看: Alamofire 5 的使用 - 基本用法...
    Lebron_James閱讀 46,960評(píng)論 36 116
  • 一枣接,Alamofire的說(shuō)明與配置 1颂暇,什么是Alamofire (1)Alamofire的前身是AFNetwor...
    騎著毛驢走起來(lái)閱讀 5,746評(píng)論 0 3
  • Swift1> Swift和OC的區(qū)別1.1> Swift沒(méi)有地址/指針的概念1.2> 泛型1.3> 類型嚴(yán)謹(jǐn) 對(duì)...
    cosWriter閱讀 11,103評(píng)論 1 32
  • Alamofire的基本用法 1.請(qǐng)求 這是一個(gè)最簡(jiǎn)單的請(qǐng)求,這個(gè)請(qǐng)求即不需要參數(shù)但惶,也不需要接收數(shù)據(jù)耳鸯。接下來(lái)我們翻...
    水落斜陽(yáng)閱讀 3,224評(píng)論 0 16
  • 重點(diǎn)來(lái)源:翻譯自官方github 添加依賴 CocoaPodspod 'Alamofire', '~> 5.0.0...
    chordwang閱讀 1,325評(píng)論 0 5