威立山

记录心路历程

0%

NSURLProtocol黑魔法

写在前面的话

实际应用:在公司的海外项目中,在App中嵌入YouTube网页,当进入一个视频详情页时,视频会自动播放。而需求是不要自动播放,让用户点击我们自定义的播放按钮把视频投到电视上播放。下面让我们一起学习下如何拦截网络请求。

URL Loading System

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

NSURLProtocol

NSURLProtocol可以让我们拦截程序中的一切网络请求,因为NSURLProtocol是一个虚基类,所以不能直接使用它,要想使用它就必须自定义一个类成为他的子类,然后实现他里面的必须实现的一些方法。主要进行如下拦截处理:

  • 自定义请求 和 响应
  • 过滤掉某些请求不让其发起、以及修改
  • 提供 自定义的全局缓存 逻辑
  • 重定向 网络请求
  • 提供 HTTP Mocking (方便前期测试)

如何拦截网络请求

注册自定义的URLProtocol子类

  • 在appDelegate中,注册自己拦截请求的URLProtocol子类;相对应的也有unregistClass方法,不让某个子类起作用
1
2
3
4
5
6
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {

[NSURLProtocol registerClass:[WSURLProtocol class]];

return YES;
}

子类必须实现的方法

  • + (BOOL)canInitWithRequest:(NSURLRequest *)request
    每次有一个请求的时候都会调用这个方法,在这个方法里面判断这个请求是否需要被处理拦截,如果返回YES就代表这个request需要被处理,反之就是不需要被处理。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
+ (BOOL)canInitWithRequest:(NSURLRequest *)request {
//判断是否处理过,防止死循环
if ([NSURLProtocol propertyForKey:protocolKey inRequest:request]) {
return NO;
}
//只拦截http和https请求
NSString *scheme = [[request URL] scheme];
if ([scheme caseInsensitiveCompare:@"http"] == NSOrderedSame ||
[scheme caseInsensitiveCompare:@"https"] == NSOrderedSame) {
return YES;
}

return NO;
}
  • + (NSURLRequest *) canonicalRequestForRequest:(NSURLRequest *)request 通常该方法你可以简单的直接返回request,但也可以在这里修改request,比如添加header,修改host等,并返回一个新的request,这是个抽象方法,子类必须实现。
1
2
3
4
5
6
+ (NSURLRequest *)canonicalRequestForRequest:	(NSURLRequest *)request {
NSMutableURLRequest *request = [request mutableCopy];
//把访问百度的request改为访问Google了
request.URL = [NSURL URLWithString:@"http://www.google.com"];
return request;
}
  • + (BOOL)requestIsCacheEquivalent:(NSURLRequest *)a toRequest:(NSURLRequest *)b 可用来使用缓存数据结束此次网络请求

  • - (void)startLoading 开始请求

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
- (void)startLoading {
NSMutableURLRequest *request = [self.request mutableCopy];
// 标记request已处理
[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 停止请求的方法,也是要实现的。

    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
-(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 setProperty:@(YES) forKey:protocolKey inRequest:request];
  • NSURLProtocol 可以拦截 NSURLConnection、NSURLSession、UIwebview 的请求,但是不能拦截WKwebview 和CFNetwork的请求
  • AFNetworking 3.0 以后版本拦截不到请求,session的创建方式不同,默认不起作用,解决方法

参考

田腾飞的博客
幻想乡