前言
网络层在一个App中也是一个不可缺乏的部分,工程师们在网络层可以发挥的空间也比较大。另外,苹果对网络请求部分已经作了很好的封装,业界的AFNetworking也被普遍使用。其它的ASIHttpRequest,MKNetworkKit啥的其实也都还不错,但前者已经弃坑,后者也在弃坑的边缘。在实际的App开发中,Afnetworking已经成为了事实上各大App的标准配置。
网络层在一个App中承载了API调用,用户操做日志记录,甚至是即时通信等任务。我接触过一些App(开源的和不开源的)的代码,在看到网络层这一块时,尤为是在看到各位架构师各显神通展现了各类技巧,我很是为之感到兴奋。但有的时候,每每也对于其中的一些缺陷感到失望。
关于网络层的设计方案会有不少,须要权衡的地方也会有不少,甚至于争议的地方都会有不少。但不管如何,我都不会对这些问题作出任何逃避,我会在这篇文章中给出我对它们的见解和解决方案,观点毫不中立,不会跟你们打太极。
这篇文章就主要会讲这些方面:
1. 网络层跟业务对接部分的设计
2. 网络层的安全机制实现
3. 网络层的优化方案
网络层跟业务对接部分的设计
在安居客App的架构更新换代的时候,我深深地感受到网络层跟业务对接部分的设计有多么重要,所以我对它作的最大改变就是针对网络层跟业务对接部分的改变。网络层跟业务层对接部分设计的好坏,会直接影响到业务工程师实现功能时的心情。
在正式开始讲设计以前,咱们要先讨论几个问题:
1. 使用哪一种交互模式来跟业务层作对接?
2. 是否有必要将API返回的数据封装成对象而后再交付给业务层?
3. 使用集约化调用方式仍是离散型调用方式去调用API?
这些问题讨论完毕以后,我会给出一个完整的设计方案来给你们作参考,设计方案是鱼,讨论的这些问题是渔,我什么都授了,你们各取所需。
使用哪一种交互模式来跟业务层作对接?
这里其实有两个问题:
1. 以什么方式将数据交付给业务层?
2. 交付什么样的数据给业务层?
以什么方式将数据交付给业务层?
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所对应的响应方法很难在编译层面做限制,不一样的业务工程师会给他取不一样的名字,这也会给代码的可维护性带来灾难。html
手机淘宝架构组的侠武同窗曾经给我分享过一个问题,在这里我也分享给你们:曾经有一个工程师在监听Notification以后,没有写释放监听的代码,固然,找到这个缘由又是很漫长的一段故事,如今找到缘由了,然而监听这个Notification的对象有那么多,不知道具体是哪一个Notificaiton,也不知道那个没释放监听的对象是谁。后来折腾了好久你们都没办法的时候,有一个经验丰富的工程师提出用hook(Method Swizzling)的方式,最终找到了那个没释放监听的对象,bug修复了。
我分享这个问题的目的并非想强调Notification多么多么很差,Notification自己就是一种设计模式,在属于他的问题领域内,Notification是很是好的一种解决方案。但我想强调的是,对于网络层这个问题领域内来看,架构师首先必定要限制代码的影响范围,在能用影响范围小的方案的时候就尽可能采用这种小的方案,不然未来要是有什么奇怪需求或者出了什么小问题,维护起来就很是麻烦。所以Notification这个方案不能做为首选方案,只能做为备选。
那么Notification也不是彻底不能使用,当需求要求跨层时,咱们就能够使用Notification,好比前面提到的网络条件切换,并且这个需求也是须要知足一对多的。
因此,为了符合前面所说的这些要求,使用Delegate可以很好地避免跨层访问,同时限制了响应代码的形式,相比Notification而言有更好的可维护性。
而后咱们顺便来讲说为何尽可能不要用block。
咱们在调试的时候常常会单步追踪到某一个地方以后,发现尼玛这里有个block,若是想知道这个block里面都作了些什么事情,这时候就比较蛋疼了。
- (void)someFunctionWithBlock:(SomeBlock *)block
{
... ...
-> block(); //当你单步走到这儿的时候,要想知道block里面都作了哪些事情的话,就很麻烦。
... ...
}
block会给内部全部的对象引用计数加一,这一方面会带来潜在的retain cycle,不过咱们能够经过Weak Self的手段解决。另外一方面比较重要就是,它会延长对象的生命周期。
在网络回调中使用block,是block致使对象生命周期被延长的其中一个场合,当ViewController从window中卸下时,若是尚有请求带着block在外面飞,而后block里面引用了ViewController(这种场合很是常见),那么ViewController是不能被及时回收的,即使你已经取消了请求,那也仍是必须得等到请求着陆以后才能被回收。
然而使用delegate就不会有这样的问题,delegate是弱引用,哪怕请求仍然在外面飞,,ViewController仍是可以及时被回收的,回收以后指针自动被置为了nil,无伤大雅。
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];
}];
这实质上跟使用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联系紧密,也是咱们不但愿发生的。
在实际使用时,代码观感是这样的:
先定义一个protocol:
@protocol ReformerProtocol <NSObject>
- (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<ReformerProtocol>)reformer
{
if (reformer == nil) {
return self.rawData;
} else {
return [reformer reformDataWithManager:self];
}
}
- (void)apiManagerDidSuccess:(APIManager *)manager
{
// 这个回调方法有多是来自二手房列表APIManager的回调,也有多是租房,也有多是新房。可是在Controller层面咱们不须要对它作额外区分,只要是同一个reformer出来的数据,咱们就能保证是必定能被self.XXXView使用的。这样的保证由reformer的实现者来提供。
NSDictionary *reformedXXXData = [manager fetchDataWithReformer:self.XXXReformer];
[self.XXXView configWithData:reformedXXXData];
}
好处1:绕开了API数据原型的转换,避免了相关成本。
在不使用特定对象表征数据的状况下,如何保持数据可读性?
不使用对象来表征数据的时候,事实上就是使用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];
}
这一大段代码看下来,我若是不说一下要点,那基本上就白写了哈:
咱们先看一下结构:
---------------------------------- -----------------------------------------
| | | |
| PropertyListReformer.m | | PropertyListReformer.h |
| | | |
| #import PropertyListReformer.h | <------- | #import "PropertyListReformerKeys.h" |
| NSString * const key = @"key" | | |
| | | |
---------------------------------- -----------------------------------------
.
/|\
|
|
|
|
---------------------------------
| |
| PropertyListReformerKeys.h |
| |
| extern NSString * const key; |
| |
---------------------------------
这么作的好处就是,未来迁移的时候至关方便,只要扔头文件就能够了,只扔头文件是不会致使拔出萝卜带出泥的状况的。并且也避免了自定义对象带来的额外代码体积。
综上,我对交付什么样的数据给业务层?这个问题的回答就是这样:
集约型API调用方式和离散型API调用方式的选择?
集约型API调用其实就是全部API的调用只有一个类,而后这个类接收API名字,API参数,以及回调着陆点(能够是target-action,或者block,或者delegate等各类模式的着陆点)做为参数。而后执行相似startRequest这样的方法,它就会去根据这些参数起飞去调用API了,而后得到API数据以后再根据指定的着陆点去着陆。好比这样:
集约型API调用方式:
[APIRequest startRequestWithApiName:@"itemList.v1" params:params success:@selector(success:) fail:@selector(fail:) target:self];
离散型API调用是这样的,一个API对应于一个APIManager,而后这个APIManager只须要提供参数就能起飞,API名字、着陆方式都已经集成入APIManager中。好比这样:
离散型API调用方式:
@property (nonatomic, strong) ItemListAPIManager *itemListAPIManager;
// getter
- (ItemListAPIManager *)itemListAPIManager
{
if (_itemListAPIManager == nil) {
_itemListAPIManager = [[ItemListAPIManager alloc] init];
_itemListAPIManager.delegate = self;
}
return _itemListAPIManager;
}
// 使用的时候就这么写:
[self.itemListAPIManager loadDataWithParams:params];
集约型API调用和离散型API调用这二者实现方案不是互斥的,单看下层,你们都是集约型。由于发起一个API请求以后,除去业务相关的部分(好比参数和API名字等),剩下的都是要统一处理的:加密,URL拼接,API请求的起飞和着陆,这些处理若是不用集约化的方式来实现,做者非癫即痴。然而对于整个网络层来讲,尤为是业务方使用的那部分,我倾向于提供离散型的API调用方式,并不建议在业务层的代码直接使用集约型的API调用方式。缘由以下:
缘由1:当前请求正在外面飞着的时候,根据不一样的业务需求存在两种不一样的请求起飞策略:一个是取消新发起的请求,等待外面飞着的请求着陆。另外一个是取消外面飞着的请求,让新发起的请求起飞。集约化的API调用方式若是要知足这样的需求,那么每次要调用的时候都要多写一部分判断和取消的代码,手段就作不到很干净。
前者的业务场景举个例子就是刷新页面的请求,刷新详情,刷新列表等。后者的业务场景举个例子是列表多维度筛选,好比你先筛选了商品类型,而后筛选了价格区间。固然,后者的状况不必定每次筛选都要调用API,咱们先假设这种筛选每次都必需要经过调用API才能得到数据。
若是是离散型的API调用,在编写不一样的APIManager时候就能够针对不一样的API设置不一样的起飞策略,在实际使用的时候,就能够没必要关心起飞策略了,由于APIMananger里面已经写好了。
缘由2:便于针对某个API请求来进行AOP。在集约型的API调用方式下,若是要针对某个API请求的起飞和着陆过程进行AOP,这代码得写成什么样。。。噢,尼玛这画面太美别说看了,我都不敢想。
缘由3:当API请求的着陆点消失时,离散型的API调用方式可以更加透明地处理这种状况。
当一个页面的请求正在天上飞的时候,用户等了很久不耐烦了,小手点了个back,而后ViewController被pop被回收。此时请求的着陆点就没了。这是很危险的状况,着陆点要是没了,就很容易crash的。通常来讲处理这个状况都是在dealloc的时候取消当前页面全部的请求。若是是集约型的API调用,这个代码就要写到ViewController的dealloc里面,但若是是离散型的API调用,这个代码写到APIManager里面就能够了,而后随着ViewController的回收进程,APIManager也会被跟着回收,这部分代码就获得了调用的机会。这样业务方在使用的时候就能够没必要关心着陆点消失的状况了,从而更加关注业务。
缘由4:离散型的API调用方式可以最大程度地给业务方提供灵活性,好比reformer机制就是基于离散型的API调用方式的。另外,若是是针对提供翻页机制的API,APIManager就能简单地提供loadNextPage方法去加载下一页,页码的管理就不用业务方去管理了。还有就是,若是要针对业务请求参数进行验证,好比用户填写注册信息,在离散型的APIManager里面实现就会很是轻松。
综上,关于集约型的API调用和离散型的API调用,我倾向于这样:对外提供一个BaseAPIManager来给业务方作派生,在BaseManager里面采用集约化的手段组装请求,放飞请求,然而业务方调用API的时候,则是以离散的API调用方式来调用。若是你的App只提供了集约化的方式,而没有离散方式的通道,那么我建议你再封装一层,便于业务方使用离散的API调用方式来放飞请求。
怎么作APIManager的继承?
若是要作成离散型的API调用,那么使用继承是逃不掉的。BaseAPIManager里面负责集约化的部分,外部派生的XXXAPIManager负责离散的部分,对于BaseAPIManager来讲,离散的部分有一些是必要的,好比API名字等,而咱们派生的目的,也是为了提供这些数据。
我在这篇文章里面列举了种种继承的坏处,呼吁你们尽可能不要使用继承。可是如今到了不起不用继承的时候,因此我得提醒一下你们别把继承用坏了。
在APIManager的状况下,咱们最直觉的思路是BaseAPIManager提供一些空方法来给子类作重载,好比apiMethodName这样的函数,然而个人建议是,不要这么作。咱们能够用IOP的方式来限制派生类的重载。
大概就是长这样:
BaseAPIManager的init方法里这么写:
// 注意是weak。
@property (nonatomic, weak) id<APIManager> child;
- (instancetype)init
{
self = [super init];
if ([self confirmsToProtocol:@protocol(APIManager)]) {
self.child = (id<APIManager>)self;
} else {
// 不遵照这个protocol的就让他crash,防止派生类乱来。
NSAssert(NO, "子类必需要实现APIManager这个protocol。");
}
return self;
}
protocol这么写,把本来要重载的函数都定义在这个protocol里面,就不用在父类里面写空方法了:
@protocol APIManager <NSObject>
@required
- (NSString *)apiMethodName;
...
@end
而后在父类里面若是要使用的话,就这么写:
[self requestWithAPIName:[self.child apiMethodName] ......];
简单说就是在init的时候检查本身是否符合预先设计的子类的protocol,这就要求全部子类必须遵照这个protocol,全部针对父类的重载、覆盖也都以这个protocol为准,protocol之外的方法不容许重载、覆盖。而在父类的代码里,能够没必要遵照这个protocol,保持了将来维护的灵活性。
这么作的好处就是避免了父类写空方法,同时也给子类带上了紧箍咒:要想当个人孩子,就要遵照这些规矩,不能乱来。业务方在实现子类的时候,就能够根据protocol中的方法去一一实现,而后约定就比较好作了:不容许重载父类方法,只容许选择实现或不实现protocol中的方法。
关于这个的具体的论述在这篇文章里面有,感兴趣的话能够看看。
网络层与业务层对接部分的小总结
这一节主要是讲了如下这些点:
1. 使用delegate来作数据对接,仅在必要时采用Notification来作跨层访问
2. 交付NSDictionary给业务层,使用Const字符串做为Key来保持可读性
4. 网络层上部分使用离散型设计,下部分使用集约型设计
5. 设计合理的继承机制,让派生出来的APIManager受到限制,避免混乱
6. 应该不止这5点...
网络层的安全机制
判断API的调用请求是来自于通过受权的APP
使用这个机制的目的主要有两点:
1. 确保API的调用者是来自你本身的APP,防止竞争对手爬你的API
2. 若是你对外提供了须要注册才能使用的API平台,那么你须要有这个机制来识别是不是注册用户调用了你的API
解决方案:设计签名
要达到第一个目的其实很简单,服务端须要给你一个密钥,每次调用API时,你使用这个密钥再加上API名字和API请求参数算一个hash出来,而后请求的时候带上这个hash。服务端收到请求以后,按照一样的密钥一样的算法也算一个hash出来,而后跟请求带来的hash作一个比较,若是一致,那么就表示这个API的调用者确实是你的APP。为了避免让别人也获取到这个密钥,你最好不要把这个密钥存储在本地,直接写死在代码里面就行了。另外适当增长一下求Hash的算法的复杂度,那就是各类Hash算法(好比MD5)加点盐,再回炉跑一次Hash啥的。这样就能解决第一个目的了:确保你的API是来自于你本身的App。
通常状况下大部分公司不会出现须要知足第二种状况的需求,除非公司开发了本身的API平台给第三方使用。这个需求跟上面的需求有一点不一样:符合受权的API请求者不仅是一个。因此在这种状况下,须要的安全机制会更加复杂一点。
这里有一个较容易实现的方案:客户端调用API的时候,把本身的密钥经过一个可逆的加密算法加密后连着请求和加密以后的Hash一块儿送上去。固然,这个可逆的加密算法确定是放在在调用API的SDK里面,编译好的。而后服务端拿到加密后的密钥和加密的Hash以后,解码获得原始密钥,而后再用它去算Hash,最后再进行比对。
保证传输数据的安全
使用这个机制的主要目的有两点:
1. 防止中间人攻击,好比说运营商很喜欢往用户的Http请求里面塞广告...
2. SPDY依赖于HTTPS,并且是将来HTTP/2的基础,他们可以提升你APP在网络层总体的性能。
解决方案:HTTPS
目前使用HTTPS的主要目的在于防止运营商往你的Response Data里面加广告啥的(中间人攻击),面对的威胁范围更广。从2011年开始,国外业界就已经提倡全部的请求(不光是API,还有网站)都走HTTPS,国内差很少晚了两年(2013年左右)才开始提倡这事,天猫是这两个月才开始作HTTPS的全APP迁移。
安全机制小总结
这一节说了两种安全机制,通常来讲第一种是标配,第二种属于可选配置。不过随着我国互联网基础设施的完善,移动设备性能的提升,以及优化技术的提升,第二种配置的缺点(速度慢)正在愈来愈微不足道,所以HTTPS也会成为不久以后的将来App的网络层安全机制标配。各位架构师们,若是你的App尚未挂HTTPS,如今就已经能够开始着手这件事情了。
网络层的优化方案
网络层的优化手段主要从如下三方面考虑:
1. 针对连接创建环节的优化
2. 针对连接传输数据量的优化
3. 针对连接复用的优化
这三方面是全部优化手段的内容,各类五花八门的优化手段基本上都不会逃脱这三方面,下面我就会分别针对这三方面讲一下各自对应的优化手段。
1. 针对连接创建环节的优化
在API发起请求创建连接的环节,大体会分这些步骤:
1. 发起请求
2. DNS域名解析获得IP
3. 根据IP进行三次握手(HTTPS四次握手),连接创建成功
其实第三步的优化手段跟第二步的优化手段是一致的,我会在讲第二步的时候一块儿讲掉。
1.1 针对发起请求的优化手段
其实要解决的问题就是网络层该不应为此API调用发起请求。
对于大部分API调用请求来讲,有些API请求所带来的数据的时效性是比较长的,好比商品详情,好比App皮肤等。那么咱们就能够针对这些数据作本地缓存,这样下次请求这些数据的时候就能够没必要再发起新的请求。
通常是把API名字和参数拼成一个字符串而后取MD5做为key,存储对应返回的数据。这样下次有一样请求的时候就能够直接读取这里面的数据。关于这里有一个缓存策略的问题须要讨论:何时清理缓存?要么就是根据超时时间限制进行清理,要么就是根据缓存数据大小进行清理。这个策略的选择要根据具体App的操做日志来决定。
好比安居客App,日志数据记录显示用户平均使用时长不到3分钟,可是用户查看房源详情的次数比较多,而房源详情数据量较大。那么这个时候,就适合根据使用时长来作缓存,我当时给安居客设置的缓存超时时间就是3分钟,这样可以保证这个缓存可以在大部分用户使用时间产生做用。嗯,极端状况下作什么缓存手段不考虑,只要可以服务好80%的用户就能够了,并且针对极端状况采用的优化手段对大部分普通用户而言是没必要要的,作了反而会对他们有影响。
再好比网络图片缓存,数据量基本上都特别大,这种就比较适合针对缓存大小来清理缓存的策略。
另外,以前的缓存的前提都是基于内存的。咱们也能够把须要清理的缓存存储在硬盘上(APP的本地存储,我就先用硬盘来表示了,虽然不多有手机硬盘的说法,哈哈),好比前面提到的图片缓存,由于图片颇有可能在很长时间以后,再被显示的,那么本来须要被清理的图片缓存,咱们就能够考虑存到硬盘上去。当下次再有显示网络图片的需求的时候,咱们能够先从内存中找,内存找不到那就从硬盘上找,这都找不到,那就发起请求吧。
固然,有些时效性很是短的API数据,就不能使用这个方法了,好比用户的资金数据,那就须要每次都调用了。
这个我在前面提到过,就是针对重复请求的发起和取消,是有对应的请求策略的。咱们先说取消策略。
若是是界面刷新请求这种,并且存在重复请求的状况(下拉刷新时,在请求着陆以前用户不断执行下拉操做),那么这个时候,后面重复操做致使的API请求就能够没必要发送了。
若是是条件筛选这种,那就取消前面已经发送的请求。虽然颇有可能这个请求已经被执行了,那么取消所带来的性能提高就基本没有了。但若是这个请求还在队列中待执行的话,那么对应的此次连接就能够省掉了。
以上是一种,另一种状况就是请求策略:相似用户操做日志的请求策略。
用户操做会触发操做日志上报Server,这种请求特别频繁,可是是暗地里进行的,不须要用户对此有所感知。因此也不必操做一次就发起一次的请求。在这里就能够采用这样的策略:在本地记录用户的操做记录,当记录满30条的时候发起一次请求将操做记录上传到服务器。而后每次App启动的时候,上传一次上次遗留下来没上传的操做记录。这样可以有效下降用户设备的耗电量,同时提高网络层的性能。
小总结
针对创建链接这部分的优化就是这样的原则:能不发请求的就尽可能不发请求,必需要发请求时,能合并请求的就尽可能合并请求。然而,任何优化手段都是有前提的,并且也不能保证对全部需求都能起做用,有些API请求就是不符合这些优化手段前提的,那就老老实实发请求吧。不过这类API请求所占比例通常不大,大部分的请求都或多或少符合优化条件,因此针对发送请求的优化手段仍是值得作的。
1.2 & 1.3 针对DNS域名解析作的优化,以及创建连接的优化
其实在整个DNS链路上也是有DNS缓存的,理论上也是可以提升速度的。这个链路上的DNS缓存在PC用户上效果明显,由于PC用户的DNS链路相对稳定,信号源不会变来变去。可是在移动设备的用户这边,链路上的DNS缓存所带来的性能提高就不太明显了。由于移动设备的实际使用场景比较复杂,网络信号源会常常变换,信号源每变换一次,对应的DNS解析链路就会变换一次,那么原链路上的DNS缓存就不起做用了。并且信号源变换的状况特别特别频繁,因此对于移动设备用户来讲,链路的DNS缓存咱们基本上能够默认为没有。因此大部分时间是手机系统自带的本地DNS缓存在起做用,可是通常来讲,移动设备上网的需求也特别频繁,专门为咱们这个App所作的DNS缓存颇有可能会被别的DNS缓存给挤出去被清理掉,这种状况是特别多的,用户看一下子知乎刷一下微博查一下地图逛一逛点评再聊个Q,回来以后颇有可能属于你本身的App的本地DNS缓存就没了。这还没完,这里还有一个只有在中国特点社会主义的互联网环境中才会有的问题:国内的互联网环境因为GFW的存在,就使得DNS服务速度会比正常状况慢很多。
基于以上三个缘由所致使的最终结果就是,API请求在DNS解析阶段的耗时会不少。
那么针对这个的优化方案就是,索性直接走IP请求,那不就绕过DNS服务的耗时了嘛。
另一个,就是上面提到的创建连接时候的第三步,国内的网络环境分北网通南电信(固然实际状况更复杂,这里随便说说),不一样服务商之间的链接,延时是很大的,咱们须要想办法让用户在最适合他的IP上给他提供服务,那么就针对咱们绕过DNS服务的手段有一个额外要求:尽量不要让用户使用对他来讲很慢的IP。
因此综上所述,方案就应该是这样:本地有一份IP列表,这些IP是全部提供API的服务器的IP,每次应用启动的时候,针对这个列表里的全部IP取ping延时时间,而后取延时时间最小的那个IP做为从此发起请求的IP地址。
针对创建链接的优化手段实际上是跟DNS域名解析的优化手段是同样的。不过这须要你的服务器提供服务的网络状况要多,通常如今的服务器都是双网卡,电信和网通。因为中国特点的互联网ISP分布,南北网络之间存在瓶颈,而咱们App针对连接的优化手段主要就是着手于如何减轻这个瓶颈对App产生的影响,因此须要维护一个IP列表,这样就能就近链接了,就起到了优化的效果。
咱们通常都是在应用启动的时候得到本地列表中全部IP的ping值,而后经过NSURLProtocol的手段将URL中的HOST修改成咱们找到的最快的IP。另外,这个本地IP列表也会须要经过一个API来维护,通常是天天第一次启动的时候读一次API,而后更新到本地。
若是你还不熟悉NSURLProtocol应该怎么玩,看完官方文档和这篇文章以及这个Demo以后,你确定就会了,其实很简单的。另外,刚才提到那篇文章的做者(mattt)还写了这个基于NSURLProtocol的工具,至关好用,是能够直接拿来集成到项目中的。
不用NSURLProtocol的话,用其余手段也能够作到这一点,但那些手段未免又比较愚蠢。
2. 针对连接传输数据量的优化
这个很好理解,传输的数据少了,那么天然速度就上去了。这里没什么花样能够讲的,就是压缩呗。各类压缩。
3. 针对连接复用的优化
创建连接自己是属于比较消耗资源的操做,耗电耗时。SPDY自带连接复用以及数据压缩的功能,因此服务端支持SPDY的时候,App直接挂SPDY就能够了。若是服务端不支持SPDY,也能够使用PipeLine,苹果原生自带这个功能。
通常来讲业界内广泛的认识是SPDY优于PipeLine,而后即使如此,SPDY可以带来的网络层效率提高其实也没有文献上的图表那么明显,但仍是有性能提高的。还有另一种比较笨的连接复用的方法,就是维护一个队列,而后将队列里的请求压缩成一个请求发出去,之因此会存在滞留在队列中的请求,是由于在上一个请求还在外面飘的时候。这种作法最终的效果表面上看跟连接复用差异不大,但并非真正的连接复用,只能说是请求合并。
不过目前业界趋势是倾向于使用HTTP/2.0来代替SPDY,不过目前HTTP/2.0尚未正式出台,相关实现大部分都处在demo阶段,因此咱们仍是先SPDY搞起就行了。将来颇有可能会放弃SPDY,转而采用HTTP/2.0来实现网络的优化。这是要提醒各位架构师注意的事情。嗯,我也不知道HTTP/2.0何时能出来。
渔说完了,鱼来了
这里是我当年设计并实现的安居客的网络层架构代码。固然,该脱敏的地方我都已经脱敏了,因此编不过是正常的,哈哈哈。可是代码比较齐全,重要地方注释我也写了不少。另外,为了让你们可以把这些代码看明白,我还附带了当年介绍这个框架演讲时的PPT。(补充说明一下,评论区好多人问PPT找不着在哪儿,PPT也在上面提到的repo里面,是个key后缀名的文件,用keynote打开)
而后就是,当年也有不少问题其实考虑得并无如今清楚,因此有些地方仍是作得不够好,好比拦截器和继承。并且当时的优化手段只有本地cache,安居客没有那么多IP能够给我ping,当年也没流行SPDY,并且API也还不支持HTTPS,因此当时的代码里面没有在这些地方作优化,比较原始。然而整个架构的基本思路一直没有变化:优先服务于业务方。另外,安居客的网络层多了一个service的概念,这是我这篇文章中没有讲的。主要是由于安居客的API提供方不少,二手房,租房,新房,X项目等等API都是不一样的API team提供的,以service做区分,若是你的app也是相似的状况,我也建议你设计一套service机制。如今这些service被我删得只剩下一个google的service,由于其余service都属于敏感内容。
另外,这里面提供的PPT我很但愿你们可以花时间去看看,在PPT里面有些更加细的东西我在博客里没有写,主要是我比较懒,而后这篇文章拖的时间比较长了,花时间搬运这个没什么意思,不过内容仍是值得各位读者去看的。关于PPT里面你们有什么问题的,也能够在评论区问,我都会回答。
总结
第二部分讲了网络安全上,客户端要作的两点。固然,从网络安全的角度上讲,服务端也要作不少不少事情,客户端要作的一些边角细节的事情也还会有不少,好比作一些代码混淆,尽量避免代码中明文展现key。不过大头主要就是这两个,并且也都是须要服务端同窗去配合的。主要偏重于介绍。(主要是也没啥好实践的,google一下教程照着来就行了)。
第三部分讲了优化,优化的全部方面都已经列出来了,若是业界再有七七八八的别的手段,也基本逃离不出本文的范围。这里有些优化手段是须要服务端同窗配合的,有些不须要,你们看各自状况来决定。主要偏重于实践。
最后给出了我以前在安居客作的网络层架构的主要代码,以及当时演讲时的PPT。关于代码或PPT中有任何问题,均可以在评论区问我。