详解iOS开发之NSURLProtocol的那些坑
nsurlprotocol
nsurlprotocol能够让你去重新定义苹果的url加载系统 (url loading system)的行为,url loading system里有许多类用于处理url请求,比如nsurl,nsurlrequest,nsurlconnection和nsurlsession等,当url loading system使用nsurlrequest去获取资源的时候,它会创建一个nsurlprotocol子类的实例,你不应该直接实例化一个nsurlprotocol,nsurlprotocol看起来像是一个协议,但其实这是一个类,而且必须使用该类的子类,并且需要被注册。
使用场景
不管你是通过uiwebview, nsurlconnection 或者第三方库 (afnetworking, mknetworkkit等),他们都是基于nsurlconnection或者 nsurlsession实现的,因此你可以通过nsurlprotocol做自定义的操作。
- 重定向网络请求
- 忽略网络请求,使用本地缓存
- 自定义网络请求的返回结果
- 一些全局的网络请求设置
接触过ios系统中url loading system都知道,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重新请求
@interface cfhttpmessageurlprotocol () <nsstreamdelegate> { nsmutableurlrequest *currequest; nsrunloop *currunloop; nsinputstream *inputstream; } @end @implementation cfhttpmessageurlprotocol /** * 是否拦截处理指定的请求 * * @param request 指定的请求 * * @return 返回yes表示要拦截处理,返回no表示不拦截处理 */ + (bool)caninitwithrequest:(nsurlrequest *)request { /* 防止无限循环,因为一个请求在被拦截处理过程中,也会发起一个请求,这样又会走到这里,如果不进行处理,就会造成无限循环 */ if ([nsurlprotocol propertyforkey:protocolkey inrequest:request]) { return no; } nsstring *url = request.url.absolutestring; // 如果url以https开头,则进行拦截处理,否则不处理 if ([url hasprefix:@"https"]) { return yes; } return no; } /** * 如果需要对请求进行重定向,添加指定头部等操作,可以在该方法中进行 */ + (nsurlrequest *)canonicalrequestforrequest:(nsurlrequest *)request { return request; } /** * 开始加载,在该方法中,加载一个请求 */ - (void)startloading { nsmutableurlrequest *request = [self.request mutablecopy]; // 表示该请求已经被处理,防止无限循环 [nsurlprotocol setproperty:@(yes) forkey:protocolkey inrequest:request]; currequest = request; [self startrequest]; } /** * 取消请求 */ - (void)stoploading { if (inputstream.streamstatus == nsstreamstatusopen) { [inputstream removefromrunloop:currunloop formode:nsrunloopcommonmodes]; [inputstream setdelegate:nil]; [inputstream close]; } [self.client urlprotocol:self didfailwitherror:[[nserror alloc] initwithdomain:@"stop loading" code:-1 userinfo:nil]]; }
以上代码中的startrequest方法是通过复制原始请求头,使用cfhttpmessageref重新发起请求的,关于这部分的代码由于跟本文章内容关系不大,这里就先不放,有兴趣的朋友可以参考我的下一篇博客。
2. 在网络请求前注册nsurlprotocol
// 注册拦截请求的nsurlprotocol [nsurlprotocol registerclass:[cfhttpmessageurlprotocol class]];
对于nsurlsession的请求,注册nsurlprotocol的方式稍有不同,是通过nsurlsessionconfiguration注册的
// nsurlsession例子 nsurlsessionconfiguration *configuration = [nsurlsessionconfiguration defaultsessionconfiguration]; nsarray *protocolarray = @[ [cfhttpmessageurlprotocol class] ]; configuration.protocolclasses = protocolarray; nsurlsession *session = [nsurlsession sessionwithconfiguration:configuration delegate:self delegatequeue:[nsoperationqueue mainqueue]]; nsurlsessiontask *task = [session datataskwithrequest:_request]; [task resume];
3. 请求结束后注销nsurlprotocol
[nsurlprotocol unregisterclass:[cfhttpmessageurlprotocol class]];
好了,到这里nsurlprotocol的使用方法大家应该有所了解了。下面主要讲一下nsurlprotocol在使用过程中可能会遇到的坑,给自己以及需要的朋友留个提醒。
1. 上面一开始就已经说了,对于webview的请求,目前nsurlprotocol还不能拦截wkwebview的请求,只能拦截uiwebview的,但后者好像appstore已经不让审核通过了(尴尬脸)。
2. nsurlprotocol在拦截nsurlsession的post请求时不能获取到request中的httpbody,这个貌似早就国外的论坛上传开了,但国内好像还鲜有人知,据苹果官方的解释是body是nsdata类型,即可能为二进制内容,而且还没有大小限制,所以可能会很大,为了性能考虑,索性就拦截时就不拷贝了(内流满面脸)。为了解决这个问题,我们可以通过把body数据放到header中,不过header的大小好像是有限制的,我试过2m是没有问题,不过超过10m就直接request timeout了。。。而且当body数据为二进制数据时这招也没辙了,因为header里都是文本数据,另一种方案就是用一个nsdictionary或nscache保存没有请求的body数据,用url为key,最后方法就是别用nsurlsession,老老实实用古老的nsurlconnection算了。。。
3. 使用nsurlprotocol时,在那两个类方法可以发送同步网络请求,而实例方法,如startloading则进入死锁,直至超时,原因是执行实例方法所在的线程并没有启动runloop,而nsurlconnection这些网络请求需要依赖于runloop的,因此这些请求根本发不出去,所以必须使用异步请求,nsurlconnection/nsurlsession的异步请求的线程保证启动了runloop。
以上就是我目前发现的坑,欢迎大家补充,也希望对大家开发有所帮助哈~所幸的是nsurlprotocol对于大量并发的请求支持的还不错,不然就要弃用了~希望对大家的学习有所帮助,也希望大家多多支持。