AFNetworking 迁移 Alamofire

背景

最近项目上完成了从AFNetworkingAlamofire的迁移,此为背景。

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.2Xcode 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的话,404500Alamofire都会认为请求成功了,而不会返回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
}

也是不会在400404500的时候得到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在200300之间,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吧。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 205,033评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 87,725评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 151,473评论 0 338
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,846评论 1 277
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,848评论 5 368
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,691评论 1 282
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,053评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,700评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 42,856评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,676评论 2 323
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,787评论 1 333
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,430评论 4 321
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,034评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,990评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,218评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,174评论 2 352
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,526评论 2 343