Apple对于HTTP/2的态度也非常积极,5月HTTP/2正式发表后不久,便在紧接着6月召开的WWDC 2015大会中,向全球开发者宣布,iOS 9 开始支持HTTP/2。尽管Apple早早地宣布支持HTTP/2,但是现在整个技术圈内提及的iOS网络层架构设计还大多数停留在HTTP 1.1时代,并没有一个与时俱进的、包含HTTP/2优化的网络层架构设计策略。 对于架构设计,我曾说过,脱离业务谈架构就是纯粹的耍流氓。因此,架构的设计一定要结合当前的业务需求来进行设计和规划,并且做好一定的可扩展性,以应对未来的变化。 本文会结合当前饿了么的业务谈谈以下几个方面内容:
移动端的APP,网络层是一个几乎完全不可或缺的角色。而也正是在网络层,由于不同家的业务模型、业务结构不一样,使得网络层的架构呈现一种百家争鸣的局面。另一方面,Apple对网络层的API也是有比较好的封装;即使你不是太熟悉Apple的网络API,使用业界流行的AFNetworking或者ASIHttpRequest也是可以简化不少的操作。不过后者的作者已经多年不维护了,因此AFNetworking基本上已经成为了iOS APP的标配。 Apple在CocoaTouch层基于CFNetworking库提供的网络API有两个大类:NSURLConnection和NSURLSession。后者从iOS7开始出现,并宣称是NSURLConnection的替代者。随着时间的推移,WWDC 2015的召开也正式宣布了iOS9中NSURLConnection的deprecated标注,完成了其历史使命,也兑现了之前的承诺。 不过在实际的开发过程中,我们可以发现,尽管标注了deprecated,并不意味着NSURLConnection的库不可以继续使用;而且,由于习惯性问题,还是有很多的工程师仍旧执着于NSURLConnection所带来的熟悉味道;特别是使用AFNetworking库的工程师们,由于AFNetworking对于NSURLConnection的封装非常精美,并且可以根据业务需要自定义添加相应的依赖关系,使得其在实际应用中让人感到无比的“舒服”。 然而,鱼与熊掌总是不可兼得。WWDC 2015 Session711告诉我们,从 iOS 9 才开始支持的 HTTP / 2 协议只能在 NSURLSession 中使用。这也就意味着,要想进化到 HTTP / 2 就不得不舍弃陪伴我们多年的 NSURLConnection ,而且还要将设计网络层架构的思维方式调整到 NSURLSession 上来。 不过庆幸的是,AFNetworking从2.0开始也提供了NSURLSession的实现版本,并且相关的 API 并没有太大的变动,对于一般性的迁移还是能够轻松应对;并且,从AFNetworking 3.0开始,正式抛弃NSURLConnection,全面投入NSURLSession的怀抱。 如果在您的网络层设计中,采用了AFNetworking来降低设计的复杂性,那么正如前面提到的,由于两者在 API 方面并没有太大的差异,因此在一般的网络层迁移过程中可以平滑地过渡。而如果你在网络层设计中直接采用了原生的 API,也不需要担心,因为 NSURLSession 的 API 被设计的更加美妙,也更加易用。不过,尽管NSURLSession有非常多值得称赞的地方,但是它毕竟是一种新的设计思想和理念。因此在实际的使用过程中我们会发现很多与之前设计思维相抵触的地方,尤其是在网络依赖性处理上,就连AFNetworking也做得不是太好,这点我们在后面的章节中还会再次提到。 综合以上而言,技术的脚步永远向前,仅凭 HTTP / 2 这一点,我们相信 Apple 也一定会把重心向 NSURLSession 偏移,继续优化她,而我们也要跟上历史的车轮,是时候让迟暮的 NSURLConnection 休息了。 架构的设计总是和业务的发展相结合和适应的。在饿了么移动多款App的发展过程中,由于不同业务的差异性导致接口、协议等都有不同的需求,给多款App设计出一个拥有干净API和高度内耦合的网络层成为了一项挑战,而这个设计也将直接影响我们APP业务工程师们的开发效率。这节主要阐述理论,抛出一些问题。在下一节会给出结合饿了么多款APP的业务下所设计的网络层的解决方案。 本节我们主要讨论两点:
与业务相结合的网络层设计 先来看与业务相结合的网络层设计。 与业务相连接最紧密的地方必然是输入与输出,而网络层的功能无疑是接受输入的数据,挑选一个相应的通道组装数据发送给服务器,然后将服务器返回的数据返回给上一层,即输出数据。接下来我们从数据的角度来看看在网络层设计中需要考虑些什么样的问题。这里我们会从以下三个方面来进行阐述:
首先是输入过程。业务数据调用网络层接口时可以称之为输入,这里一般会有两种形式的设计。 第一种比较常见,很多时候会被称为集中式的API处理,即将一些经常使用的网络层调用的代码封装成一到两个函数供上层调用。上层输入相关的参数便能取得相关的回调。如以下函数: (void)networkTransferWithURLString:(NSString *)urlString andParameters:(NSDictionary *)parameters isPOST:(BOOL)isPost transferType:(NETWORK_TRANSFER_TYPE)transferType andSuccessHandler:(void (^)(id responseObject))successHandler andFailureHandler:(void (^)(NSError *error))failureHandler { // 封装AFN } 另一种形式的设计,则采用一种继承形式设计每一个API,而每一个API都对应一个类,这个类中将该API的所有参数都设定好,并提供“开始”接口和“返回”的Block,很多时候我们称这种为分布式的API处理。一个比较通用的BaseAPI可以有下列可配置项: typedef NS_ENUM(NSUInteger, DRDRequestMethodType) { DRDRequestMethodTypeGET = 0, DRDRequestMethodTypePOST = 1, DRDRequestMethodTypeHEAD = 2, DRDRequestMethodTypePUT = 3, DRDRequestMethodTypePATCH = 4, DRDRequestMethodTypeDELETE = 5 }; @interface DRDBaseAPI : NSObject @property (nonatomic, copy, nullable) NSString *baseUrl; @property (nonatomic, copy, nullable) void (^apiCompletionHandler)(_Nonnull id responseObject, NSError * _Nullable error); - (DRDRequestMethodType)apiRequestMethodType; - (DRDRequestSerializerType)apiRequestSerializerType; - (DRDResponseSerializerType)apiResponseSerializerType; - (void)start; - (void)cancel; ... @end 每一个具体的API都可以继承自这个BaseAPI,当上层业务需要进行网络调用时,实例化一个需要调用的API接口,对返回的Block进行编码,同时开启接口。如以下代码: DRDAPIPostCall *apiPost = [[DRDAPIPostCall alloc] init]; [apiPost setApiCompletionHandler:^(id responseObject, NSError * error) { }]; [apiPost start]; 这两种网络层接口的设计其实都是对应不同的业务所产生出来的思维,因此必然都有其优缺点。例如,第一种形式的接口其优点在于简单粗暴,适用于业务逻辑相对简单并且统一的RESTFUL API网络接口。但是缺点也非常明显,一旦遇上稍微复杂一些的网络接口情况,便需要在ViewController里写入大量的逻辑来达到目的。这也同时会使得原本就臃肿不堪的ViewController变得更加的庞大。 第二种的设计则优雅得多。将大量的配置逻辑都放在了另一个类文件中进行设计,ViewController中的代码会变得更轻盈一些。而将配置放在类中还有另一个好处,那便是可以增加许多平时不使用的可配置项,来增加整个网络层的可扩展性;与此同时,每一个API对应了一个不同的类的设计,又可以让不同的API可以有不同的表象,如分别遵循不同的JSON-RPC版本。 不过这种形式的设计也存在触目惊心的问题,那便是类爆炸。如果是小型的APP,则问题并不是那么明显;而如果是中大型APP的话,动则上百个API,会使得后期的维护变得有些吃力。
说完了输入问题,接下来输出的设计。在输出部分的设计中,可以说是八仙过海各显神通。 网络层的传输大多以异步加载为主,即服务器响应后由网络层来负责将数据推给上层业务线程。在iOS的体系中,也提供了很多种方式用于这种场景的处理,例如直接广播的Notification、函数回调的delegate以及最具特色的Block,都能够完成这种任务。那么采用哪种方式呢?在回答这个问题前我们先来看看这几种方式其各自的优缺点。
Notification,顾名思义的广播,其特点在于一对多地发送相关数据的通知。优点非常明显,易于实现;但缺点也很明显,会破坏整个APP架构设计中的层次结构,造成跨层的调用和处理。
Delegate,最常用的的回调方式。优点是后期易于维护且不会造成跨层的调用;缺点则是回调代码与输入的逻辑代码大部分时候不会放在一起,增加了一些后期阅读上的成本。
Block是OC语言中的特性,其优点恰好是Delegate的缺点,即它让回调的代码能够和调用的代码保持在相同位置,利于静态代码追踪和逻辑思维的延续。缺点则在于容易造成循环引用(Retain Cycle);并且对于大型APP来说,埋点这种AOP行为通常在Block中难以为继,且会造成Debug上的一些困难。 在Block的使用过程中,一定要注意使用weakSelf和strongSelf来打破循环引用。否则造成的内存泄漏会造成后期排查的困难。
也许读者看到这会更困惑了,究竟什么样的方案更佳?个人认为还是要从业务需求出发来进行设计,从我自身而言我更喜欢Block+Notification的形式,然后在适当的时候辅以Delegate完成。
数据回调的问题已经基本解决,但是新的问题也摆在了我们的面前:上层该看到怎样的数据? 在这里我们会发现非常多的应用场景,比如大多数情况下,业务层都希望返回的是与其自身相关的数据结构(Model实例),在这样的前提下能够非常地方便地对本地的数据进行相关的操作;而又比如说查询一个操作结果的是与非,那么本身数据就只有一个yes或者no,这时候采用一个数据结构来囊括便会显得复杂和臃肿;又或是网络层采取了JSON-RPC这样的协议,返回回来的信息存在大量的冗余数据,但上层业务却是若水万千只取一瓢饮。 从以上各种场景中我们可以看到,业务所需的数据形式非常多变,因此最好的方式还是交给上层自己去处理。一种常见的方法就是设定一个Delegate或者Block进行返回数据的转换,将JSON或者XML等格式转成所需要的数据格式以方便上层业务继续处理。不过我个人更倾向于在API本身就实现好这个Delegate或者Block所描述的转换函数,这样会让API的层次更加清晰。下一节我会谈到我们的处理方式。 与安全相关的网络层设计 接下来我们来看看与安全性相关的设计。其实总体来说,使用了HTTPS基本上就已经足够保证你的网络安全性了,这里我也就不一一列举其好处。事实上国外多数的大公司以及国内的BAT几乎都已经是全站HTTPS了,免费的SSL证书的申请难度也在不断降低,门槛上已经可以说是没有门槛。因此为了站点的安全性,上HTTPS吧。 不过,HTTPS如果使用不当仍然会存在一些的小缺陷,MITMA(Man-in-the-middle attack)攻击便是其中的一种。尝试这样一种情况,使用Charles这样的抓包工具来抓取HTTPS的包,Charles会让我们去安装它自己颁发的根证书。一旦我们选择和信任了这个根证书,我们会发现Charles能够顺利地显示整个HTTPS通信的情况了。 对于这种中间人攻击,目前一般的解决方案即采取SSL Pinning,即将服务器的公钥证书与整个APP打包在一起发出,然后在网络请求时候将服务器发送过来的证书与本地证书进行比较,从而避免中间人攻击的可能性。关于这部分的设计,AFNetworking已经有相关的实现了,我在《正确使用AFNetworking的SSL保证网络安全》有过详细阐述,这里就不再赘述了。 说完了理论,现在结合实际来谈谈我们的解决方案。 我们要使用HTTP/2,那么在网络库的选择上必然需要使用NSURLSession来达到目的,并且我们也不希望自己去实现序列化以及RESTFUL的复杂性,因此AFNetworking3.0成了一个比较不错的选择。但是似乎仅仅有这些还不够。接下来会分为以下几个部分来谈谈我们的解决方案:
业务协议 从业务协议上来说,饿了么众多APP中,每款APP都有其自身的特点,例如有些采取RESTFUL的设计,也有采用JSON-RPC的设计来达到业务目的。这时候如果采取集中式的API设计,相对应JSON-RPC会产生大量的RPC协议封装代码。并且对于不同版本、类型的RPC协议,需要有不同的集中函数或者增加大量的参数来处理其中的差异性。 如果采取分布式的API设计,则可以将这部分协议代码放进API自身类中来进行处理。在这里,我设计了一个RPCProtocol,由业务方自己来定义所需要遵循的业务RPC标准。而每个API都保存一个rpcDelegate字段来自定义自己的上层协议,而如果为空时,即代表着不进行RPC封装而是直接发送,从而达到JSON-RPC和RESTFUL在一个APP共存的目的;并且由于每个API都可以指定不同的rpcDelegate,因此可以适用于服务器端不同的RPC版本兼容性。这里,RPCProtocol会有一些这样的阐述: NS_ASSUME_NONNULL_BEGIN @protocol DRDRPCProtocol |
|
声明:文章版权归原作者所有 部分文章转自互联网 如有侵权请联系
[邮箱地址] 删除
|