对比 iOS 网络组件:AFNetworking VS ASIHTTPRequest

阅读数:43662 2013 年 2 月 28 日

话题:iOS语言 & 开发

在开发 iOS 应用过程中,如何高效的与服务端 API 进行数据交换,是一个常见问题。一般开发者都会选择一个第三方的网络组件作为服务,以提高开发效率和稳定性。这些组件把复杂的网络底层操作封装成友好的类和方法,并且加入异常处理等。

那么,大家最常用的组件是什么?这些组件是如何提升开发效率和稳定性的?哪一款组件适合自己,是 AFNetworking(AFN)还是 ASIHTTPRequest(ASI)?几乎每一个 iOS 互联网应用开发者都会面对这样的选择题,要从这两个最常用的组件里选出一个好的还真不是那么容易。

单单从两个控件版本提交的时间节点来看,AFN 的第一个提交是 2011 年的 1 月 1 日,那个时候 ASI 早已是 1.8+ 的版本了;而当 AFN 发布 1.0 版,2012 年 10 月份的时候,ASI 早早的已经停止更新了。这样看起来,AFN 是 ASI 的继任者,似乎不存在之前提到的选择困难的问题,而事实并非如此。本文将从用法、功能、性能和原理几个方面对二者进行简单对比,看看二者之间到底存在着怎样的区别,到底应该如何选择。

1、用法对比

首先,从推荐用法上就可以看出二者设计理念上大有不同。

图 1,AFN 的示例代码,发起请求(出自:Posts.m

AFN 官方推荐的使用方法是,为一系列相关的请求定义一个HTTPClient,共用一个 BaseURL。每次请求把 URL 中除 BaseURL 的 Path 部分做为参数传给 HTTPClient 的静态方法,并注册一个 Block 用于回调。

图 2,ASI 示例代码,发起异步请求(出自:ASIHTTPRequestTests.m

ASI 推荐使用方法就非常传统,每一个请求都由构造方法初始化一个(共享)实例,通过这个实例配置参数并发起请求。ASI 最初使用 delegate 模式回调,在 iOS SDK 支持 Block 之后也提供了注册 Block 的实例方法。

以上引用的两段代码都出自各自项目的示例工程。对比两段代码可以很清楚的看出,同样是发起一个最普通的异步请求,使用 AFN 只需要调用一个静态方法,但代码可读性较差;而 ASI 的示例看起来更清晰,但需要调用多个实例方法才能完成一次请求。AFN 的设计更加工程化,或者说对使用者更友好,而 ASI 的设计更经典,典型的 OOP。

除了初级用法上的区别,二者的高级功能和对扩展的支持也颇有不同。

2、高级功能

AFN 只封装了一些常用功能,满足基本需求,而直接忽略了很多扩展功能。例如:AFN 默认没有封装同步请求,如果开发者需要使用同步请求,则需要重写 getPath:parameters:success:failure 方法,对 AFHTTPRequestOperation 进行同步处理;而 ASI 则是直接通过调用一个 startSynchronous 方法。

此外 AFN 针对JSONXMLPListImage四种数据结构封装了各自处理器,开发者可以把处理器注册到操作队列中,直接在回调方法中获得格式化以后的数据。在示例工程中就使用了 JSON 处理器:把 AFJSONRequestOperation 注册到操作队列里。

图 3,AFN 示例代码,初始化自定义的 HTTPClient(出自:AFAppDotNetAPIClient.m

而 ASI 在这方面显得更原始,没有针对任何数据类型做特别封装,只是预留了各种接口和工具供开发者自行扩展。ASI 比 AFN 提供更多扩展功能还有一个原因,它把许多内部用到的功能也抽象成类和方法。例如:

ASIHTTPRequestDataCompressor和 ASIHTTPRequestDataDecompressor 两个类,只用于压缩本地文件,构造 POST Body 和解压缩返回数据,但这两个类仍然被设计为独立功能,提供了对多种数据结构进行压缩和解压缩的方法。

对比二者的高级功能和对扩展的支持后,可以看出 AFN 把初级功能(或者叫常用功能)做到了 90 分。调用方式够简单,处理器够丰富,使用者用起来可以算是轻松加愉快。但它放弃了对高级功能的支持,要满足较复杂的需求,就要大费周折了,在这方面最多只有 40 分。而 ASI 显然不满足于做好初级功能,但为了提供更丰富的可扩展接口,导致初级功能用起来也要花上一些力气。虽然 ASI 单独提供了支持Amazon S3Rackspace Cloud Files的控件,但对于生在红旗下的我朝开发者来说基本没用,所以在初级功能的支持上 ASI 能得个 70 分,牺牲了初级功能的易用性,换来的是良好的扩展性,在高级功能的使用上远远好于 AFN,也能得个 70 分。

从使用角度对比过后,基本上对这两个项目有一个整体上的认识,再深入下去看看二者的性能如何。

3、性能对比

我分别用 AFN 和 ASI 进行了测试,测试环境如下:iPhone5,联通 3G 信号全满,室内静止状态,请求国内双线机房独立服务器的静态文件,1~20K 共 20 个文件,每个文件请求 20 次,记录从创建请求到完全下载文件的耗时,结果如下:

图 4,AFN 连续访问 1 ~ 20K 文件耗时

图 5,ASI 连续访问 1 ~ 20K 文件耗时

图 4 是 AFN 的记录图,绿色为 20 次请求中耗时最久的一次,蓝色为耗时最短的一次,黄色为去除最大值和最小值的 18 次平均值。从这个图可以看出,AFN 最开始创建对象耗时近 2.5 秒,随后稳定下来,在 3K、7K、15K 和 20K 时出现了抖动。图 5 是 ASI 做相同测试的结果,首次创建对象近 2.25 秒,略优于 AFN,同样在 5K、11K、13K、14K 和 16K 发生了一些抖动,但抖动幅度似乎小于 AFN,可见稳定性更好一些。

下边是把二者的测试结果放在一起的对比图,可以更直观的比较二者的区别。

图 6,ASI 和 AFN 耗时最大值对比

图 6 的最大值对比可以更明显的看出二者的抖动对比,ASI 略好一些。

图 7,ASI 和 AFN 耗时最小值对比

图 7 的最小值对比可以看出,在每一个大小的测试中 ASI 的最佳性能似乎都要优于 AFN。

图 8,ASI 和 AFN 耗时平均值对比

图 8 是耗时平均值的对比,更能够说明问题。文件小于 12K 的测试中 ASI 的性能优势并没有非常明显,超过 12K 以后,ASI 优势开始明显起来,每一次请求都要比 AFN 节约 20% ~ 30%,近 0.1 秒。同时从这张图上还可以看出,随着下载文件变大,请求耗时并不是线形增长的,这是由于一次请求大部分时间都消耗在建立连接上,而真正接收数据只占用了极少时间,这个问题不在本篇文章的讨论范围,所以不多说,有兴趣的读者可以移步http://segmentfault.com/t/ios进一步讨论。

4、原理分析

ASI 的性能似乎全面优于 AFN,那下边从二者的实现原理上看一下到底是什么原因造成这种差距。ASI 基于 CFNetwork 框架开发,而 AFN 基于 NSURL,底层的区别是导致二者性能差距的重要原因之一。

图 9,ASI 和 AFN 以及底层框架的关系

我们知道所有网络通信的基础是 Socket,一个 Socket 与另一个连接并传送数据。BSD Socket 是一类最常见的 Socket 抽象接口。

Core Foundation 框架中的 CFSocket 就是基于 BSD Socket 开发的。它几乎涵盖了 BSD Socket 的全部功能,更重要的是把 Socket 整合到事件的处理循环中。Core Founda-tion 中较高层的 CFStream 是基于 CFSocket 开发的读写流支持。

CFNetwork 是基于 Core Foundation 中 CFStream 的一个底层高性能网络框架,它由提供基础服务的 CFSocketStream,支持 HTTP 协议的 CFHTTP,基于 CFHTTP 用于身份认证的 CFHTTPAuthentication 和支持 FTP 协议的 CFFTP 组成。

正如图 9 所示,ASI 是基于 CFHTTP 开发的一个组件;而 AFN 的基础——NSURL,也是基于 CFNetwork 开发的。也就是说 ASI 相比 AFN 更加底层,这就从一定程度上造成二者的性能差距。

另一个方面,虽然二者都使用 NSOperation 和 NSOperationQueue 实现但底层的区别也导致实现方式上有非常大的差别。

ASI 的直接操作对象 ASIHTTPRequest 是 NSOperation 的子类,实现了 NSCopying 协议。在 initialize 和 initWithURL: 方法中初始化相关属性并配置一系列请求相关参数默认值。此外,ASIHTTPRequest 还提供了一系列的实例方法用来配置请求对象。在异步请求的处理上,ASIHTTPRequest 对象初始化结束后,在 startAsynchronous 方法中把对象加入共享操作队列。此后,包括创建 CFHTTPMessageRef,也就是处理网络请求的主要对象(事实上是一个指向 __CFHTTPMessage 结构的指针),在内的所有操作都在 ASIHTTPRequest 对象所属的子线程中完成。

AFN 的直接操作对象 AFHTTPClient 不同于 ASI,是一个实现了 NSCoding 和 NSCopying 协议的 NSObject 子类。AFHTTPClient 是一个封装了一系列操作方法的“工具类”,处理请求的操作类是一系列单独的,基于 NSOperation 封装的,AFURLConnectionOperation 的子类。AFN 的示例代码中通过一个静态方法,使用 dispatch_once() 的方式创建 AFHTTPClient 的共享实例,这也是官方建议的使用方法。在创建 AFHTTPClient 的初始化方法中,创建了 OperationQueue 并设置一系列参数默认值。在 getPath:parameters:success:failure 方法中创建 NSURLRequest,以 NSURLRequest 对象实例作为参数,创建一个 NSOperation,并加入在初始化发方中创建的 NSOperationQueue。以上操作都是在主线程中完成的。在 NSOperation 的 start 方法中,以此前创建的 NSURLRequest 对象为参数创建 NSURLConnection 并开启连结。

在异步回调的处理上二者也有区别,ASI 采取的是 CFHTTP 请求完成,直接回调 ASIHTTPRequest 的实例方法,通过储存的实例对象记录的信息完成 Delegate 模式或 Block 模式的回调。而 AFN 则直接使用了 NSOperation 的 completionBlock 属性。

这些实现方式也可以看出,ASI 显得更加底层,并没有过多使用 Cocoa 框架中已经封装的 API,而 AFN 则更加实用主义,逻辑简单清晰,大量使用了框架 API。这一点也是造成二者性能差别的原因之一。

总结

通过以上的对比,基本可以这样评价:AFN 适合逻辑简单的应用,或者更适合开发资源尚不丰富的团队,因为 AFN 的易用性要比 ASI 好很多,而这样的应用(或团队)对底层网络控件的定制化要求也非常低。ASI 更适合已经发展了一段时间的应用,或者开发资源相对丰富的团队,因为往往这些团队(或他们的应用)已经积累了一定的经验,无论是产品上还是技术上的。需求复杂度就是在这种时候高起来,而且底层订制的需求也越来越多,此时 AFN 就很难满足需求,需要牺牲一定的易用性,使用 ASI 作为网络底层控件。SegmentFault 开源客户端现在被设计为一款简单的阅读客户端,几乎没有定制要求,因此,目前我选择了 AFN 作为网络控件。

以上对 ASI 和 AFN 两款最常用的 iOS 底层网络控件做了初步的介绍,要更深入的了解两款控件,还需要大家继续研究各自的源码。大家遇到任何关于 iOS 的技术问题都可以在http://segmentfault.com/t/ios进行讨论。另外大家也可以持续关注SegmentFault 的开源客户端,与更多的开发者共同探讨 iOS 开发技术。

作者简介

高嘉峻(微博:@gaosboy),SegmentFault.com 联合创始人,杭州 iOS 开发者沙龙发起人,资深 iOS 开发者。