田腾飞的博客

【iOS开发】黑魔法NSURLProtocol

最近也是比较忙,再加上自己也比较懒,所以博客好久没有更新了,其实真正的原因是我居然迷上了太极拳,😓真是服了自己了。不过技术还是要继续研究的,博客也也是要更新的,大牛指路仍然要继续。

今天分享一下NSURLProtocol,他在iOS编程技术中像是神一般的存在,今天我就来说说他。

URL Loading System


如图所示,URL Loading System是iOS一系列网络请求类的集合,包括已经过期不用的NSConnection和现在流行的NSURLSession,还包括一些请求认证的类,一个sessionConfig的类,还有关于处理请求缓存的类等,当然还包括我们要说的这个NSURLProtocol类。

对,我没说错,NSURLPtotocol类并不是一个protocol,他其实就是一个类,而且是一个“虚基类”-虚拟的父类吧。

URL Loading System可以发出的请求种类有ftp://,http://,https://,file://,data://请求。

NSURLProtocol的作用

NSURLProtocol可以拦截监听每一个URL Loading System中发出request请求,记住是URL Loading System中那些类发出的骑请求,如果不是这些类发出的请求,NSURLProtocol就没办法拦截和监听了。

NSURLProtocol的使用

因为NSURLProtocol是一个虚基类,所以不能直接使用它,要想使用它就必须自定义一个类成为他的子类,然后实现他里面的必须实现的一些方法,那么我们还要告诉系统:“喂,你发出的request,要让我的子类XXX类过一遍啊!”所以NSURLProtocol有一个register方法告诉系统那个子类要起作用。

1
2
3
4
5
6
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
[NSURLProtocol registerClass:[TFURLProtocol class]];
return YES;
}

相对应的也有unregistClass方法,不让某个子类起作用,这个起作用的时候并不是一定要在appDelegate中,你想要他在什么时候起作用,某个请求之前注册他就行,相应的不想他起作用就unregist他就行了。

子类必须实现的一些方法

  • (BOOL)canInitWithRequest:(NSURLRequest *)request

每次有一个请求的时候都会调用这个方法,在这个方法里面判断这个请求是否需要被处理拦截,如果返回YES就代表这个request需要被处理,反之就是不需要被处理。

1
2
3
4
5
6
7
8
9
10
11
12
13
+ (BOOL)canInitWithRequest:(NSURLRequest *)request {
if ([NSURLProtocol propertyForKey:protocolKey inRequest:request]) {
return NO;
}
NSString *scheme = [[request URL] scheme];
if ([scheme caseInsensitiveCompare:@"http"] == NSOrderedSame ||
[scheme caseInsensitiveCompare:@"https"] == NSOrderedSame) {
return YES;
}
return NO;
}
  • (NSURLRequest ) canonicalRequestForRequest:(NSURLRequest )request

这个方法就是返回规范的request,一般使用就是直接返回request,不做任何处理的

1
2
3
+ (NSURLRequest *)canonicalRequestForRequest:(NSURLRequest *)request {
return request;
}
  • (void)startLoading

这个方法作用很大,把当前请求的request拦截下来以后,在这个方法里面对这个request做各种处理,比如添加请求头,重定向网络,使用自定义的缓存等。作用非常之大。下面就是一个重定向的例子。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
/**
* 开始请求
*/
- (void)startLoading {
NSMutableURLRequest *request = [self.request mutableCopy];
//把访问百度的request改为访问Google了
request.URL = [NSURL URLWithString:@"http://www.google.com"];
[NSURLProtocol setProperty:@(YES) forKey:protocolKey inRequest:request];
//使用NSURLSession继续把重定向的request发送出去
NSURLSessionConfiguration *config = [NSURLSessionConfiguration ephemeralSessionConfiguration];
NSOperationQueue *mainQueue = [NSOperationQueue mainQueue];
NSURLSession *session = [NSURLSession sessionWithConfiguration:config delegate:self delegateQueue:mainQueue];
NSURLSessionDataTask *task = [session dataTaskWithRequest:request];
[task resume];
}
  • (void)stopLoading

相应的还有一个停止请求的方法,也是要实现的。

死循环的坑

有没有看到这两句代码?

1
2
3
if ([NSURLProtocol propertyForKey:protocolKey inRequest:request]) {
return NO;
}
1
[NSURLProtocol setProperty:@(YES) forKey:protocolKey inRequest:request];

这两句是为了防止死循环的,也是NSURLProtocol里必须写的方法。试想一下当我在startLoading的时候还会继续发出这个request,那么这个时候还是会拦截到这个request,然后进行处理,然后再次在startLoading中发送出去,然后继续拦截。。。。。。。。

所以在我们startLoading里面,我们对这个request进行标记,标记他已经被处理过了,然后在canInitWithRequest方法中根据这个标记拿到这个request,如果被标记了,就不再次进行处理了,如果没有标记过就要进行处理,这就很好的解决了死循环问题。

NSURLProtocolClient

如果我们使用UIWebView发送一个request,拦截以后当我们使用NSURLSession发出了request,那么这个request的response是无法回到这个UIWebView的,因为可以理解成不是同一个地方发出的request,这个response只能有session来处理,那我们怎么才能让这个response回到刚开始的UIWebView呢?

NSURLProtocolClient就可以看做是URL Loading System,我们把response告诉client,也就是URL Loading System,让他来继续处理这个response,因为一切都是基于URL Loading System发生的,所以把response交给他,他会自动处理这个response回到webView。

每一个NSURLProtocol的子类都有一个client对象来处理请求得到的response。其实下面这些写法都是差不多固定的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
-(void)URLSession:(NSURLSession *)session task:(NSURLSessionTask *)task didCompleteWithError:(NSError *)error {
if (error) {
[self.client URLProtocol:self didFailWithError:error];
} else {
[self.client URLProtocolDidFinishLoading:self];
}
}
-(void)URLSession:(NSURLSession *)session dataTask:(NSURLSessionDataTask *)dataTask didReceiveResponse:(NSURLResponse *)response completionHandler:(void (^)(NSURLSessionResponseDisposition))completionHandler {
[self.client URLProtocol:self didReceiveResponse:response cacheStoragePolicy:NSURLCacheStorageNotAllowed];
completionHandler(NSURLSessionResponseAllow);
}
-(void)URLSession:(NSURLSession *)session dataTask:(NSURLSessionDataTask *)dataTask didReceiveData:(NSData *)data {
[self.client URLProtocol:self didLoadData:data];
}
- (void)URLSession:(NSURLSession *)session dataTask:(NSURLSessionDataTask *)dataTask willCacheResponse:(NSCachedURLResponse *)proposedResponse completionHandler:(void (^)(NSCachedURLResponse *cachedResponse))completionHandler
{
completionHandler(proposedResponse);
}

总结

NSURLProtocol的一些坑

  1. 死循环
  2. 调试恶心。因为打开一个页面,里面的每一个请求包括网页图片等都会去走一遍子类中请求处理的判断方法,导致很多想调试的request找不到。
  3. WKWebView不起作用,因为WKWebView走得是WebKit内核,不走苹果这一套逻辑,目前貌似还没有有效的解决方法。

注意点

可以注册多个NSURLProtocol的子类,注册多个NSURLProtocol子类会逆序去执行,也就是先注册的子类后执行。

常见用法总结

  1. 重定向网络请求(已经举过例子了)
  2. 改变request的请求头
1
2
3
4
5
6
7
8
9
10
11
12
- (void)startLoading {
NSMutableURLRequest *request = [self.request mutableCopy];
//给请求头添加一个请求体
NSMutableDictionary *headers = [request.allHTTPHeaderFields mutableCopy];
[headers setObject:@"ttf" forKey:@"i am ttf"];
request.allHTTPHeaderFields = headers;
[NSURLProtocol setProperty:@(YES) forKey:protocolKey inRequest:request];
.....然后使用NSURLSession发送request
}
  1. 忽略网络请求使用本地缓存

首先自定一个URLResponse类,把资源转化为这个自定义类落地持久化,然后把这个类转换成URL Loading System可以接受的NSURLResponse类,发送给client,其实主要就是startLoading里面。

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
- (void) startLoading {
//1. 获取缓存的response
CachedURLResponse *cachedResponse = [self cachedResponseForCurrentRequest];
//2. 判断缓存response是否存在
if (cachedResponse) {
NSData *data = cachedResponse.data;
NSString *mimeType = cachedResponse.mimeType;
NSString *encoding = cachedResponse.encoding;
//构造一个新的response
NSURLResponse *response = [[NSURLResponse alloc] initWithURL:self.request.URL
MIMEType:mimeType
expectedContentLength:data.length
textEncodingName:encoding];
//将新的response作为request对应的response
[self.client URLProtocol:self
didReceiveResponse:response
cacheStoragePolicy:NSURLCacheStorageNotAllowed];
//设置request对应的 响应数据 response data
[self.client URLProtocol:self didLoadData:data];
//标记请求结束
[self.client URLProtocolDidFinishLoading:self];
} else {
NSMutableURLRequest *newRequest = [self.request mutableCopy];
[NSURLProtocol setProperty:@YES
forKey:MyURLProtocolHandledKey
inRequest:newRequest];
NSURLSessionConfiguration *config = [NSURLSessionConfiguration defaultSessionConfiguration];
NSOperationQueue *mainQueue = [NSOperationQueue mainQueue];
NSURLSession *session = [NSURLSession sessionWithConfiguration:config delegate:self delegateQueue:mainQueue];
NSURLSessionDataTask *task = [session dataTaskWithRequest:newRequest];
[task resume];
}
}

另外也可以参考一下“OHHTTPStubs的实现方式”,核心就是使用的NSURLProtocol。

小伙伴们如果感觉文章可以,可以关注博主博客

小伙伴们也可以关注博主微博,探索博主内心世界😁

如要转载请注明出处。

热评文章