基于 Rx 的网络层实践

作者:李富强投稿发布于本专栏

日常开发中,API请求的管理是我们无法回避的一个难题,相对于web,客户端的网络请求处理要更加复杂,例如异步、通用参数、数据模型化、通用错误处理、可取消。更高一些的用户体验要求是,不阻塞用户的操作,随时可以退出以及重试,这也就要求我们对网络请求的各个状态处理都要非常完善,这就需要一个非常完善的网络层提供支撑。

网络框架

Apple 在 iOS SDK 中是为我们提供了 HTTP 网络请求接口的,从 CFNetwork 到 NSURLConnection,以及最近的 NSURLSession,但这些框架我们很少直接去使用。因为官方的 SDK 只提供了最基础的接口,而我们实际业务开发中,网络层需要提供更多的功能,才能提高业务开发效率。下面简单举几个例子说明一下为什么需要框架。

第一,HTTP 参数的处理,例如GET的请求参数在经过 URL Encoding 之后添加 URL 的 query 中,POST 请求的包体组装( multi-part 或者 form ),都是比较繁琐但是相对通用的逻辑,这些都应该在网络层处理之后,让业务层可以无感知地去使用。

第二,返回数据的处理,例如,我们指定服务器返回 JSON 数据,框架会把这些信息处理添加到 HTTP 协议中,我们的业务层逻辑就会很简单,获得可以序列化的数据或者直接报错。

第三,请求状态的管理,例如,限制请求最大并发数、网络请求的取消等。如果使用 NSURLConnection 。还有一个比较经典的示例,就是 NSURLConnection 需要 runloop 机制的支持才能处理回调,如果你启动一个线程,但是没有启动它的 runloop,你在这个线程中发起网络请求的话,这个请求不会产生回调。如果我们把所有的 NSURLConnection 都放到主线程中发起,这会产生一个其他问题,比如主线程的负担过重。

由于这些需求的存在,iOS 开发中诞生了几乎是事实标准的网络框架,早期的 ASIHTTPRequest,这个框架是基于 CFNetwok 直接实现的,逻辑非常复杂,从我开始使用到结束使用,也只是看懂了大概的架构,但是细节一直没有深入探究。ASI 使用底层技术的好处在于性能的确较高。相对于 AFNetworking ,ASI 提供了更多的功能支持,例如大文件下载、同步请求。后续因为作者不再维护,同时暴露出了一些安全问题,以及 AFNetworking 的流行,慢慢大家就不再使用了。Objective-C 时代,AFNetworking 几乎是事实上的网络框架SDK,我个人感觉,最主要的原因是因为简单,AFN 2.x 中 NSURLConnection + runloop、NSOperation、HTTP 参数的处理等,都是非常优雅的代码,建议每个 iOS 开发人员都应该看看。

我们上面提到的一些网络层的需求,AFNetworking 都帮我们进行了处理,但是一般来说,在 AFNetworking 这类网络框架的基础上,大家往往还会再做一层封装,一方面适应自己的业务需求,另外一方面是考虑到后续框架的变迁而隐藏底层网络框架,比如 AFNetworking 从 2.x 到 3.x 的变迁等。我个人的封装方式是定义一个 Request 类,隐藏底层的 NSURLSessionTask 或者 NSOperation。例如:

  • class CusomRequest {
  • var task: URLSessionTask?
  • let url: URL
  • init(method: Method, url: URL, parameters: [String: Any]? = nil, headers: [String: Any]? = nil) {
  • // ...
  • }
  • func start(callback: CompletionClosure) {
  • // ...
  • }
  • func cancel() {
  • }
  • }

Rx

当Swift第一版本发布的时候,我快速学习了一下,但是非常失望,感觉这是一个半成品,很多基本的特性都无法支持。后续断断续续写了一些小东西,Swift 3.0 之后,终于感觉比较成熟。公司内部的项目,由于基础库庞大、包大小的限制等原因,一直无法实践 Swift 。但是我业余的 night job 基本上都在使用 Swift,在做小东西的过程中,也需要设计网络层,当我去探索的时候,发现了 RxAlamofire 这个效率极高的框架,把 RxSwift 和 Alamofire 结合到了一起。

简单介绍一下我对 Reactive Programming 的理解,最开始接触这个概念是从ReactiveCocoa 开始的,后续也经常使用,感觉非常方便。虽然 ReactiveCocoa 也有 Swift 版本,但是当我开始阅读 Rx 官方的文档之后,就果断选择了 RxSwift 替代 ReactiveCocoa,我个人的理解有三方面:

  • 首先,我认为 Rx 的文档质量非常优秀,与学习 ReactiveCocoa 时资料匮乏形成鲜明对比,大家只需要看官网的文档就能无障碍理解和使用。
  • 其次,我认为 RxSwift 的概念上更好理解,Observable 与 Observer 的关系非常清晰,Obervable 生产 data flow,经过各种 Operators 变化,最终提供给Observer 消费。官网的一段话: In ReactiveX an observer subscribes to an Observable.
  • 最后,Rx 支持很多语言,并且概念上都是一致的,让我们可以做到 Learn Once, Write Everywhere,我自己尝试过 RxJava,除了一些语法上不一致,其他概念理解起来毫无障碍。甚至 RxJS 在前端领域也越来越流行,这个诱惑很致命的。

我认为对 Rx 设计思想最好的解释是 Rx 官网 (http://reactivex.io/) 的一段话:ReactiveX is a combination of the best ideas from the Observer pattern, the Iterator pattern, and functional programming。三种设计模式或者思想,观察者模式,迭代器模式,函数式编程。

针对 Rx,我个人的观点是 Rx 抹平了调用接口,无论是进行网络请求,还是定位,甚至 UI 变化,业务层不再需要了解API的具体调用方式,只要服务层封装到位,对业务层而言,只用获取一个 Observable 处理即可,这就是我认为的最大的优势,再加上比较通用化的 Operators,更能简化客户端复杂的数据流操作。举个简单的例子,定位完成之后,发起多个网络请求,请求全部成功之后,页面数据才能显示,中间任何一个步骤出现错误都会中断。如果使用 Rx,你会发现这些问题可以大大简化,而使用异步回调,会需要非常多的变量来辅助处理。

网络层

解决的问题

选定网络框架之后,就需要搭建自己项目的网络层实现。在网络库的基础上,我们之所以为项目再设定一个网络层,除了我们之前提到的隐藏底层网络框架细节之外,还主要为了解决几个问题。首先,通用参数,例如设备唯一标识、登录用户 token 信息、反爬虫签名、设备的基本信息等。

其次, 统一的错误处理,例如底层网络 HTTP 发生错误、JSON 解析或者数据格式产生错误等,我们可以与 PM 约定较为用户友好的提示,在网络层统一处理。否则业务层在处理网络数据的时都需要再做重复这样的逻辑,有洁癖的人应该都无法这种 copy-paste 模式的重复代码。

最后,统一的业务数据处理。一般来说,API 服务器返回的 JSON 数据都有通用的结构。例如

  • {
  • "code": 200,
  • "message": "请求已成功提交",
  • "data": {
  • "id": 111,
  • "nick_name": "大强",
  • "avatar": "http://xxx.png"
  • }
  • }

假设两端服务器与客户端约定,code 等于200表示业务处理成功,其他值表示错误码,那我们就可以在网络层判断 code 值,如果不等于正常值,直接返回 error,业务层的重复逻辑就会再次减少。同时,服务器返回的数据都在 data 字段,在确认 code 等于200,即业务处理成功之后,我们可以把模型化逻辑也在网络层统一处理。经过这两个逻辑的处理,业务层的逻辑进一步简化,失败返回 error,成功返回数据模型。

实现

我们最终的目的是业务层接口尽量简单,处理的逻辑尽可能少,先看看我们最终要达到的效果是什么,个人主页请求用户信息,我会在代码注释中为大家简单解释一下这段代码:

  • /// 网络层
  • extension Network{
  • func userDetail(userid: Int) -> Observable<User> {
  • let url = URL(string: baseUrl + "/user/detail.do")!
  • let parameters: [String: Any] = ["userid": userid]
  • return rx_json(.get, url, parameters: parameters).data()
  • }
  • }
  • /// 业务层
  • Network.default.userDetail(userid: self.userId)
  • .subscribe(
  • onNext:{ [weak self] (user) in
  • /// 处理正常返回的业务数据
  • guard let `self` = self else {return}
  • self.user = user
  • self.refreshUI()
  • },
  • onError: { [weak self] (error) in
  • /// 处理错误信息
  • Toast(text: error.localizedDescription).show()
  • self?.navigationController?.popViewController(animated: true)
  • }
  • )
  • /// 假设网络请求耗,而用户此时退出页面,这行代码会把请求cancel,避免网络资源的浪费
  • .addDisposableTo(disposeBag)

看完了最终效果,我们开始架构我们的整个网络框架,首先是 Network 对象,用于管理网络请求的单例对象。下面这段代码解决了上面提到的网络层的第一个问题,通用参数的处理。

```
class Network {

  • /// 单例
  • static let `default`: Network = {
  • return Network()
  • }()
  • // 主机地址
  • let baseUrl = "http://example.com"
  • /// JSON解析的通用key值
  • static let ok = 200
  • static let codeKey = "code"
  • static let messageKey = "message"
  • static let dataKey = "data"
  • static let cursorKey = "cursor"
  • /// 添加通用参数
  • func commonParameters(parameters: [String: Any]?) -> [String: Any] {
  • var newParameters: [String: Any] = [:]
  • if parameters != nil {
  • newParameters = parameters!
  • }
  • /// 添加通用参数
  • /// 用户token参数
  • if let token = AccountCenter.default.user?.token {
  • newParameters["token"] = token
  • }
  • /// App版本信息
  • if let version = Bundle.main.object(forInfoDictionaryKey: "CFBundleShortVersionString") {
  • newParameters["app_version"] = version
  • }
  • return newParameters
  • }
  • /// 统一的API请求入口
  • func rx_json(_ method: Alamofire.HTTPMethod,
  • _ url: URLConvertible,
  • parameters: [String: Any]? = nil,
  • encoding: ParameterEncoding = URLEncoding.default,
  • headers: [String: String]? = nil)
  • -> Observable<JSON> {
  • return string(
  • method,
  • url,
  • parameters: commonParameters(parameters: parameters),
  • encoding: encoding,
top Created with Sketch.