Alamofire框架的使用一 —— 基本用法 對於使用Objective-C的開發者,一定非常熟悉AFNetworking這個網路框架。在蘋果推出的Swift之後,AFNetworking的作者專門用Swift來編寫一個類似AFNetworking的網路框架,稱為Alamofire。Alamofi ...
Alamofire框架的使用一 —— 基本用法
對於使用Objective-C的開發者,一定非常熟悉AFNetworking
這個網路框架。在蘋果推出的Swift之後,AFNetworking
的作者專門用Swift來編寫一個類似AFNetworking
的網路框架,稱為Alamofire
。Alamofire地址 >>
我分兩篇文章介紹如何使用Alamofire框架。文章的內容主要是翻譯Alamofire的readme。第二篇文章 >>
功能
- 鏈式請求/響應方法
- URL / JSON / plist參數編碼
- 上傳文件/數據/流/多表單數據
- 使用請求或者斷點下載來下載文件
- 使用URL憑據進行身份認證
- HTTP響應驗證
- 包含進度的上傳和下載閉包
- cURL命令的輸出
- 動態適配和重試請求
- TLS證書和Public Key Pinning
- 網路可達性
- 全面的單元和集成測試覆蓋率
組件庫
為了讓Alamofire
專註於核心網路的實現,Alamofire生態系統還有另外兩個庫:
- AlamofireImage:一個圖片庫,包括圖像響應序列化器、
UIImage
和UIImageView
的擴展、自定義圖像濾鏡、記憶體中自動清除和基於優先順序的圖像下載系統。 - AlamofireNetworkActivityIndicator:控制iOS應用的網路活動指示器。包含可配置的延遲計時器來幫助減少閃光,並且支持不受Alamofire管理的URLSession實例。
要求的使用環境
- iOS 8.0+ / macOS 10.10+ / tvOS 9.0+ / watchOS 2.0+
- Xcode 8.3+
- Swift 3.1+
安裝方法
CocoaPods
source 'https://github.com/CocoaPods/Specs.git'
platform :ios, '10.0'
use_frameworks!
target '項目名稱' do
pod 'Alamofire', '~> 4.7'
end
iOS版本和Alamofire
版本可以自己根據實際情況自行更改。CocoaPods是比較常用的第三方庫管理工具,其他方法就不詳細說了。
如何使用
發請求
Alamofire.request("http://baidu.com/")
響應處理
直接在請求後面用點語法鏈接響應處理:
Alamofire.request("https://httpbin.org/get").responseJSON { response in
print(response.request) // 原始的URL請求
print(response.response) // HTTP URL響應
print(response.data) // 伺服器返回的數據
print(response.result) // 響應序列化結果,在這個閉包里,存儲的是JSON數據
if let JSON = response.result.value {
print("JSON: \(JSON)")
}
}
在上面的例子中,responseJSON
handler直接拼接到請求後面,當請求完成後被調用。這個閉包一旦收到響應後,就會處理這個響應,並不會因為等待伺服器的響應而造成阻塞執行。請求的結果僅在響應閉包的範圍內可用。其他任何與伺服器返回的響應或者數據相關的操作,都必須在這個閉包內執行。
Alamofire預設情況下包含五種不同的響應handler:
// 響應 Handler - 未序列化的響應
func response(
queue: DispatchQueue?,
completionHandler: @escaping (DefaultDataResponse) -> Void)
-> Self
// 響應數據 Handler - 序列化成數據類型
func responseData(
queue: DispatchQueue?,
completionHandler: @escaping (DataResponse<Data>) -> Void)
-> Self
// 響應字元串 Handler - 序列化成字元串類型
func responseString(
queue: DispatchQueue?,
encoding: String.Encoding?,
completionHandler: @escaping (DataResponse<String>) -> Void)
-> Self
// 響應 JSON Handler - 序列化成Any類型
func responseJSON(
queue: DispatchQueue?,
completionHandler: @escaping (DataResponse<Any>) -> Void)
-> Self
// 響應 PropertyList (plist) Handler - 序列化成Any類型
func responsePropertyList(
queue: DispatchQueue?,
completionHandler: @escaping (DataResponse<Any>) -> Void))
-> Self
所有的響應handler都不會對響應進行驗證。也就是說響應狀態碼在400..<500
和500..<600
範圍內,都不會觸發錯誤。
響應 Handler
response
handler不處理任何響應數據。它僅僅是從URL session delegate中轉發信息。
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)")
}
}
一般情況下不建議使用這種沒有響應序列化器的handler,而應該使用下麵有特定序列化器的handler。
響應數據 Handler
responseData
handler使用responseDataSerializer
(這個對象把伺服器的數據序列化成其他類型)來提取伺服器返回的數據。如果沒有返回錯誤並且有數據返回,那麼響應Result
將會是.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)")
}
}
響應字元串 Handler
responseString
handler使用responseStringSerializer
對象根據指定的編碼格式把伺服器返回的數據轉換成String
。如果沒有返回錯誤並且伺服器的數據成功地轉換為String
,那麼響應Result
將會是.success
,value
是String
類型。
Alamofire.request("https://httpbin.org/get").responseString { response in
print("Success: \(response.result.isSuccess)")
print("Response String: \(response.result.value)")
}
如果沒有指定編碼格式,將會使用伺服器的HTTPURLResponse
指定的格式。如果伺服器無法確定編碼格式,那麼預設使用.isoLatin1
。
響應 JSON Handler
responseJSON
handler使用responseJSONSerializer
根據指定的JSONSerialization.ReadingOptions
把伺服器返回的數據轉換成Any
類型。如果沒有返回錯誤並且伺服器的數據成功地轉換為JSON
對象,那麼響應Result
將會是.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
完成的。
鏈式響應handler
響應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)")
}
註意:在同一個請求中使用多個響應handler,要求伺服器的數據會被序列化多次,每次對應一個handler。
響應handler隊列
預設情況下,響應handler是在主隊列執行的。但是我們也可以自定義隊列:
let utilityQueue = DispatchQueue.global(qos: .utility)
Alamofire.request("https://httpbin.org/get").responseJSON(queue: utilityQueue) { response in
print("Executing response handler on utility queue")
}
響應驗證
預設情況下,Alamofire把所有完成的請求當做是成功的請求,無論響應的內容是什麼。如果響應有一個不能被接受的狀態碼或者MIME類型,在響應handler之前調用validate
將會產生錯誤。
手動驗證
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)
}
}
自動驗證
自動驗證在200…299
範圍內的狀態碼;如果請求頭中有指定Accept
,那麼也會驗證響應頭的與請求頭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)
}
}
響應緩存
響應緩存是使用系統的框架URLCache
來處理的。它提供了記憶體和磁碟上的緩存,並允許我們控制記憶體和磁碟的大小。
預設情況下,Alamofire
利用共用的URLCache
。
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
時,可以傳入方法參數:
Alamofire.request("https://httpbin.org/get") // 預設是get請求
Alamofire.request("https://httpbin.org/post", method: .post)
Alamofire.request("https://httpbin.org/put", method: .put)
Alamofire.request("https://httpbin.org/delete", method: .delete)
參數編碼
Alamofire支持三種參數編碼:URL
、JSON
和PropertyList
。還支持遵循了ParameterEncoding
協議的自定義編碼。
URL編碼
URLEncoding
類型創建了一個URL編碼的查詢字元串來設置或者添加到一個現有的URL查詢字元串,或者設置URL請求的請求體。查詢字元串是否被設置或者添加到現有的URL查詢字元串,或者被作為HTTP請求體,決定於編碼的Destination
。編碼的Destination
有三個case:
.methodDependent
:為GET
、HEAD
和DELETE
請求使用編碼查詢字元串來設置或者添加到現有查詢字元串,並且使用其他HTTP方法來設置請求體。.queryString
:設置或者添加編碼查詢字元串到現有查詢字元串.httpBody
:把編碼查詢字元串作為URL請求的請求體
一個編碼請求的請求體的Content-Type
欄位被設置為application/x-www-form-urlencoded; charset=utf-8
。因為沒有公開的標準說明如何編碼集合類型,所以按照慣例在key後面添加[]
來表示數組的值(foo[]=1&foo[]=2
),在key外麵包一個中括弧來表示字典的值(foo[bar]=baz
)。
使用URL編碼參數的GET請求
let parameters: Parameters = ["foo": "bar"]
// 下麵這三種寫法是等價的
Alamofire.request("https://httpbin.org/get", parameters: parameters) // encoding 預設是`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編碼參數的POST請求
let parameters: Parameters = [
"foo": "bar",
"baz": ["a", 1],
"qux": [
"x": 1,
"y": 2,
"z": 3
]
]
// 下麵這三種寫法是等價的
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
設置Bool
類型參數的編碼
URLEncoding.BoolEncoding
提供了兩種編碼方式:
.numeric
:把true
編碼為1
,false
編碼為0
.literal
:把true
編碼為true
,false
編碼為false
預設情況下:Alamofire使用.numeric
。
可以使用下麵的初始化函數來創建URLEncoding
,指定Bool編碼的類型:
let encoding = URLEncoding(boolEncoding: .literal)
設置Array
類型參數編碼
URLEncoding.ArrayEncoding
提供了兩種編碼方式:
.brackets
: 在每個元素值的key後面加上一個[]
,如foo=[1,2]
編碼成foo[]=1&foo[]=2
.noBrackets
:不添加[]
,例如foo=[1,2]
編碼成``foo=1&foo=2`
預設情況下,Alamofire使用.brackets
。
可以使用下麵的初始化函數來創建URLEncoding
,指定Array編碼的類型:
let encoding = URLEncoding(arrayEncoding: .noBrackets)
JSON編碼
JSONEncoding
類型創建了一個JOSN對象,並作為請求體。編碼請求的請求頭的Content-Type
請求欄位被設置為application/json
。
使用JSON編碼參數的POST請求
let parameters: Parameters = [
"foo": [1,2,3],
"bar": [
"baz": "qux"
]
]
// 下麵這兩種寫法是等價的
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"}}
屬性列表編碼
PropertyListEncoding
根據關聯格式和寫選項值,使用PropertyListSerialization
來創建一個屬性列表對象,並作為請求體。編碼請求的請求頭的Content-Type
請求欄位被設置為application/x-plist
。
自定義編碼
如果提供的ParameterEncoding
類型不能滿足我們的要求,可以創建自定義編碼。下麵演示如何快速自定義一個JSONStringArrayEncoding
類型把JSON字元串數組編碼到請求中。
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
}
}
手動URL請求參數編碼
ParameterEncoding
API可以在創建網路請求外面使用。
let url = URL(string: "https://httpbin.org/get")!
var urlRequest = URLRequest(url: url)
let parameters: Parameters = ["foo": "bar"]
let encodedURLRequest = try URLEncoding.queryString.encode(urlRequest, with: parameters)
HTTP請求頭
可以直接在請求方法添加自定義HTTP請求頭,這有利於我們在請求中添加請求頭。
let headers: HTTPHeaders = [
"Authorization": "Basic QWxhZGRpbjpvcGVuIHNlc2FtZQ==",
"Accept": "application/json"
]
Alamofire.request("https://httpbin.org/headers", headers: headers).responseJSON { response in
debugPrint(response)
}
對於那些不變的請求頭,建議在URLSessionConfiguration
設置,這樣就可以自動被用於任何URLSession
創建的URLSessionTask
。
預設的Alamofire SessionManager
為每一個請求提供了一個預設的請求頭集合,包括:
Accept-Encoding
,預設是gzip;q=1.0, compress;q=0.5
。Accept-Language
,預設是系統的前6個偏好語言,格式類似於en;q=1.0
。User-Agent
,包含當前應用程式的版本信息。例如iOS Example/1.0 (com.alamofire.iOS-Example; build:1; iOS 10.0.0) Alamofire/4.0.0
。
如果要自定義這些請求頭集合,我們必須創建一個自定義的URLSessionConfiguration
,defaultHTTPHeaders
屬性將會被更新,並且自定義的會話配置也會應用到新的SessionManager
實例。
認證
認證是使用系統框架URLCredential
和URLAuthenticationChallenge
實現的。
支持的認證方案
- HTTP Basic
- HTTP Digest
- Kerberos
- NTLM
HTTP Basic認證
在合適的時候,在一個請求的authenticate
方法會自動提供一個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)
}
根據伺服器實現,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)
}
使用URLCredential認證
let user = "user"
let password = "password"
let credential = URLCredential(user: user, password: password, persistence: .forSession)
Alamofire.request("https://httpbin.org/basic-auth/\(user)/\(password)")
.authenticate(usingCredential: credential)
.responseJSON { response in
debugPrint(response)
}
註意:使用URLCredential
來做認證,如果伺服器發出一個challenge,底層的URLSession
實際上最終會發兩次請求。第一次請求不會包含credential,並且可能會觸發伺服器發出一個challenge。這個challenge會被Alamofire接收,credential會被添加,然後URLSessin
會重試請求。
將數據下載到文件
Alamofire可以把伺服器的數據下載到記憶體(in-memory)或者硬碟(on-disk)中。所有Alamofire.request
API下載的數據都是存儲在記憶體中。這比較適合小文件,更高效;但是不適合大文件,因為大文件會把記憶體耗盡。我們要使用Alamofire.download
API把伺服器的數據下載到硬碟中。
下麵這個方法只適用於macOS
。因為在其他平臺不允許在應用沙盒外訪問文件系統。下麵會講到如何在其他平臺下載文件。
Alamofire.download("https://httpbin.org/image/png").responseData { response in
if let data = response.result.value {
let image = UIImage(data: data)
}
}
下載文件存儲位置
我們可以提供一個DownloadFileDestination
閉包把臨時文件夾的文件移動到一個目標文件夾。在臨時文件真正移動到destinationURL
之前,閉包內部指定的DownloadOptions
將會被執行。目前支持的DownloadOptions
有下麵兩個:
.createIntermediateDirectories
:如果指定了目標URL,將會創建中間目錄。.removePreviousFile
:如果指定了目標URL,將會移除之前的文件
let destination: DownloadRequest.DownloadFileDestination = { _, _ in
let documentsURL = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask)[0]
let fileURL = documentsURL.appendPathComponent("pig.png")
return (fileURL, [.removePreviousFile, .createIntermediateDirectories])
}
Alamofire.download(urlString, to: destination).response { response in
print(response)
if response.error == nil, let imagePath = response.destinationURL?.path {
let image = UIImage(contentsOfFile: imagePath)
}
}
也可以直接使用建議的下載目標API:
let destination = DownloadRequest.suggestedDownloadDestination(directory: .documentDirectory)
Alamofire.download("https://httpbin.org/image/png", to: destination)
下載進度
所有的DownloadRequest
都可以使用downloadProgress
API來反饋下載進度。
Alamofire.download("https://httpbin.org/image/png")
.downloadProgress { progress in
print("Download Progress: \(progress.fractionCompleted)")
}
.responseData { response in
if let data = response.result.value {
let image = UIImage(data: data)
}
}
downloadProgress
API還可以接受一個queue
參數來指定下載進度閉包在哪個DispatchQueue
中執行。
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)
}
}
恢復下載
如果一個DownloadRequest
被取消或中斷,底層的URL會話會生成一個恢複數據。恢複數據可以被重新利用併在中斷的位置繼續下載。恢複數據可以通過下載響應訪問,然後在重新開始請求的時候被利用。
重要:在iOS 10 - 10.2, macOS 10.12 - 10.12.2, tvOS 10 - 10.1, watchOS 3 - 3.1.1中,resumeData
會被後臺URL會話配置破壞。因為在resumeData
的生成邏輯有一個底層的bug,不能恢復下載。具體情況可以到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
}
}
}
}
上傳數據到伺服器
使用JOSN或者URL編碼參數上傳一些小數據到伺服器,使用Alamofire.request
API就已經足夠了。如果需要發送很大的數據,需要使用Alamofire.upload
API。當我們需要在後臺上傳數據時,也可以使用Alamofire.upload
。
上傳數據
let imageData = UIPNGRepresentation(image)!
Alamofire.upload(imageData, to: "https://httpbin.org/post").responseJSON { response in
debugPrint(response)
}
上傳文件
let fileURL = Bundle.main.url(forResource: "video", withExtension: "mov")
Alamofire.upload(fileURL, to: "https://httpbin.org/post").responseJSON { response in
debugPrint(response)
}
上傳多部分表單數據
Alamofire.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)
}
}
)
上傳進度
所有的UploadRequest
都可以使用uploadProgress
和downloadProgress
APIs來反饋上傳和下載進度。
let fileURL = Bundle.main.url(forResource: "video", withExtension: "mov")
Alamofire.upload(fileURL, to: "https://httpbin.org/post")
.uploadProgress { progress in // 預設在主線程中執行
print("Upload Progress: \(progress.fractionCompleted)")
}
.downloadProgress { progress in // 預設在主線程中執行
print("Download Progress: \(progress.fractionCompleted)")
}
.responseJSON { response in
debugPrint(response)
}
統計指標
時間表
Alamofire在一個請求周期內收集時間,並創建一個Tineline
對象,它是響應類型的一個屬性。
Alamofire.request("https://httpbin.org/get").responseJSON { response in
print(response.timeline)
}
上面的Timeline
信息包括:
- Latency: 0.428 seconds (延遲)
- Request Duration: 0.428 seconds (請求時間)
- Serialization Duration: 0.001 seconds (序列化時間)
- Total Duration: 0.429 seconds (總時間)
URL會話任務指標
在iOS和tvOS 10和macOS 10.12中,蘋果發佈了新的URLSessionTaskMetrics
APIs。這個任務指標封裝了關於請求和響應執行的神奇統計信息。這個API和Timeline
非常相似,但是提供了很多Alamofire沒有提供的統計信息。這些指標可以通過任何響應去訪問。
Alamofire.request("https://httpbin.org/get").responseJSON { response in
print(response.metrics)
}
註意:這些API只能在iOS和tvOS 10和macOS 10.12中使用。所以,根據部署目標,可能需要加入版本判斷:
Alamofire.request("https://httpbin.org/get").responseJSON { response in
if #available(iOS 10.0. *) {
print(response.metrics)
}
}
cURL命令輸出
調試平臺問題很讓人厭煩。慶幸的是,Alamofire的Request
對象遵循了CustomStringConvertible
和CustomDebugStringConvertible
協議來提供一些非常有用的調試工具。
CustomStringConvertible
let request = Alamofire.request("https://httpbin.org/ip")
print(request)
// GET https://httpbin.org/ip (200)
CustomDebugStringConvertible
let request = Alamofire.request("https://httpbin.org/get", parameters: ["foo": "bar"])
debugPrint(request)
輸出:
$ curl -i \
-H "User-Agent: Alamofire/4.0.0" \
-H "Accept-Encoding: gzip;q=1.0, compress;q=0.5" \
-H "Accept-Language: en;q=1.0,fr;q=0.9,de;q=0.8,zh-Hans;q=0.7,zh-Hant;q=0.6,ja;q=0.5" \
"https://httpbin.org/get?foo=bar"
第一部分完。
作者:Lebron_James
鏈接:https://www.jianshu.com/p/f8c3adb056cf
來源:簡書
簡書著作權歸作者所有,任何形式的轉載都請聯繫作者獲得授權並註明出處。