详解iOS开发之NSURLProtocol的那些坑
NSURLProtocol
NSURLProtocol能够让你去重新定义苹果的URL加载系统(URLLoadingSystem)的行为,URLLoadingSystem里有许多类用于处理URL请求,比如NSURL,NSURLRequest,NSURLConnection和NSURLSession等,当URLLoadingSystem使用NSURLRequest去获取资源的时候,它会创建一个NSURLProtocol子类的实例,你不应该直接实例化一个NSURLProtocol,NSURLProtocol看起来像是一个协议,但其实这是一个类,而且必须使用该类的子类,并且需要被注册。
使用场景
不管你是通过UIWebView,NSURLConnection或者第三方库(AFNetworking,MKNetworkKit等),他们都是基于NSURLConnection或者NSURLSession实现的,因此你可以通过NSURLProtocol做自定义的操作。
- 重定向网络请求
- 忽略网络请求,使用本地缓存
- 自定义网络请求的返回结果
- 一些全局的网络请求设置
接触过iOS系统中URLLoadingSystem都知道,NSURLProtocol是如此地强大,可以拦截应用内几乎所有的网络请求(除了WKWebView),并可以修改请求头,返回client任意自定义的数据等等,据说很多做网络缓存都是利用这个类的。
那么,首先讲解一下NSURLProtocol怎么使用吧。
1.定义一个NSURLProtocol的子类
在继承NSURLProtocol中,我们需要实现
+(BOOL)canInitWithRequest:(NSURLRequest*)request,定义拦截请求的URL规则
-(void)startLoading,对于拦截的请求,系统创建一个NSURLProtocol对象执行startLoading方法开始加载请求
-(void)stopLoading,对于拦截的请求,NSURLProtocol对象在停止加载时调用该方法
+(NSURLRequest*)canonicalRequestForRequest:(NSURLRequest*)request,可选方法,对于需要修改请求头的请求在该方法中修改
下面代码定义了一个专门拦截https请求的NSURLProtocol子类,并通过CFHttpMessageRef重新请求
@interfaceCFHttpMessageURLProtocol(){ NSMutableURLRequest*curRequest; NSRunLoop*curRunLoop; NSInputStream*inputStream; } @end @implementationCFHttpMessageURLProtocol /** *是否拦截处理指定的请求 * *@paramrequest指定的请求 * *@return返回YES表示要拦截处理,返回NO表示不拦截处理 */ +(BOOL)canInitWithRequest:(NSURLRequest*)request{ /*防止无限循环,因为一个请求在被拦截处理过程中,也会发起一个请求,这样又会走到这里,如果不进行处理,就会造成无限循环*/ if([NSURLProtocolpropertyForKey:protocolKeyinRequest:request]){ returnNO; } NSString*url=request.URL.absoluteString; //如果url以https开头,则进行拦截处理,否则不处理 if([urlhasPrefix:@"https"]){ returnYES; } returnNO; } /** *如果需要对请求进行重定向,添加指定头部等操作,可以在该方法中进行 */ +(NSURLRequest*)canonicalRequestForRequest:(NSURLRequest*)request{ returnrequest; } /** *开始加载,在该方法中,加载一个请求 */ -(void)startLoading{ NSMutableURLRequest*request=[self.requestmutableCopy]; //表示该请求已经被处理,防止无限循环 [NSURLProtocolsetProperty:@(YES)forKey:protocolKeyinRequest:request]; curRequest=request; [selfstartRequest]; } /** *取消请求 */ -(void)stopLoading{ if(inputStream.streamStatus==NSStreamStatusOpen){ [inputStreamremoveFromRunLoop:curRunLoopforMode:NSRunLoopCommonModes]; [inputStreamsetDelegate:nil]; [inputStreamclose]; } [self.clientURLProtocol:selfdidFailWithError:[[NSErroralloc]initWithDomain:@"stoploading"code:-1userInfo:nil]]; }
以上代码中的startRequest方法是通过复制原始请求头,使用CFHttpMessageRef重新发起请求的,关于这部分的代码由于跟本文章内容关系不大,这里就先不放,有兴趣的朋友可以参考我的下一篇博客。
2.在网络请求前注册NSURLProtocol
//注册拦截请求的NSURLProtocol [NSURLProtocolregisterClass:[CFHttpMessageURLProtocolclass]];
对于NSURLSession的请求,注册NSURLProtocol的方式稍有不同,是通过NSURLSessionConfiguration注册的
//NSURLSession例子 NSURLSessionConfiguration*configuration=[NSURLSessionConfigurationdefaultSessionConfiguration]; NSArray*protocolArray=@[[CFHttpMessageURLProtocolclass]]; configuration.protocolClasses=protocolArray; NSURLSession*session=[NSURLSessionsessionWithConfiguration:configurationdelegate:selfdelegateQueue:[NSOperationQueuemainQueue]]; NSURLSessionTask*task=[sessiondataTaskWithRequest:_request]; [taskresume];
3.请求结束后注销NSURLProtocol
[NSURLProtocolunregisterClass:[CFHttpMessageURLProtocolclass]];
好了,到这里NSURLProtocol的使用方法大家应该有所了解了。下面主要讲一下NSURLProtocol在使用过程中可能会遇到的坑,给自己以及需要的朋友留个提醒。
1.上面一开始就已经说了,对于WebView的请求,目前NSURLProtocol还不能拦截WKWebView的请求,只能拦截UIWebview的,但后者好像AppStore已经不让审核通过了(尴尬脸)。
2.NSURLProtocol在拦截NSURLSession的POST请求时不能获取到Request中的HTTPBody,这个貌似早就国外的论坛上传开了,但国内好像还鲜有人知,据苹果官方的解释是Body是NSData类型,即可能为二进制内容,而且还没有大小限制,所以可能会很大,为了性能考虑,索性就拦截时就不拷贝了(内流满面脸)。为了解决这个问题,我们可以通过把Body数据放到Header中,不过Header的大小好像是有限制的,我试过2M是没有问题,不过超过10M就直接Requesttimeout了。。。而且当Body数据为二进制数据时这招也没辙了,因为Header里都是文本数据,另一种方案就是用一个NSDictionary或NSCache保存没有请求的Body数据,用URL为key,最后方法就是别用NSURLSession,老老实实用古老的NSURLConnection算了。。。
3.使用NSURLProtocol时,在那两个类方法可以发送同步网络请求,而实例方法,如startLoading则进入死锁,直至超时,原因是执行实例方法所在的线程并没有启动runloop,而NSURLConnection这些网络请求需要依赖于runloop的,因此这些请求根本发不出去,所以必须使用异步请求,NSURLConnection/NSURLSession的异步请求的线程保证启动了runloop。
以上就是我目前发现的坑,欢迎大家补充,也希望对大家开发有所帮助哈~所幸的是NSURLProtocol对于大量并发的请求支持的还不错,不然就要弃用了~希望对大家的学习有所帮助,也希望大家多多支持毛票票。