iOS 网络层架构设计与实现

前言

客户端网络层交互流程:

  • 获取参数,统一配置
  • 根据API配置公共参数
  • 构造网络请求
  • 发送网络请求
  • 获取返回结果
  • 展示数据

注:此处还应该有个 数据持久化 流程,因项目业务层逻辑及存储方案各不相同,本篇中不展开介绍
另:本篇幅中暂时只涉及到HTTP的请求,TCP及UDP的网络交互处理又不一样,需要考虑的也不相同(如:TCP考虑包顺序处理及回调分发问题,UDP考虑丢包及无序处理)

记录: iOS TCP链接使用 GCDAsyncSocket库;
UDP简单实现丢包处理方法,给文件分块,每个数据包的头部添加一个唯一标识序号的ID值,当接收到的包头部ID不是期望中的ID号,则判定丢包,将丢包ID发回服务端,服务端接收到丢包响应则重发丢失的数据包
UDP模拟TCP协议三次握手,这样对丢包处理有帮助

本文是博主15年读casa中网络层设计方案的记录与思考总结。

聊聊AFN

AFN2.x特性:

  • 使用NSOperation,支持取消,不需占用完整线程(只在一个线程等待)
  • 通过NSOperationQueue来进行并发任务数量限制(maxConcurrentOperationCount)
  • 可暂停,可添加依赖
  • 安全性设置(HTTPS、AFURLConnectOperationSSLPinningMode)
  • HTTP缓存(NSURLCache,数据缓存在本地sqlite里,需要服务器配合,设置请求头部信息)
    注:在等待请求时只有一个Thread,在这个Thread上启动一个runloop监听NSURLConnection的NSMachPort类型源。start里面直接跳转到这个线程执行,在加入NSOperationQueue时,顶多start方法执行的时候占用一个线程,然后真正的发送请求和等待都是在这个NetworkRequestThread里面进行的(而最终访问网络并拉取数据不是这个线程,是NSURLConnection统一调度)。

网络交互中可以做哪些

流程项 可操作项
获取参数,统一配置 进行参数验证
根据API配置公共参数 组件化配置HTTP头部、cookie、签名等
构造请求 设置拦截器进行拦截请求
发出请求 对已发出请求记录ID,统一管理(缓存)
返回结果 设置拦截器进行网络请求返回拦截,验证返回数据正确性、缓存数据
展示数据 根据API及业务层展示的需要,使用reformer将数据转化成任何你想要的东西(NSDictionary、DataList、View)
1.参数验证
  • 注册账号或者发货信息等有必填选项,避免调用API产生不必要的开销
  • 代码即文档,只需看这个验证函数就知道需要传什么参数
2.组件化
  • 使用工厂模式生产出不同的服务,每个服务配置好自己的线上线下url及版本号等信息(方便灵活),独立的组件来解耦API调用逻辑生产各种参数
3.根据不同API的配置使用2种策略
  • 正在请求时,忽略新来的请求(当滚动tableview时,会频繁触发加载下一页的事件,如果当前APIManager正在加载下一页,那么就不需要再发送加载请求)
  • 正在请求时,取消过去已发送的请求,执行现在请求(查询商品,切换筛选条件时,如果前一次筛选条件的请求正在进行中,那么就应当取消前一次请求,执行现在的请求 — 考虑到正在进行的请求取消设计)
4.通过将NSOperation保存requestID成NSMutableDictionary,随时依照requestID查找请求并支持取消
5.缓存
  • 接口返回的数据很少变动,不希望做重复请求
  • 网络慢或者服务器等异常状况容灾

引申:对于一些点赞和取消 频繁与网络交互的设计考虑

网络层与业务层对接部分设计

1.使用哪种交互模式与业务层做对接
  • 以什么方式将数据交付给业务层
    a.以Delegate为主,Notification为辅;尽可能减少跨层数据交流的可能,限制耦合;统一回调方法,便于调试和维护;在跟业务层对接的部分只采用一种对接方式,限制灵活性,以此来交换应用的可维护性(delegate对上下文有限制性)。
    b.在网络请求和网络层接受请求的地方,使用Block没问题,但在获得数据交给业务方时,最好还是通过Delegate去通知到业务方。
  • 交付什么样的数据给业务层
    选择合适的reformer将View可以直接使用的数据(甚至reformer可以用来直接生成View)转化好之后交付给View;对于网络层而言,只需要保持住原始数据即可,不需要主动转化成数据model,数据采用NSDictionary加const字符串key来表征,避免了使用model来表征带来的迁移困难,同时不失去可读性
2.是否有必要将API返回的数据封装成对象然后再交付给业务层
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
先定义一个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];
}
}

a.reformer是一个符合ReformerProtocol的对象,它提供了通用的方法供Manager使用;

b.API的原始数据(JSON对象)由Manager实例保管,reformer方法里面取Manager的原始数据(manager.rawData)做转换,然后交付出去。就比方是:莲蓬头的水管部分是Manager,负责提供原始水流(数据流),reformer就是不同的漏斗模式,换什么reformer就能出来什么形式的水(是不是有一种RAC信号的感觉)。

c.例子中举的场景是一个API数据被多个View使用的情况,体现了reformer的一个特点:可以根据需要改变同一数据来源的展示方式。比如API数据展示的是“附近的小区”,那么这个数据可以被列表(XXXView)和地图(YYYView)共用,不同的view使用的数据转化方式不一样,这就通过不同的reformer解决了。

3.使用集约型调用方式还是离散型方式调用API

建议方式:对外提供一个BaseAPIManager来给业务方做派生,在BaseManager里面采用集约型的手段,加密处理、URL拼接、组装请求和放飞请求,然而业务方调用API时,则是以离散的API方式来调用。
实际上,在博主之前做的项目中,账号系统及充值计费系统是属于集约型调用方式,主要是需求简单,调用接口多,返回数据处理逻辑简单。

离散型单独对某个API请求的起飞和着陆过程可以进行AOP拦截,做到参数验证及返回数据缓存等操作,reformer机制就是基于离散型的API调用方式的。

设计代码层面

1.APIBaseManager存在的意义

具体代码链接

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
BaseAPIManager的init方法里这么写:
// 注意是weak。
@property (nonatomic, weak) id<APIManager> child;
- (instancetype)init
{
self = [super init];
if ([self conformsToProtocol:@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] ......];
  • 将子类的继承方式标准化 <APIManager>
  • interceptor 同时支持外部拦截和内部拦截(TODO:说明内部拦截和外部拦截区别)
  • 提供撤销网络请求的方法
  • 获得数据后,使用reformer转化成图片、语音以及任何你希望得到的东西
  • 可配置缓存
  • 它是衔接业务逻辑和底层API调用的一个组件,其派生出来的各种APIManager在各个APP上都可重用、可移植,方便代码管理
    blabla:同一个API不用重复写同一段调用代码和回调取数据逻辑,提高代码的组件化程度和集成度;Manager和Controller之间的关系可以不必非常紧密,切换API非常方便,降低了不必要的耦合(属于Model层)
    2.使用reformer(外观模式)
    a.在处理单View对多API,以及在单API对多View的情况时,reformer提供了非常优雅的手段来响应这种需求,隔离了转化逻辑和主体业务逻辑,避免了维护灾难。
    b.转化逻辑集中,且将转化次数转为只有一次。使用数据原型的转化逻辑至少有两次,第一次把json映射成对应model,第二次把model变成能被View处理的数据。而reformer一步到位。
    c.业务数据和业务有了适当的隔离,将来如果业务逻辑有修改,换一个reformer改掉就好;若其他业务也有相同的数据转化逻辑,其他业务直接拿着这个reformer就可以用了,不需要重写。

网络层优化方案

  • 安全机制
  • 请求优化(缓存,策略)
  • DNS缓存映射
  • 链接复用(SPDY)

参考:深度优化iOS网络模块
iOS网络优化之DNS映射

还可以做哪些?

  • 请求重发机制
  • 批量网络请求发送,并统一设置他们的回调
  • 设置有相互依赖的网络请求发送
    扩展提问:网络部分token过期,如何处理?

遇到token失效就扔个Notification出去。XAPIManager请求失败(token失效,且在BaseApiManager中判断)后,扔出通知,将通知的object设为当前失败的这个Manager也就是XManager。然后中间人收到通知,记录下随着Notification过来的Object,也就是失败的那个Manager。然后在当前ViewController中present登陆页面。中间人收到Token刷新成功的回调,拿出刚才拿到的失败的那个Manager,直接调用[XManager loadData],顺便dismiss掉登录页面,就好了。参数会由XManager的paramSource提供,你只管调loadData就好了,paramSource的重要性就体现在这里了。回调什么的也在你上一次失败时的页面内,整个过程完全无缝,且基本没有耦合