背景
最近項目上完成了從AFNetworking
到Alamofire
的遷移,此為背景。
Overview
- Alamofire簡介
- 使用方式
- 圖片下載
- 如何在OC項目中使用
- 從AFNetworking遷移到Alamofire注意事項
- 總結
Alamofire簡介
Alamofire is an HTTP networking library written in Swift.
上面這句話是Alamofire的官方Github上面的描述,意思是說:“Alamofire是一個用Swift寫的HTTP網絡庫”。其開發團隊與AFNetworking
是同一個團隊(或許AFNetworking中的AF就是Alamofire,哈哈??。)
Alamofire的Github地址:https://github.com/Alamofire/Alamofire
下面是Alamofire的大版本更新時間。
- Alamofire第一次Release是在2015年5月11日,發布了1.0版本;
- 之后在同一年(2015年)9月發布了2.0,
- 同年10月份就發布了3.0版本,
- 之后2016年的9月,發布了4.0版本
- 知道今天,4.0都是一個相對穩定的版本,現在最新版本
4.7.3
,支持最新的Swift 4.2
和Xcode 10
,可以說更新速度十分快了。同時我們可以看到,現在Alamofire團隊正在開發Alamofire 5.0
安裝
Cocoapods
在Podfile中添加如下代碼:
source 'https://github.com/cocoapods/specs.git'
platform :ios, '10.0'
use_frameworks!
target '<Your Target Name>' do
...other pods
pod 'Alamofire', '~> 4.7'
end
Carthage
github "Alamofire/Alamofire" ~> 4.7
Swift Package Manager
dependencies: [
.Package(url: "https://github.com/Alamofire/Alamofire.git", majorVersion: 4)
]
使用方式
關于一些細節問題,后面在后面的章節從AFNetworking遷移到Alamofire注意事項
中會有涉及,這里只介紹基本的使用方法。
Alamofire.request(url).response { response in
if let error = response.result.error {
// Handle error
return
}
// Handle response.result.value
}
大家可以看到,在這里,是使用Alamofire發了一個最簡單的請求,和AFNetworking還是由不少區別的,比如不管是GET
, POST
, 都使用request方法,而方法會作為參數傳入,并且request的時候不會直接傳入completion的closure,而是在.request(url)
之后,使用.response()
來拿到response,那么一個提醒是——在我們使用.request(url)
之后,這個請求就已經發出去了,使用.response()
只是把已經獲得的response傳給你,當然如果你直接鏈式的調用.response()
的話,可能還沒有收到response,所以這里還是異步的completion closure的形式來獲取response的。
還有一點需要注意的是:如果沒有加Validation的話,404
,500
Alamofire都會認為請求成功了,而不會返回error。只用調用了validation()
才能正常的獲得error。如下所示:
Alamofire.request(url).validate().response { response in
if let error = response.result.error {
// Handle error
return
}
// Handle response.result.value
}
另外一點需要注意是:如果按如下方式寫的話
let dataRequest = Alamofire.request(url)
dataRequest.validate().response { response in
if let error = response.result.error {
// Handle error
return
}
// Handle response.result.value
}
也是不會在400
,404
,500
的時候得到error的,如果你一定需要單獨寫request的話,可以寫成下面這樣:
let dataRequest = Alamofire.request(url).validate()
dataRequest.response { response in
if let error = response.result.error {
// Handle error
return
}
// Handle response.result.value
}
最后一點是validate()
函數的默認validation是StatusCode在200
到300
之間,ContentType必須是你使用的response方法能夠解析的。例如.responseJSON()
需要ContentType是application/json
之類的。具體大家可以自己嘗試。復雜的API可以根據需要自行查看Alamofire的文檔。
附Alamofire request方法完整參數簽名:
Alamofire.request("https://httpbin.org/post", method: .post, parameters: parameters, encoding: JSONEncoding.default, headers: headers)
圖片下載
使用網絡Framework,大家都很關注一個問題,就是圖片下載的問題,有些人可能會使用SDWebImage
,或者KingFisher
之類的網絡圖片第三方庫,但是也有一些人會不希望引入過多的第三方庫,所以也比較關注一個第三方庫是否有圖片下載的功能。好消息是,Alamofire確實有,Alamofire有一個對Alamofire拓展的庫,叫做AlamofireImage
,可以滿足你的需求。這里簡單介紹下我們需要的API。
import Alamofire
import AlamofireImage
Alamofire.request("https://httpbin.org/image/png").responseImage { response in
if let image = response.result.value {
print("image downloaded: \(image)")
}
}
首先如上面這段代碼所示,我們可以方便的直接下載一個圖片,然后把它使用在我們需要的地方,另外我們也可以它對UIImageView
的拓展快速的給一個ImageView設置網絡圖片。API如下:
let imageView = UIImageView()
imageView.af_setImage(withURL: url)
如果你不喜歡“_”的命名方式大量出現在你的代碼中,你也可以自己給UIImageView加一個extension,代碼如下:
import Alamofire
import AlamofireImage
extension UIImageView {
func setImage(withURL url: URL) {
af_setImage(withURL: url)
}
}
let imageView = UIImageView()
imageView.setImage(withURL: url)
這樣不僅可以避免代碼中出現“_”,還可以對第三方API做一個隔離,不需要到處import AlamofireImage
,將來你想換一個網絡圖片的庫了,很簡單,把setImage
的實現換成新的庫的API就可以了。
如何在OC項目中使用
首先,Alamofire不用直接在OC的代碼中調用,因為其中有許多API有類似于泛型,Swift Enum等。但是,這并不能夠阻擋我們在OC的代碼中使用Alamofire。
思路確認:
- 首先,只有Swift可以使用Alamofire。
- 第二,有關泛型,Swift 非Int類型的Enum,還有一些Swifty的語法OC不能使用。
明白了這兩點,思路就有了,那就是用Swift封裝一層Alamofire,并且對外只暴露OC可以使用的API。舉個簡單的例子:
class AlamofireNetworkClient: NSObject {
@objc func fetchData(completion: @escaping ([String: Any]) -> Void) {
Alamofire
.request("https://httpbin.org/get", method: .get, parameters: [:], encoding: URLEncoding.default, headers: nil)
.responseJSON { response in
switch response.result {
case .success(let value):
let data = value as? [String: Any] ?? [:]
completion(data)
case .failure(let error):
print(error.localizedDescription)
}
}
}
}
大家可以看上面這段代碼,對于Swift中才能使用的Enum, .get
還有類似與.success(let value)
這樣的代碼,我們就將其留在封裝的的API之中,對外我們只是用可以再OC中使用的代碼,例如我們的completion的closure,如果希望將HTTP METHOD也傳進來的話,可以使用字符串的方式加在方法的參數列表中傳入。在轉成Swift中的Enum類型。
從AFNetworking遷移到Alamofire注意事項
介于可能會有很多人和我們一樣,現在也在使用AFNetworking,這里簡單說一下我們在整個遷移過程中遇到的問題。
首先,簡單介紹下我們遷移的流程。
我們的遷移流程如上圖,通過以上流程,可以相對平滑的替換掉現有的AFNetworking,并且也方便將來使用新的Networking的庫(這里需要提的一點是,我們使用所有的第三方庫的時候,特別是工具類的第三方庫的時候,對第三方庫進行一個簡單的封裝,并且用Protocol來限制其行為,可以有效的降低將來替換的工作量)。當然,我們還遇到了許多問題,這里就不一一描述了,只針對幾個典型的問題進行描述。
AFNetworking的API被在項目中直接使用的問題
這個問題的答案大家其實從遷移流程中已經看到了,我們不打算一一將使用AFNetworking的地方替換成Alamofire,而是先將現有AFNetworking API使用收束到一個統一的類中,我們這里叫
AFNetworkClient
。之后刪除所有AFNetworking的頭文件引入,除了AFNetworkClient
,這樣我們就有了一個封裝好的API Client。之后就如流程中寫的,完成替換工作即可。
AFNetworking與Alamofire API有許多默認行為不一致的問題
在替換的過程中,我們會發現,其實有很多原來的AFNetworking中的API不見了,或者行為不一樣了,這其實還是造成了不少困擾的。
- Response驗證
AFNetworking有默認的錯誤驗證,對于404,500這類的http statusCode,我們會在回調中拿到一個error。但是在Alamofire中,它的默認行為是不對狀態碼和Content-Type驗證,既它認為只要我收到了Response,我就認為你的請求是成功的,至于狀態碼,那不過是你收到的Response的一種,不能算錯誤。當然,這里它還是提供了一個API的,使用如下寫法可以讓Alamofire也驗證Response
Alamofire.get("url").validate().response {
// handler response.
}
- Response Serializer的類型少了
AFNetworking幫我們實現了多種Serializer,甚至還有一種組合的Serializer,可以組合多種Serializer使用,因為對于NetworkClient,如果不在invoke method的時候傳類型,我們只能讓其支持多種Response Content Type,AFNetworking和Alamofire都能支持多種類型的Response,其實我們在設計NetworkingClient的API的時候應該考慮這點,但是因為遺留問題,我們很難將所有的API調用需要的Serializer類型全部理清楚,所以我們使用Alamofire實現了一個類似AFNetworking的組合Serializer,代碼如下:
import Alamofire
import Foundation
extension DataRequest {
static func jsonObject(with data: Data?) -> Result<Any> {
guard let data = data else {
return .failure(AFError.responseValidationFailed(reason: .dataFileNil))
}
do {
let object = try JSONSerialization.jsonObject(with: data, options: JSONSerialization.ReadingOptions(rawValue: 0))
return .success(object)
} catch let error {
return .failure(error)
}
}
static func xmlObject(with data: Data?) -> Result<Any> {
guard let data = data else {
return .failure(AFError.responseValidationFailed(reason: .dataFileNil))
}
return .success(XMLParser(data: data))
}
static func imageObject(with data: Data?) -> Result<Any> {
guard let data = data,
let image = UIImage(data: data, scale: UIScreen.main.scale) else {
return .failure(AFError.responseValidationFailed(reason: .dataFileNil))
}
return .success(image)
}
}
extension DataRequest {
static var acceptableXMLContentTypes: [String] = [
"application/xml",
"text/xml",
]
static var acceptableJSONContentTypes: [String] = [
"text/json",
"text/javascript",
"application/json",
"application/hal+json",
]
static var acceptableImageContentTypes: [String] = [
"image/tiff",
"image/jpeg",
"image/gif",
"image/png",
"image/ico",
"image/x-icon",
"image/bmp",
"image/x-bmp",
"image/x-xbitmap",
"image/x-ms-bmp",
"image/x-win-bitmap",
]
static func compoundResponseSerializer() -> DataResponseSerializer<Any> {
return DataResponseSerializer { (_, response, data, error) -> Result<Any> in
if let error = error {
return .failure(error)
}
guard let mimeType = response?.mimeType else {
return .failure(AFError.responseValidationFailed(reason: .missingContentType(acceptableContentTypes: DataRequest.acceptableJSONContentTypes + DataRequest.acceptableXMLContentTypes + DataRequest.acceptableImageContentTypes)))
}
switch mimeType {
case let mimeType where DataRequest.acceptableJSONContentTypes.contains(mimeType):
return DataRequest.jsonObject(with: data)
case let mimeType where DataRequest.acceptableXMLContentTypes.contains(mimeType):
return DataRequest.xmlObject(with: data)
case let mimeType where DataRequest.acceptableImageContentTypes.contains(mimeType):
return DataRequest.imageObject(with: data)
default:
return .success(data as Any)
}
}
}
@discardableResult
func responseCompoundData(queue: DispatchQueue? = nil, completionHandler: @escaping (DataResponse<Any>) -> Void) -> Self {
return response(queue: queue, responseSerializer: DataRequest.compoundResponseSerializer(), completionHandler: completionHandler)
}
}
總結
Alamofire相比AFNetworking,無論從API的優雅程度,定位(Alamofire - 工具 VS AFNetworking - 模塊),Alamofire都更勝一籌。當然,還有一個重要的原因,那就是AFNetworking的開發人員現在都去開發Alamofire了,AFNetworking經常慢于蘋果更新速度很多,這意味著你常常需要手動Fork一份自己改,所以還是痛下決心替換成Alamofire吧。