达达快送iOS版网络AOP方案探索与实践
引言
一、AOP简介
AOP(Aspect Oriented Programming),意为面向切面编程,它是面向对象编程范式的一种补充。AOP在iOS下的应用比较广泛,较为常见的是无痕埋点,安全保护等。而网络AOP主要是在网络层切面拦截,便于对请求和响应做二次处理。
二、网络AOP的方案对比
1.通过网络框架来实现AOP
-
AFNetWorking
AFN中提供了一套基于task各状态的通知,可以满足一些监控、日志的需求。
-
Moya
Moya是Alamorfire基础上的封装,利用Swift的协议能力,实现了一套POP的网络库。它还提供了可选化插件的接口,通过遵守PluginType,可以对请求的Request和返回的Response拦截和修改,这也是AOP的思想体现。
-
自定义网络框架
2.使用runtime方案hook
3.NSURLProtocol方案
|
方案 |
核心思路 |
优点 |
缺点 |
|
|
监听AFN提供的各时机通知,获取相应的数据 |
1.代码量很小。 2.结构依靠AFN提供的通知串联,逻辑简单 |
1.只可监控AFN发起的请求。 2.不能修改请求/响应数据。 |
|
基于Moya的实现 |
实现PluginType协议,操作相应数据 |
1.moya是gitHub上的高星项目,开发者持续维护,稳定性强。 2.基于协议实现,对swift有很好的支持。 3.支持对请求/响应数据的读写。 |
1.Moya只能在Swift项目中使用。 2.只限于moya框架。 |
|
基于自定义网络框架的实现 |
在统一出入口处制作一个切面,通过通知/代理等方式实现转发功能 |
1.实现方式自由,出现问题易于定位。 2.自定义框架可以针对app做更多优化。 3.支持对请求/响应数据的读写。 |
1.对于小公司来说成本相对较高。 2.由于是在自定义框架之上做拦截处理,对于其他网络库及自带请求的一些sdk将没有拦截能力。 |
|
基于runtime的实现 |
对NSURLSession等系统请求类进行方法交换 |
1.方案思路明确,易于理解。 2.基于底层实现,对上层代码无影响 3.支持对请求/响应数据的读写。 |
1.需要交换的方法数量很多,代码量很大。 2.实现复杂,风险较大。 |
|
基于NSURLProtocol |
实现NSURLProtocol子类并注册,系统会在请求发出时自动创建实例来处理请求 |
1.苹果推荐 2.可移植 3.支持对请求/响应数据的读写。 |
对于同一个请求,只有一个NSURLProtocol可以响应并处理。 |
三、网络AOP方案在达达app的使用情况
基于达达app多个网络框架并用的现状,通过网络框架来实现AOP的三个方案显然都不适用。那只能从runtime和NSURLProtocol两种方案里面进行选择,考虑到runtime方案较为复杂且风险较大,而NSURLProtocol又是业内最为通用的方案,我们决定使用NSURLProtocol方案。
1.整体结构
当我们实现并注册了一个NSURLProtocol子类时,所有基于URL Loading System的网络请求都将被该类拦截,如果不搭建一个转发体系,当我们有多个功能需要借助NSURLProtocol实现的话,势必会使这个类膨胀,代码难以维护。因此更好的方案是将NSURLProtocol视作一个入口,在此基础上封装一套转发机制,新增的功能以插件的形式插入。
当NSURLProtocol拦截到请求时,会将请求转发到拦截器中,各功能插件实现拦截器协议并注册到拦截器中,拦截器协议定义了两个关键节点函数,给插件提供访问request和response的时机。
多插件注册时,插件的优先级会在很大程度上影响请求。举个例子,当流量插件优先级高于加密插件时,流量的统计就是不准确的(加密会增大请求体体积)。但过于复杂的优先级体系又将增加插件的设计和使用难度,最终决定由插件的注册顺序决定它的调用顺序。这对于调用方来说也是最直观明了的。
2.设计思路
3.开发中遇到的问题
问题1:拦截到的HTTPBody为空
post请求的request.HTTPBody是空的,我们需要从request的HTTPBodyStream中取出并赋值给HTTPBody
if ([method isEqualToString:@"post"]) {newRequest = request.mutableCopy;NSMutableData *httpBody = [NSMutableData data];NSInputStream *stream = request.HTTPBodyStream;[stream open];NSInteger maxLength = 1024;uint8_t buffer[maxLength];BOOL isReadEnd = NO;while (!isReadEnd) {NSInteger readLength = [stream read:buffer maxLength:maxLength];if (stream.streamError == nil && readLength > 0) {[httpBody appendBytes:buffer length:readLength];} else {isReadEnd = YES;}}}
问题2: 本地的网络拥塞,导致后续请求超时
NSMutableData *httpBody = [NSMutableData data];NSInputStream *stream = request.HTTPBodyStream;[stream open];NSInteger maxLength = 1024;uint8_t buffer[maxLength];BOOL isReadEnd = NO;while (!isReadEnd) {NSInteger readLength = [stream read:buffer maxLength:maxLength];if (stream.streamError == nil && readLength > 0) {[httpBody appendBytes:buffer length:readLength];} else {isReadEnd = YES;}}[stream close];newRequest.HTTPBody = httpBody;
结语
通过几种AOP方案,我们很容易构建出一个易拓展,低风险的针对原生的网络切面,可以无侵入的对请求做二次处理。然而由于WKWebview的特殊性,上述方案无法很好地完成网页请求的拦截,下一次,我们可以一起探讨WKWebview的拦截,实现真正“无死角”网络切面。