背景
最近项目上完成了从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吧。