NSURLProtocol总结

NSURLProtocol总结

NSURLProtocol可以说是苹果允许的一个中间人攻击组件,功能非常强大。但是关于NSURLProtocol的官方文档不多,不过业内已经有很多人踩坑,网上相关的文章比较多,这里把网上搜集到的相关文章做一个汇总

NSURLProtocol介绍

NSURLProtocol是URL Loading System中的一个模块,能拦截这个系统中发出的所有请求,那具体是哪些类发出的请求呢?NSURLConnectionNSURLSessionUIWebVIew(页面内部的所有请求包括ajax)WKWebVIew(不能直接拦截,需要hack),以及基于他们的封装如AFNetworkingAlamofire等都是能拦截的,而基于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
3
NSURLSessionConfiguration *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
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
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47

#pragma mark - NSURLConnectionDelegate

- (void)connection:(NSURLConnection *)connection
didFailWithError:(NSError *)error {
[[self client] URLProtocol:self didFailWithError:error];
}

- (BOOL)connectionShouldUseCredentialStorage:(NSURLConnection *)connection {
return YES;
}

- (void)connection:(NSURLConnection *)connection
didReceiveAuthenticationChallenge:(NSURLAuthenticationChallenge *)challenge {
[[self client] URLProtocol:self didReceiveAuthenticationChallenge:challenge];
}

- (void)connection:(NSURLConnection *)connection
didCancelAuthenticationChallenge:(NSURLAuthenticationChallenge *)challenge {
[[self client] URLProtocol:self didCancelAuthenticationChallenge:challenge];
}

#pragma mark - NSURLConnectionDataDelegate
- (NSURLRequest *)connection:(NSURLConnection *)connection willSendRequest:(NSURLRequest *)request redirectResponse:(NSURLResponse *)response {
//并非重定向请求才会走这个方法,一般的请求也会回调到这里。如果不做判断话会导致把一般请求当成重定向,而且在ios8.1上会直接崩溃
if (response != nil){
self.response = response;
[[self client] URLProtocol:self wasRedirectedToRequest:request redirectResponse:response];
}
return request;
}

- (void)connection:(NSURLConnection *)connection didReceiveResponse:(NSURLResponse *)response {
[[self client] URLProtocol:self didReceiveResponse:response cacheStoragePolicy:NSURLCacheStorageAllowed];
}

- (void)connection:(NSURLConnection *)connection didReceiveData:(NSData *)data {
[[self client] URLProtocol:self didLoadData:data];
}

- (NSCachedURLResponse *)connection:(NSURLConnection *)connection willCacheResponse:(NSCachedURLResponse *)cachedResponse {
return cachedResponse;
}

- (void)connectionDidFinishLoading:(NSURLConnection *)connection {
[[self client] URLProtocolDidFinishLoading:self];
}

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会起作用。(还没做实验证实)

参考资料