iOS应用架构谈 网络层设计方案
前言
网络层在一个App
中也是一个不可缺少的部分,工程师们在网络层能够发挥的空间也比较大。另外,苹果对网络请求部分已经做了很好的封装,业界的AFNetworking
也被广泛使用。其它的ASIHttpRequest
,MKNetworkKit
啥的其实也都还不错,但前者已经弃坑,后者也在弃坑的边缘。在实际的App
开发中,Afnetworking
已经成为了事实上各大App的标准配置。
网络层在一个App中承载了API调用,用户操作日志记录,甚至是即时通讯等任务。我接触过一些App(开源的和不开源的)的代码,在看到网络层这一块时,尤其是在看到各位架构师各显神通展示了各种技巧,我非常为之感到兴奋。但有的时候,往往也对于其中的一些缺陷感到失望。
关于网络层的设计方案会有很多,需要权衡的地方也会有很多,甚至于争议的地方都会有很多。但无论如何,我都不会对这些问题做出任何逃避,我会在这篇文章中给出我对它们的看法和解决方案,观点绝不中立,不会跟大家打太极。
这篇文章就主要会讲这些方面:
- 网络层跟业务对接部分的设计
- 网络层的安全机制实现
- 网络层的优化方案
网络层跟业务对接部分的设计
在安居客App的架构更新换代的时候,我深深地感觉到网络层跟业务对接部分的设计有多么重要,因此我对它做的最大改变就是针对网络层跟业务对接部分的改变。网络层跟业务层对接部分设计的好坏,会直接影响到业务工程师实现功能时的心情。
在正式开始讲设计之前,我们要先讨论几个问题:
- 使用哪种交互模式来跟业务层做对接?
- 是否有必要将API返回的数据封装成对象然后再交付给业务层?
- 使用集约化调用方式还是离散型调用方式去调用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。
block很难追踪,难以维护
我们在调试的时候经常会单步追踪到某一个地方之后,发现尼玛这里有个block
,如果想知道这个block
里面都做了些什么事情,这时候就比较蛋疼了。
- (void)someFunctionWithBlock:(SomeBlock *)block
{
... ...
-> block(); //当你单步走到这儿的时候,要想知道block里面都做了哪些事情的话,就很麻烦。
... ...
}
block会延长相关对象的生命周期
block
会给内部所有的对象引用计数加一,这一方面会带来潜在的retain cycle
,不过我们可以通过Weak Self
的手段解决。另一方面比较重要就是,它会延长对象的生命周期。
在网络回调中使用block
,是block
导致对象生命周期被延长的其中一个场合,当ViewController
从window
中卸下时,如果尚有请求带着block
在外面飞,然后block里面引用了ViewController
(这种场合非常常见),那么ViewController
是不能被及时回收的,即便你已经取消了请求,那也还是必须得等到请求着陆之后才能被回收。
然而使用delegate
就不会有这样的问题,delegate
是弱引用,哪怕请求仍然在外面飞,,ViewController
还是能够及时被回收的,回收之后指针自动被置为了nil
,无伤大雅。
block在离散型场景下不符合使用的规范
block
和delegate
乍看上去在作用上是很相似,但是关于它们的选型有一条严格的规范:当回调之后要做的任务在每次回调时都是一致的情况下,选择delegate
,在回调之后要做的任务在每次回调时无法保证一致,选择block。在离散型调用的场景下,每一次回调都是能够保证任务一致的,因此适用delegate
。这也是苹果原生的网络调用也采用delegate
的原因,因为苹果也是基于离散模型去设计网络调用的,而且本文即将要介绍的网络层架构也是基于离散型调用的思路去设计的。
在集约型调用的场景下,使用block
是合理的,因为每次请求的类型都不一样,那么自然回调要做的任务也都会不一样,因此只能采用block
。AFNetworking
就是属于集约型调用,因此它采用了block
来做回调。
就我所知,目前大部分公司的App网络层都是集约型调用,因此广泛采取了block
回调。但是在App的网络层架构设计中直接采用集约型调用来为业务服务的思路是有问题的,因此在迁移到离散型调用时,一定要注意这一点,记得迁回delegate
回调。关于离散型和集约型调用的介绍和如何选型,我在后面的集约型API调用方式和离散型API调用方式的选择?小节中有详细的介绍。
所以平时尽量不要滥用block
,尤其是在网络层这里。
统一回调方法,便于调试和维护
前面讲的是跨层问题,区分了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];
}];