NSURLProtocol总结
NSURLProtocol可以说是苹果允许的一个中间人攻击组件,功能非常强大。但是关于NSURLProtocol的官方文档不多,不过业内已经有很多人踩坑,网上相关的文章比较多,这里把网上搜集到的相关文章做一个汇总
NSURLProtocol介绍
NSURLProtocol是URL Loading System中的一个模块,能拦截这个系统中发出的所有请求,那具体是哪些类发出的请求呢?NSURLConnection
、NSURLSession
、UIWebVIew(页面内部的所有请求包括ajax)
、WKWebVIew(不能直接拦截,需要hack)
,以及基于他们的封装如AFNetworking
、Alamofire
等都是能拦截的,而基于CFNetwork
的请求则不能被拦截
进行拦截以后可以做非常多的自定义行为,比如说:
- 拦截图片加载请求,转为从本地文件加载
- 对HTTP返回内容进行mock和stub
- 对发出请求的header进行格式化
- 对发出的媒体请求进行签名
- 创建本地代理服务,用于数据变化时对URL请求的更改
- 故意制造畸形或非法返回数据来测试程序的鲁棒性
- 过滤请求和返回中的敏感信息
- 在既有协议基础上完成对 NSURLConnection 的实现且与原逻辑不产生矛盾
NSURLProtocol的用法
新建、注册子类
NSURLProtocol
是一个抽象类,必须被子类化之后才能使用,所以要新建一个继承于它的子类。
基于NSURLConnection或者使用[NSURLSession sharedSession]创建的网络请求,调用registerClass方法即可1
2[NSURLProtocol registerClass:NetworkURLProtocol.class]; //注册
[NSURLProtocol unregisterClass:NetworkURLProtocol.class]; //注销
而基于NSURLSession的网络请求,需要通过配置NSURLSessionConfiguration对象的protocolClasses属性来注册。(网络上有文章说protocolClasses这个数组里只有第一个NSURLProtocol会起作用。并以OHHTTPStubs库为例子,它是在注册先NSURLProtocol插入到protocolClasses的第一个,进行拦截。拦截完成之后又进行移除。这个还没有做实验证实)1
2
3NSURLSessionConfiguration *sessionConfiguration = [NSURLSessionConfiguration defaultSessionConfiguration];
sessionConfiguration.protocolClasses = @[NetworkURLProtocol.class];
//取消注册的话直接把protocol从数组中移除就行
重载必要的方法
(1)当遍历到我们自定义的NSURLProtocol时,系统先会调用canInitWithRequest:
这个方法。顾名思义,这是整个流程的入口,只有这个方法返回YES我们才能够继续后续的处理。我们可以在这个方法的实现里面进行请求的过滤,筛选出需要进行处理的请求1
2
3
4
5
6
7
8
9
10
11
12
13
14+ (BOOL)canInitWithRequest:(NSURLRequest *)request {
//是否有已编辑的标识,用来识别是第一次过来的请求还是已经改写过的请求
BOOL isEditedRequest = [[NSURLProtocol propertyForKey:@"NetworkURLProtocol" inRequest:request] boolValue];
//根据配置来过滤请求
BOOL configDeny = ![[NetworkManager sharedManager].config canCaptureRequest:request];
if (isEditedRequest || configDeny) {
return NO;
}
return YES;
}
(2)canInitWithRequest
返回YES后系统会把请求回调到这里。在这里完成请求的编辑/替换工作。需要注意的是系统会以这个方法返回值为参数,再次调用上面说到的canInitWithRequest
方法,所以这里一定要添加一些标志变量来区分请求是否被编辑,不然系统就会因为循环调用这两个方法而陷入死循环1
2
3
4
5
6
7
8+ (NSURLRequest *)canonicalRequestForRequest:(NSURLRequest *)request
{
NSMutableURLRequest *mutableReqeust = [request mutableCopy];
[NSURLProtocol setProperty:@YES
forKey:@"NetworkURLProtocol"
inRequest:mutableReqeust];
return [mutableReqeust copy];
}
(3)处理开始和结束的状态1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16//Request被编辑完后,系统会调用这个方法,这里一般用来发起请求
//这里方法里一定要有手动发起网络请求的逻辑,否则整个流程就不会往下走
- (void)startLoading
{
self.connection = [[NSURLConnection alloc] initWithRequest:[[self class] canonicalRequestForRequest:self.request] delegate:self startImmediately:YES];
}
//请求结束后会调用这个方法,在这里取消请求,以及做一些数据处理工作
- (void)stopLoading
{
[self.connection cancel];
}
//用于判断你的自定义reqeust是否相同,这里返回默认实现即可。主要应用场景是某些直接使用缓存而非再次请求网络的地方。如果不是用来做缓存的话这个方法可以不实现
+ (BOOL)requestIsCacheEquivalent:(NSURLRequest *)a toRequest:(NSURLRequest *)b
{
return [super requestIsCacheEquivalent:a toRequest:b];
}
调用NSURLProtocolClient协议方法
NSURLProtocol给了一次机会让我们接管请求,我们在完成接管的过程中还是需要向我们的客户端(发起请求的业务代码)提供服务。NSURLProtocol实例中有个id <NSURLProtocolClient> client
属性来帮我们做到这一点,我们只需要在合适的时机来调用NSURLProtocolClient
协议规定的方法就行。
注意我们的模拟行为需要尽可能的和系统保持一致,所以NSURLProtocolClient
协议规定的方法最好在对应的时机都要有相应的调用,换句话说就是NSURLProtocolClient
协议规定的每一个方法都至少调用一遍
1 |
|
NSURLProtocol的注意事项
(1) NSURLProtocol多线程问题。有同学可能会想到网络请求中多个request都会走NSURLProtocol的代理方法,那会不会有并发的问题。其实每个网络请求都会实例化一个NSURLProtocol的子类对象,所以每个网络请求之间不会互相影响
(2) NSURLProtocolClient回调必须跟发起请求的代码发送保持在一个线程、相同的Runloop。这个也很好理解,因为我们的改写逻辑对客户端(一般是业务代码)应该是透明的,所以行为和状态都应该和系统的网络请求框架保持一致:因此我们要在start方法中记录当前线程和Runloop模式。然后在记录的线程以相同的Runloop模式回调NSURLProtocolClient的方法1
[self performSelector:onThread:withObject:waitUntilDone:modes:];
(3) NSURLSession的POST请求拿不到HTTPBody。苹果官方的解释是Body是NSData类型,而且还没有大小限制。为了性能考虑,拦截时就没有拷贝
(4) WKWebview不能直接拦截。WKWebView在独立于app进程之外的进程中执行网络请求,请求数据不经过主进程,因此在WKWebView上直接使用NSURLProtocol无法拦截请求。需要hack一下,具体的技术方案请看”让WKWebView支持NSURLProtocol“
(5) 多个NSURLProtocol嵌套使用。若一个项目中存在多个NSURLProtocol,那么NSURLProtocol的拦截顺序跟注册的方式和顺序有关。
对于使用registerClass方法注册:多个NSURLProtocol拦截顺序为注册顺序的反序,即后注册的的NSURLProtocol先拦截。
对于通过配置NSURLSessionConfiguration对象的protocolClasses属性来注册的:
protocolClasses这个数组里据说只有第一个NSURLProtocol会起作用。(还没做实验证实)