NSURLProtocol和NSRunLoop的那些坑
转自:http://xiangwangfeng.com/2014/11/29/NSURLProtocol%E5%92%8CNSRunLoop%E7%9A%84%E9%82%A3%E4%BA%9B%E5%9D%91/
参考:http://www.raywenderlich.com/59982/nsurlprotocol-tutorial
最近用AFNetworking替换掉了工程里的ASIHttpRequest,结果陆续碰到很多问题:
- 如何统一地添加全局的HTTP头(不仅仅是UA而已)
- 如何优雅地进行流量统计
- 对特定的地址进行CDN加速(URL到IP的替换)
- 怎么实现HTTP的同步请求
前三个需求对于ASIHttpReqeust来说都不是问题,只需要在几个统一的点进行修改即可。而使用AFNetworking后就没有那么容易了:一方面AFNetworking中生成NSURLRequest的点比较多,并没有一个统一的路径。其次工程中会有部分直接使用NSURLConnecion的场景,无法统一。经cyzju提醒发现了NSURLProtocol这个大杀器,可惜对应的文档过于简略,唯一比较详细的介绍就只有RW的这篇教程而已,掉了很多坑,值得记上一笔。
NSURLProtocol
概念
NSURLProtocol也是苹果众多黑魔法中的一种,使用它可以轻松地重定义整个URL Loading System。当你注册自定义NSURLProtocol后,就有机会对所有的请求进行统一的处理,基于这一点它可以让你
- 自定义请求和响应
- 提供自定义的全局缓存支持
- 重定向网络请求
- 提供HTTP Mocking (方便前期测试)
- 其他一些全局的网络请求修改需求
使用方法
继承NSURLPorotocl,并注册你的NSURLProtocol
[NSURLProtocol registerClass:[YXURLProtocol class]];
当NSURLConnection准备发起请求时,它会遍历所有已注册的NSURLProtocol,询问它们能否处理当前请求。所以你需要尽早注册这个Protocol。
实现NSURLProtocol的相关方法
当遍历到我们自定义的NSURLProtocol时,系统先会调用canInitWithRequest:这个方法。顾名思义,这是整个流程的入口,只有这个方法返回YES我们才能够继续后续的处理。我们可以在这个方法的实现里面进行请求的过滤,筛选出需要进行处理的请求。
+ (BOOL)canInitWithRequest:(NSURLRequest *)request
{
if ([NSURLProtocol propertyForKey:YXURLProtocolHandled inRequest:request])
{
return NO;
}
NSString *scheme = [[request URL] scheme];
NSDictionary *dict = [request allHTTPHeaderFields];
return [dict objectForKey:@"custom_header"] == nil &&
([scheme caseInsensitiveCompare:@"http"] == NSOrderedSame ||
[scheme caseInsensitiveCompare:@"https"] == NSOrderedSame);
}
当筛选出需要处理的请求后,就可以进行后续的处理,需要至少实现如下4个方法
+ (NSURLRequest *)canonicalRequestForRequest:(NSURLRequest *)request
{
return request;
}
+ (BOOL)requestIsCacheEquivalent:(NSURLRequest *)a
toRequest:(NSURLRequest *)b
{
return [super requestIsCacheEquivalent:a toRequest:b];
}
- (void)startLoading
{
NSMutableURLRequest *mutableReqeust = [[self request] mutableCopy];
[YXURLProtocol applyCustomHeaders:mutableReqeust];
[NSURLProtocol setProperty:@(YES)
forKey:YXURLProtocolHandled
inRequest:mutableReqeust];
self.connection = [NSURLConnection connectionWithRequest:mutableReqeust
delegate:self];
}
- (void)stopLoading
{
[self.connection cancel];
self.connection = nil;
}
- canonicalRequestForRequest: 返回规范化后的request,一般就只是返回当前request即可。
- requestIsCacheEquivalent:toRequest: 用于判断你的自定义reqeust是否相同,这里返回默认实现即可。它的主要应用场景是某些直接使用缓存而非再次请求网络的地方。
- startLoading和stopLoading 实现请求和取消流程。
实现NSURLConnectionDelegate和NSURLConnectionDataDelegate
因为在第二步中我们接管了整个请求过程,所以需要实现相应的协议并使用NSURLProtocolClient将消息回传给URL Loading System。在我们的场景中推荐实现所有协议。
- (void)connection:(NSURLConnection *)connection
didFailWithError:(NSError *)error
{
[self.client URLProtocol:self
didFailWithError:error];
}
- (NSURLRequest *)connection:(NSURLConnection *)connection willSendRequest:(NSURLRequest *)request redirectResponse:(NSURLResponse *)response
{
if (response != nil)
{
[[self client] URLProtocol:self wasRedirectedToRequest:request redirectResponse:response];
}
return request;
}
- (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];
}
- (void)connection:(NSURLConnection *)connection
didReceiveResponse:(NSURLResponse *)response
{
[self.client URLProtocol:self
didReceiveResponse:response
cacheStoragePolicy:[[self request] cachePolicy]];
}
- (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