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的話,404,500Alamofire都會認為請求成功了,而不會返回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在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吧。

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。