iOS应用架构谈(三):网络层设计方案(上)

jopen 9年前

编者按:iOS客户端应用架构看似简单,但实际上要考虑的事情不少。本文作者将以系列文章的形式来讨论iOS应用架构中的种种问题,本文是其中的第三篇,主要讲网络层设计以及安全机制和优化方案。

前言

网络层在一个App中也是一个不可缺少的部分,工程师们在网络层能够发挥的空间也比较大。另外,苹果对网络请求部分已经做了很好的封装,业界的 AFNetworking也被广泛使用。其它的ASIHttpRequest,MKNetworkKit啥的其实也都还不错,但前者已经弃坑,后者也在弃 坑的边缘。在实际的App开发中,Afnetworking已经成为了事实上各大App的标准配置。

网络层在一个App中承载了API调用,用户操作日志记录,甚至是即时通讯等任务。我接触过一些App(开源的和不开源的)的代码,在看到 网络层这一块时,尤其是在看到各位架构师各显神通展示了各种技巧,我非常为之感到兴奋。但有的时候,往往也对于其中的一些缺陷感到失望。

关于网络层的设计方案会有很多,需要权衡的地方也会有很多,甚至于争议的地方都会有很多。但无论如何,我都不会对这些问题做出任何逃避,我会在这篇文章中给出我对它们的看法和解决方案,观点绝不中立,不会跟大家打太极。

这篇文章就主要会讲这些方面:

  1. 网络层跟业务对接部分的设计
  2. 网络层的安全机制实现
  3. 网络层的优化方案

网络层跟业务对接部分的设计

在安居客App的架构更新换代的时候,我深深地感觉到网络层跟业务对接部分的设计有多么重要,因此我对它做的最大改变就是针对网络层跟业务对接部分的改变。网络层跟业务层对接部分设计的好坏,会直接影响到业务工程师实现功能时的心情。

在正式开始讲设计之前,我们要先讨论几个问题:

  1. 使用哪种交互模式来跟业务层做对接?
  2. 是否有必要将API返回的数据封装成对象然后再交付给业务层?
  3. 使用集约化调用方式还是离散型调用方式去调用API?

这些问题讨论完毕之后,我会给出一个完整的设计方案来给大家做参考,设计方案是鱼,讨论的这些问题是渔,我什么都授了,大家各取所需。

使用哪种交互模式来跟业务层做对接?

这里其实有两个问题:一,以什么方式将数据交付给业务层?二,交付什么样的数据给业务层?

以什么方式将数据交付给业务层?

iOS开发领域有很多对象间数据的传递方式,我看到的大多数App在网络层所采用的方案主要集中于这三种:Delegate,Notification,Block。KVO和Target-Action我目前还没有看到有使用的。

目前我知道边锋主要是采用的block,大智慧主要采用的是Notification,安居客早期以Block为主,后面改成了以 Delegate为主,阿里没发现有通过Notification来做数据传递的地方(可能有),Delegate、Block以及target- action都有,阿里iOS App网络层的作者说这是为了方便业务层选择自己合适的方法去使用。这里大家都是各显神通,每次我看到这部分的时候,我都喜欢问作者为什么采用这种交互方 案,但很少有作者能够说出个条条框框来。

然而在我这边,我的意见是以Delegate为主,Notification为辅。原因如下:

  • 尽可能减少跨层数据交流的可能,限制耦合
  • 统一回调方法,便于调试和维护
  • 在跟业务层对接的部分只采用一种对接手段(在我这儿就是只采用delegate这一个手段)限制灵活性,以此来交换应用的可维护性

尽可能减少跨层数据交流的可能,限制耦合

什么叫跨层数据交流?就是某一层(或模块)跟另外的与之没有直接对接关系的层(或模块)产生了数据交换。为什么这种情况不好?严格来说应该 是大部分情况都不好,有的时候跨层数据交流确实也是一种需求。之所以说不好的地方在于,它会导致代码混乱,破坏模块的封装性。我们在做分层架构的目的其中 之一就在于下层对上层有一次抽象,让上层可以不必关心下层细节而执行自己的业务。

所以,如果下层细节被跨层暴露,一方面你很容易因此失去邻层对这个暴露细节的保护;另一方面,你又不可能不去处理这个细节,所以处理细节的相关代码就会散落各地,最终难以维护。

说得具象一点就是,我们考虑这样一种情况:A<-B<-C。当C有什么事件,通过某种方式告知B,然后B执行相应的逻辑。一旦 告知方式不合理,让A有了跨层知道C的事件的可能,你 就很难保证A层业务工程师在将来不会对这个细节作处理。一旦业务工程师在A层产生处理操作,有可能是补充逻辑,也有可能是执行业务,那么这个细节的相关处 理代码就会有一部分散落在A层。然而前者是不应该散落在A层的,后者有可能是需求。另外,因为B层是对A层抽象的,执行补充逻辑的时候,有可能和B层针对 这个事件的处理逻辑产生冲突,这是我们很不希望看到的。

那么什么情况跨层数据交流会成为需求?在网络层这边,信号从2G变成3G变成4G变成Wi-Fi,这个是跨层数据交流的其中一个需求。不过其他的跨层数据交流需求我暂时也想不到了,哈哈,应该也就这一个吧。

严格来说,使用Notification来进行网络层和业务层之间数据的交换,并不代表这一定就是跨层数据交流,但是使用 Notification给跨层数据交流开了一道口子,因为Notification的影响面不可控制,只要存在实例就存在被影响的可能。另外,这也会导 致谁都不能保证相关处理代码就在唯一的那个地方,进而带来维护灾难。作为架构师,在这里给业务工程师限制其操作的灵活性是必要的。另 外,Notification也支持一对多的情况,这也给代码散落提供了条件。同时,Notification所对应的响应方法很难在编译层面作限制,不 同的业务工程师会给他取不同的名字,这也会给代码的可维护性带来灾难。

手机淘宝架构组的侠武同学曾经给我分享过一个问题,在这里我也分享给大家:曾经有一个工程师在监听Notification之后,没有写释 放监听的代码,当然,找到这个原因又是很漫长的一段故事,现在找到原因了,然而监听这个Notification的对象有那么多,不知道具体是哪个 Notificaiton,也不知道那个没释放监听的对象是谁。后来折腾了很久大家都没办法的时候,有一个经验丰富的工程师提出用hook(Method Swizzling)的方式,最终找到了那个没释放监听的对象,bug修复了。

我分享这个问题的目的并不是想强调Notification多么多么不好,Notification本身就是一种设计模式,在属于它的问题 领域内,Notification是非常好的一种解决方案。但我想强调的是,对于网络层这个问题领域内来看,架构师首先一定要限制代码的影响范围,在能用 影响范围小的方案的时候就尽量采用这种小的方案,否则将来要是有什么奇怪需求或者出了什么小问题,维护起来就非常麻烦。因此Notification这个 方案不能作为首选方案,只能作为备选。

那么Notification也不是完全不能使用,当需求要求跨层时,我们就可以使用Notification,比如前面提到的网络条件切换,而且这个需求也是需要满足一对多的。

所以,为了符合前面所说的这些要求,使用Delegate能够很好地避免跨层访问,同时限制了响应代码的形式,相比Notification而言有更好的可维护性。

然后我们顺便来说说为什么尽量不要用block

1. block很难追踪,难以维护

我们在调试的时候经常会单步追踪到某一个地方之后,发现尼玛这里有个block,如果想知道这个block里面都做了些什么事情,这时候就比较蛋疼了。

- (void)someFunctionWithBlock:(SomeBlock *)block  {      ... ...   -> block();  //当你单步走到这儿的时候,要想知道block里面都做了哪些事情的话,就很麻烦。      ... ...  }

2. block会延长相关对象的生命周期

block会给内部所有的对象引用计数加一,这一方面会带来潜在的retain cycle,不过我们可以通过Weak Self的手段解决。另一方面比较重要就是,它会延长对象的生命周期。

在网络回调中使用block,是block导致对象生命周期被延长的其中一个场合,当ViewController从window中卸下 时,如果尚有请求带着block在外面飞,然后block里面引用了ViewController(这种场合非常常见),那么 ViewController是不能被及时回收的,即便你已经取消了请求,那也还是必须得等到请求着陆之后才能被回收。

然而使用delegate就不会有这样的问题,delegate是弱引用,哪怕请求仍然在外面飞,,ViewController还是能够及时被回收的,回收之后指针自动被置为了nil,无伤大雅。

所以平时尽量不要滥用block,尤其是在网络层这里。

3. 统一回调方法,便于调试和维护

前面讲的是跨层问题,区分了Delegate和Notification,顺带谈了一下Block。然后现在谈到的这个情况,就是另一个采 用Block方案不是很合适的情况。首先,Block本身无好坏对错之分,只有合适不合适。在这一节要讲的情况里,Block无法做到回调方法的统一,调 试和维护的时候也很难在调用栈上显示出来,找的时候会很蛋疼。

在网络请求和网络层接受请求的地方时,使用Block没问题。但是在获得数据交给业务方时,最好还是通过Delegate去通知到业务方。 因为Block所包含的回调代码跟调用逻辑放在同一个地方,会导致那部分代码变得很长,因为这里面包括了调用前和调用后的逻辑。从另一个角度说,这在一定 程度上违背了single function,single task的原则,在需要调用API的地方,就只要写API调用相关的代码,在回调的地方,写回调的代码。

然后我看到大部分App里,当业务工程师写代码写到这边的时候,也意识到了这个问题。因此他们会在block里面写个一句话的方法接收参数,然后做转发,然后就可以把这个方法放在其他地方了,绕过了Block的回调着陆点不统一的情况。比如这样:

  [API callApiWithParam:param successed:^(Response *response){          [self successedWithResponse:response];      } failed:^(Request *request, NSError *error){          [self failedWithRequest:request error:error];      }];

这实质上跟使用Delegate的手段没有什么区别,只是绕了一下,不过还是没有解决统一回调方法的问题,因为block里面写的方法名字可能在不 同的ViewController对象中都会不一样,毕竟业务工程师也是很多人,各人有各人的想法。所以架构师在这边不要贪图方便,还是使用 delegate的手段吧,业务工程师那边就能不用那么绕了。Block是目前大部分第三方网络库都采用的方式,因为在发送请求的那一部分,使用 Block能够比较简洁,因此在请求那一层是没有问题的,只是在交换数据之后,还是转变成delegate比较好,比如AFNetworking里面:

  [AFNetworkingAPI callApiWithParam:self.param successed:^(Response *response){          if ([self.delegate respondsToSelector:@selector(successWithResponse:)]) {              [self.delegate successedWithResponse:response];          }      } failed:^(Request *request, NSError *error){          if ([self.delegate respondsToSelector:@selector(failedWithResponse:)]) {              [self failedWithRequest:request error:error];          }      }];

这样在业务方这边回调函数就能够比较统一,便于维护。

综上,对于以什么方式将数据交付给业务层?这个问题的回答是这样:

尽可能通过Delegate的回调方式交付数据,这样可以避免不必要的跨层访问。当出现跨层访问的需求时(比如信号类型切换),通过Notification的方式交付数据。正常情况下应该是避免使用Block的。

交付什么样的数据给业务层?

我见过非常多的App的网络层在拿到JSON数据之后,会将数据转变成对应的对象原型。注意,我这里指的不是NSDictionary,而 是类似Item这样的对象。这种做法是能够提高后续操作代码的可读性的。在比较直觉的思路里面,是需要这部分转化过程的,但这部分转化过程的成本是很大 的,主要成本在于:

  1. 数组内容的转化成本较高:数组里面每项都要转化成Item对象,如果Item对象中还有类似数组,就很头疼。
  2. 转化之后的数据在大部分情况是不能直接被展示的,为了能够被展示,还需要第二次转化。
  3. 只有在API返回的数据高度标准化时,这些对象原型(Item)的可复用程度才高,否则容易出现类型爆炸,提高维护成本。
  4. 调试时通过对象原型查看数据内容不如直接通过NSDictionary/NSArray直观。
  5. 同一API的数据被不同View展示时,难以控制数据转化的代码,它们有可能会散落在任何需要的地方。

其实我们的理想情况是希望API的数据下发之后就能够直接被View所展示。首先要说的是,这种情况非常少。另外,这种做法使得View和API联系紧密,也是我们不希望发生的。

在设计安居客的网络层数据交付这部分时,我添加了reformer(名字而已,叫什么都好)这个对象用于封装数据转化的逻辑,这个对象是一 个独立对象,事实上,它是作为Adaptor模式存在的。我们可以这么理解:想象一下我们洗澡时候使用的莲蓬头,水管里出来的水是API下发的原始数据。 reformer就是莲蓬头上的不同水流挡板,需要什么模式,就拨到什么模式。

在实际使用时,代码观感是这样的:

先定义一个protocol:  @protocol ReformerProtocol   - (NSDictionary)reformDataWithManager:(APIManager *)manager;  @end  在Controller里是这样:  @property (nonatomic, strong) id<ReformerProtocol> XXXReformer;  @property (nonatomic, strong) id<ReformerProtocol> YYYReformer;  #pragma mark - APIManagerDelegate  - (void)apiManagerDidSuccess:(APIManager *)manager  {      NSDictionary *reformedXXXData = [manager fetchDataWithReformer:self.XXXReformer];      [self.XXXView configWithData:reformedXXXData];      NSDictionary *reformedYYYData = [manager fetchDataWithReformer:self.YYYReformer];      [self.YYYView configWithData:reformedYYYData];  }  在APIManager里面,fetchDataWithReformer是这样:  - (NSDictionary)fetchDataWithReformer:(id)reformer  {      if (reformer == nil) {          return self.rawData;      } else {          return [reformer reformDataWithManager:self];      }  }
  • 要点1:reformer是一个符合ReformerProtocol的对象,它提供了通用的方法供Manager使用。
  • 要点2:API的原始数据(JSON对象)由Manager实例保管,reformer方法里面取Manager的原始数据 (manager.rawData)做转换,然后交付出去。莲蓬头的水管部分是Manager,负责提供原始水流(数据流),reformer就是不同的 模式,换什么reformer就能出来什么水流。
  • 要点3:例子中举的场景是一个API数据被多个View使用的情况,体现了reformer的一个特点:可以根据需要改变同一数据来源 的展示方式。比如API数据展示的是“附近的小区”,那么这个数据可以被列表(XXXView)和地图(YYYView)共用,不同的view使用的数据 的转化方式不一样,这就通过不同的reformer解决了。
  • 要点4:在一个view用来同一展示不同API数据的情况,reformer是绝佳利器。比如安居客的列表view的数据来源可能有三 个:二手房列表API,租房列表API,新房列表API。这些API返回来的数据的value可能一致,但是key都是不一致的。这时候就可以通过同一个 reformer来做数据的标准化输出,这样就使得view代码复用成为可能。这体现了reformer另外一个特点:同一个reformer出来的数据 是高度标准化的。形象点说就是:只要莲蓬头不换,哪怕水管的水变成海水或者污水了,也依旧能够输出符合洗澡要求的淡水水流。举个例子:
- (void)apiManagerDidSuccess:(APIManager *)manager  {      // 这个回调方法有可能是来自二手房列表APIManager的回调,也有可能是  租房,也有可能是新房。但是在Controller层面我们不需要对它做额外区分,  只要是同一个reformer出来的数据,我们就能保证是一定能被self.XXXView使  用的。这样的保证由reformer的实现者来提供。      NSDictionary *reformedXXXData = [manager   fetchDataWithReformer:self.XXXReformer];      [self.XXXView configWithData:reformedXXXData];  }
  • 要点5:有没有发现,使用reformer之后,Controller的代码简洁了很多?而且,数据原型在这种情况下就没有必要存在了,随之而来的成本也就被我们绕过了。

reformer本质上就是一个符合某个protocol的对象,在controller需要从api manager中获得数据的时候,顺便把reformer传进去,于是就能获得经过reformer重新洗过的数据,然后就可以直接使用了。

更抽象地说,reformer其实是对数据转化逻辑的一个封装。在controller从manager中取数据之后,并且把数据交给 view之前,这期间或多或少都是要做一次数据转化的,有的时候不同的view,对应的转化逻辑还不一样,但是展示的数据是一样的。而且往往这一部分代码 都非常复杂,且跟业务强相关,直接上代码,将来就会很难维护。所以我们可以考虑采用不同的reformer封装不同的转化逻辑,然后让 controller根据需要选择一个合适的reformer装上,就像洗澡的莲蓬头,需要什么样的水流(数据的表现形式)就换什么样的头,然而水(数 据)都是一样的。这种做法能够大大提高代码的可维护性,以及减少ViewController的体积。

总结一下,reformer事实上是把转化的代码封装之后再从主体业务中拆分了出来,拆分出来之后不光降低了原有业务的复杂度,更重要的 是,它提高了数据交付的灵活性。另外,由于Controller负责调度Manager和View,因此它是知道Manager和View之间的关系 的,Controller知道了这个关系之后,就有了充要条件来为不同的View选择不同的Reformer,并用这个Reformer去改造 Mananger的数据,然后ViewController获得了经过reformer处理过的数据之后,就可以直接交付给view去使用。 Controller因此得到瘦身,负责业务数据转化的这部分代码也不用写在Controller里面,提高了可维护性。

所以reformer机制能够带来以下好处:

  • 好处1:绕开了API数据原型的转换,避免了相关成本。
  • 好处2:在处理单View对多API,以及在单API对多View的情况时,reformer提供了非常优雅的手段来响应这种需求,隔离了转化逻辑和主体业务逻辑,避免了维护灾难。
  • 好处3:转化逻辑集中,且将转化次数转为只有一次。使用数据原型的转化逻辑至少有两次,第一次是把JSON映射成对应的原型,第二次是 把原型转变成能被View处理的数据。reformer一步到位。另外,转化逻辑在reformer里面,将来如果API数据有变,就只要去找到对应 reformer然后改掉就好了。
  • 好处4:Controller因此可以省去非常多的代码,降低了代码复杂度,同时提高了灵活性,任何时候切换reformer而不必切换业务逻辑就可以应对不同View对数据的需要。
  • 好处5:业务数据和业务有了适当的隔离。这么做的话,将来如果业务逻辑有修改,换一个reformer就好了。如果其他业务也有相同的 数据转化逻辑,其他业务直接拿这个reformer就可以用了,不用重写。另外,如果controller有修改(比如UI交互方式改变),可以放心换 controller,完全不用担心业务数据的处理。

在不使用特定对象表征数据的情况下,如何保持数据可读性?

不使用对象来表征数据的时候,事实上就是使用NSDictionary的时候。事实上,这个问题就是,如何在NSDictionary表征数据的情况下保持良好的可读性?

苹果已经给出了非常好的做法,用固定字符串做key,比如你在接收到KeyBoardWillShow的Notification时,带了 一个userInfo,他的key就都是类似UIKeyboardAnimationCurveUserInfoKey这样的,所以我们采用这样的方案来 维持可读性。下面我举一个例子:

PropertyListReformerKeys.h  extern NSString * const kPropertyListDataKeyID;  extern NSString * const kPropertyListDataKeyName;  extern NSString * const kPropertyListDataKeyTitle;  extern NSString * const kPropertyListDataKeyImage;  PropertyListReformer.h  #import "PropertyListReformerKeys.h"  ... ...  PropertyListReformer.m  NSString * const kPropertyListDataKeyID = @"kPropertyListDataKeyID";  NSString * const kPropertyListDataKeyName = @"kPropertyListDataKeyName";  NSString * const kPropertyListDataKeyTitle = @"kPropertyListDataKeyTitle";  NSString * const kPropertyListDataKeyImage = @"kPropertyListDataKeyImage";  - (NSDictionary *)reformData:(NSDictionary *)originData fromManager:(APIManager *)manager  {      ... ...      ... ...      NSDictionary *resultData = nil;      if ([manager isKindOfClass:[ZuFangListAPIManager class]]) {          resultData = @{              kPropertyListDataKeyID:originData[@"id"],              kPropertyListDataKeyName:originData[@"name"],              kPropertyListDataKeyTitle:originData[@"title"],              kPropertyListDataKeyImage:[UIImage imageWithUrlString:originData[@"imageUrl"]]          };      }      if ([manager isKindOfClass:[XinFangListAPIManager class]]) {          resultData = @{              kPropertyListDataKeyID:originData[@"xinfang_id"],              kPropertyListDataKeyName:originData[@"xinfang_name"],              kPropertyListDataKeyTitle:originData[@"xinfang_title"],              kPropertyListDataKeyImage:[UIImage imageWithUrlString:originData[@"xinfang_imageUrl"]]          };      }      if ([manager isKindOfClass:[ErShouFangListAPIManager class]]) {          resultData = @{              kPropertyListDataKeyID:originData[@"esf_id"],              kPropertyListDataKeyName:originData[@"esf_name"],              kPropertyListDataKeyTitle:originData[@"esf_title"],              kPropertyListDataKeyImage:[UIImage imageWithUrlString:originData[@"esf_imageUrl"]]          };      }      return resultData;  }  PropertListCell.m  #import "PropertyListReformerKeys.h"  - (void)configWithData:(NSDictionary *)data  {      self.imageView.image = data[kPropertyListDataKeyImage];      self.idLabel.text = data[kPropertyListDataKeyID];      self.nameLabel.text = data[kPropertyListDataKeyName];      self.titleLabel.text = data[kPropertyListDataKeyTitle];  }

这一大段代码看下来,我如果不说一下要点,那基本上就白写了哈:

我们先看一下结构:

 iOS应用架构谈(三):网络层设计方案(上)

来自:http://www.infoq.com/cn/articles/ios-app-arch-3-1