NSURLProtocol全攻略


title: NSURLProtocol 全攻略
author: 全凯
description: NSURLProtocol是URL Loading System的重要组成部分,具有非常强大的功能,本文全面介绍了NSURLProtocol的方方面面。
categories: iOS
date: 2017/02/15
tags:

  • iOS
  • 网络

一位著名的iOS大神Mattt Thompson在http://nshipster.com/nsurlprotocol/ 博客里说过,说“NSURLProtocol is both the most obscure and the most powerful part of the URL Loading System.”NSURLProtocol是URL Loading System中功能最强大也是最晦涩的部分。

这句话给了NSURLProtocol一个非常准确的定性。NSURLProtocol作为URL Loading System中的一个独立部分存在,能够拦截所有的URL Loading System发出的网络请求,拦截之后便可根据需要做各种自定义处理,是iOS网络层实现AOP(面向切面编程)的终极利器,所以功能和影响力都是非常强大的。但是关于NSURLProtocol的文档非常少,文档陈旧,包括苹果官方的文档也介绍得比较简单。而且,对于NSURLProtocol的使用,有坑的地方非常多。所以说它也是晦涩的并且是危险的。


什么是 NSURLProtocol

NSURLProtocol是URL Loading System的重要组成部分。
首先虽然名叫NSURLProtocol,但它却不是协议。它是一个抽象类。我们要使用它的时候需要创建它的一个子类。
NSURLProtocol在iOS系统中大概处于这样一个位置:

NSURLProtocol能拦截哪些网络请求

NSURLProtocol能拦截所有基于URL Loading System的网络请求。
这里先贴一张URL Loading System的图:


所以,可以拦截的网络请求包括NSURLSession,NSURLConnection以及UIWebVIew。
基于CFNetwork的网络请求,以及WKWebView的请求是无法拦截的。
现在主流的iOS网络库,例如AFNetworking,Alamofire等网络库都是基于NSURLSession或NSURLConnection的,所以这些网络库的网络请求都可以被NSURLProtocol所拦截。
还有一些年代比较久远的网络库,例如ASIHTTPRequest,MKNetwokit等网路库都是基于CFNetwork的,所以这些网络库的网络请求无法被NSURLProtocol拦截。


使用 NSURLProtocol

如上文所说,NSURLProtocol是一个抽象类。我们要使用它的时候需要创建它的一个子类。

@interface CustomURLProtocol : NSURLProtocol

使用NSURLProtocol的主要可以分为5个步骤:
注册—>拦截—>转发—>回调—>结束

注册:

对于基于NSURLConnection或者使用[NSURLSession sharedSession]创建的网络请求,调用registerClass方法即可。

[NSURLProtocol registerClass:[NSClassFromString(@"CustomURLProtocol") class]];

对于基于NSURLSession的网络请求,需要通过配置NSURLSessionConfiguration对象的protocolClasses属性。

NSURLSessionConfiguration *sessionConfiguration = [NSURLSessionConfiguration defaultSessionConfiguration];
sessionConfiguration.protocolClasses = @[[NSClassFromString(@"CustomURLProtocol") class]];

拦截:

在拦截到网络请求后,NSURLProtocol会依次执行下列方法:

+ (BOOL)canInitWithRequest:(NSURLRequest *)request

该方法会拿到request的对象,我们可以通过该方法的返回值来筛选request是否需要被NSURLProtocol做拦截处理。
比如:

+ (BOOL)canInitWithRequest:(NSURLRequest *)request {
    
    NSString * scheme = [[request.URL scheme] lowercaseString];
    
    if ([scheme isEqual:@"http"]) {
        return YES;
    }
    return NO;
}

这里我们就只会拦截http的请求。

+ (NSURLRequest *)canonicalRequestForRequest:(NSURLRequest *)request

在该方法中,我们可以对request进行处理。例如修改头部信息等。最后返回一个处理后的request实例。

转发:

在拦截到网络请求,并且对网络请求进行定制处理以后。我们需要将网络请求重新发送出去。

- (id)initWithRequest:(NSURLRequest *)request cachedResponse:(NSCachedURLResponse *)cachedResponse client:(id<NSURLProtocolClient>)client

该方法会创建一个NSURLProtocol实例,这里每一个网络请求都会创建一个新的实例。

- (void)startLoading

接下来就是转发的核心方法startLoading。在该方法中,我们把处理过的request重新发送出去。至于发送的形式,可以是基于NSURLConnection,NSURLSession甚至CFNetwork。

回调:

既是面向切面的编程,就不能影响到原来网络请求的逻辑。所以上一步将网络请求转发出去以后,当收到网络请求的返回,还需要再将返回值返回给原来发送网络请求的地方。
主要需要需要调用到

[self.client URLProtocol:self didFailWithError:error];
[self.client URLProtocolDidFinishLoading:self];
[self.client URLProtocol:self didReceiveResponse:response cacheStoragePolicy:NSURLCacheStorageNotAllowed];
[self.client URLProtocol:self didLoadData:data];

这四个方法来回调给原来发送网络请求的地方。
这里假设我们在转发过程中是使用NSURLSession发送的网络请求,那么在NSURLSession的回调方法中,我们做相应的处理即可。并且我们也可以对这些返回,进行定制化处理。

- (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];
}

结束:

在一个网络请求完全结束以后,NSURLProtocol回调用到

- (void)stopLoading

在该方法里,我们完成在结束网络请求的操作。以NSURLSession为例:

- (void)stopLoading {
    [self.session invalidateAndCancel];
    self.session = nil;
}

以上便是NSURLProtocol的基本流程。


应用:

既然NSURLProtocol功能非常强大,那么在具体开发中,会有哪些应用呢?

  • 网络请求缓存
  • 网络请求mock stub,知名的库OHHTTPStubs就是基于NSURLProtocol
  • 网络相关的数据统计
  • URL重定向
  • 配合实现HTTPDNS
  • ......

坑&注意事项:

使用NSURLProtocol碰到的坑也特别多,有的是很少有文档提及所以没有注意到的,有的甚至是至今还没解释的。下面列举一些我碰到的问题:

多个NSURLProtocol嵌套使用

若一个项目中存在多个NSURLProtocol,那么NSURLProtocol的拦截顺序跟注册的方式和顺序有关。
*对于使用registerClass方法注册的情况:
多个NSURLProtocol拦截顺序为注册顺序的反序,即后注册的的NSURLProtocol先拦截。
*对于通过配置NSURLSessionConfiguration对象的protocolClasses属性来注册的情况:
protocolClasses这个数组里只有第一个NSURLProtocol会起作用。
所以我们看到OHHTTPStubs库在注册的时候进行了这样的处理:

+ (void)setEnabled:(BOOL)enable forSessionConfiguration:(NSURLSessionConfiguration*)sessionConfig
{
    // Runtime check to make sure the API is available on this version
    if (   [sessionConfig respondsToSelector:@selector(protocolClasses)]
        && [sessionConfig respondsToSelector:@selector(setProtocolClasses:)])
    {
        NSMutableArray * urlProtocolClasses = [NSMutableArray arrayWithArray:sessionConfig.protocolClasses];
        Class protoCls = OHHTTPStubsProtocol.class;
        if (enable && ![urlProtocolClasses containsObject:protoCls])
        {
            [urlProtocolClasses insertObject:protoCls atIndex:0];
        }
        else if (!enable && [urlProtocolClasses containsObject:protoCls])
        {
            [urlProtocolClasses removeObject:protoCls];
        }
        sessionConfig.protocolClasses = urlProtocolClasses;
    }
    else
    {
        NSLog(@"[OHHTTPStubs] %@ is only available when running on iOS7+/OSX9+. "
              @"Use conditions like 'if ([NSURLSessionConfiguration class])' to only call "
              @"this method if the user is running iOS7+/OSX9+.", NSStringFromSelector(_cmd));
    }
}

就是把自己的NSURLProtocol插入到protocolClasses的第一个,进行拦截。拦截完成之后,又进行移除。

关于不能拦截WKWebView

原因是WKWebView 在独立于 app 进程之外的进程中执行网络请求,请求数据不经过主进程,因此,在 WKWebView 上直接使用 NSURLProtocol 无法拦截请求。
具体可以参考 wkwebview的那些坑这篇文章。文章也给出了不算完美的解决方案。

canInitWithRequest方法多次调用

偶尔会出现canInitWithRequest方法多次调用的情况,这个问题出现非常的奇怪,目前还不清楚原因。但是因为我们在canInitWithRequest方法中会判断是否拦截过的标记。所以这个问题不会影响到正常使用。另外还发现,当我们在进行网络请求之前把缓存清除掉,也不会出现这个问题。

使用NSURLSession的坑

在NSURLProtocol中使用NSURLSession有很多莫名其妙的问题,基本上都是系统的bug。
我们可以在http://www.openradar.me/search?query=nsurlprotocol 这里看到关于NSURLProtocol的系统bug,基本都与NSURLSession有关。比较明显的就是:

  • 拦截到的Request中的HTTPBody为nil;
  • startLoading在某些特殊情况会出现死锁;
  • 关于注册registerClass方法只适用于sharedSession创建的网络请求;
  • ……

这些问题都是在使用NSURLProtocol需要特别注意的。


总结:

NSURLProtocol的强大功能,为iOS网络开发提供了非常大的可操作空间。在商业项目中,也得到了广泛的应用,但我们在应用的同时,也要注意避免NSURLProtocol存在的问题。不过好在随着iOS系统的发展,关于NSURLProtocol的系统bug已经越来越少。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 159,015评论 4 362
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,262评论 1 292
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 108,727评论 0 243
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,986评论 0 205
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,363评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,610评论 1 219
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,871评论 2 312
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,582评论 0 198
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,297评论 1 242
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,551评论 2 246
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,053评论 1 260
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,385评论 2 253
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,035评论 3 236
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,079评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,841评论 0 195
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,648评论 2 274
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,550评论 2 270

推荐阅读更多精彩内容