NSURLProtocol全攻略


title: NSURLProtocol 全攻略
author: 全凱
description: NSURLProtocol是URL Loading System的重要組成部分,具有非常強(qiáng)大的功能,本文全面介紹了NSURLProtocol的方方面面。
categories: iOS
date: 2017/02/15
tags:

  • iOS
  • 網(wǎng)絡(luò)

一位著名的iOS大神Mattt Thompson在http://nshipster.com/nsurlprotocol/ 博客里說(shuō)過(guò),說(shuō)“NSURLProtocol is both the most obscure and the most powerful part of the URL Loading System.”NSURLProtocol是URL Loading System中功能最強(qiáng)大也是最晦澀的部分。

這句話給了NSURLProtocol一個(gè)非常準(zhǔn)確的定性。NSURLProtocol作為URL Loading System中的一個(gè)獨(dú)立部分存在,能夠攔截所有的URL Loading System發(fā)出的網(wǎng)絡(luò)請(qǐng)求,攔截之后便可根據(jù)需要做各種自定義處理,是iOS網(wǎng)絡(luò)層實(shí)現(xiàn)AOP(面向切面編程)的終極利器,所以功能和影響力都是非常強(qiáng)大的。但是關(guān)于NSURLProtocol的文檔非常少,文檔陳舊,包括蘋果官方的文檔也介紹得比較簡(jiǎn)單。而且,對(duì)于NSURLProtocol的使用,有坑的地方非常多。所以說(shuō)它也是晦澀的并且是危險(xiǎn)的。


什么是 NSURLProtocol

NSURLProtocol是URL Loading System的重要組成部分。
首先雖然名叫NSURLProtocol,但它卻不是協(xié)議。它是一個(gè)抽象類。我們要使用它的時(shí)候需要?jiǎng)?chuàng)建它的一個(gè)子類。
NSURLProtocol在iOS系統(tǒng)中大概處于這樣一個(gè)位置:

NSURLProtocol能攔截哪些網(wǎng)絡(luò)請(qǐng)求

NSURLProtocol能攔截所有基于URL Loading System的網(wǎng)絡(luò)請(qǐng)求。
這里先貼一張URL Loading System的圖:


所以,可以攔截的網(wǎng)絡(luò)請(qǐng)求包括NSURLSession,NSURLConnection以及UIWebVIew。
基于CFNetwork的網(wǎng)絡(luò)請(qǐng)求,以及WKWebView的請(qǐng)求是無(wú)法攔截的。
現(xiàn)在主流的iOS網(wǎng)絡(luò)庫(kù),例如AFNetworking,Alamofire等網(wǎng)絡(luò)庫(kù)都是基于NSURLSession或NSURLConnection的,所以這些網(wǎng)絡(luò)庫(kù)的網(wǎng)絡(luò)請(qǐng)求都可以被NSURLProtocol所攔截。
還有一些年代比較久遠(yuǎn)的網(wǎng)絡(luò)庫(kù),例如ASIHTTPRequest,MKNetwokit等網(wǎng)路庫(kù)都是基于CFNetwork的,所以這些網(wǎng)絡(luò)庫(kù)的網(wǎng)絡(luò)請(qǐng)求無(wú)法被NSURLProtocol攔截。


使用 NSURLProtocol

如上文所說(shuō),NSURLProtocol是一個(gè)抽象類。我們要使用它的時(shí)候需要?jiǎng)?chuàng)建它的一個(gè)子類。

@interface CustomURLProtocol : NSURLProtocol

使用NSURLProtocol的主要可以分為5個(gè)步驟:
注冊(cè)—>攔截—>轉(zhuǎn)發(fā)—>回調(diào)—>結(jié)束

注冊(cè):

對(duì)于基于NSURLConnection或者使用[NSURLSession sharedSession]創(chuàng)建的網(wǎng)絡(luò)請(qǐng)求,調(diào)用registerClass方法即可。

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

對(duì)于基于NSURLSession的網(wǎng)絡(luò)請(qǐng)求,需要通過(guò)配置NSURLSessionConfiguration對(duì)象的protocolClasses屬性。

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

攔截:

在攔截到網(wǎng)絡(luò)請(qǐng)求后,NSURLProtocol會(huì)依次執(zhí)行下列方法:

+ (BOOL)canInitWithRequest:(NSURLRequest *)request

該方法會(huì)拿到request的對(duì)象,我們可以通過(guò)該方法的返回值來(lái)篩選request是否需要被NSURLProtocol做攔截處理。
比如:

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

這里我們就只會(huì)攔截http的請(qǐng)求。

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

在該方法中,我們可以對(duì)request進(jìn)行處理。例如修改頭部信息等。最后返回一個(gè)處理后的request實(shí)例。

轉(zhuǎn)發(fā):

在攔截到網(wǎng)絡(luò)請(qǐng)求,并且對(duì)網(wǎng)絡(luò)請(qǐng)求進(jìn)行定制處理以后。我們需要將網(wǎng)絡(luò)請(qǐng)求重新發(fā)送出去。

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

該方法會(huì)創(chuàng)建一個(gè)NSURLProtocol實(shí)例,這里每一個(gè)網(wǎng)絡(luò)請(qǐng)求都會(huì)創(chuàng)建一個(gè)新的實(shí)例。

- (void)startLoading

接下來(lái)就是轉(zhuǎn)發(fā)的核心方法startLoading。在該方法中,我們把處理過(guò)的request重新發(fā)送出去。至于發(fā)送的形式,可以是基于NSURLConnection,NSURLSession甚至CFNetwork。

回調(diào):

既是面向切面的編程,就不能影響到原來(lái)網(wǎng)絡(luò)請(qǐng)求的邏輯。所以上一步將網(wǎng)絡(luò)請(qǐng)求轉(zhuǎn)發(fā)出去以后,當(dāng)收到網(wǎng)絡(luò)請(qǐng)求的返回,還需要再將返回值返回給原來(lái)發(fā)送網(wǎng)絡(luò)請(qǐng)求的地方。
主要需要需要調(diào)用到

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

這四個(gè)方法來(lái)回調(diào)給原來(lái)發(fā)送網(wǎng)絡(luò)請(qǐng)求的地方。
這里假設(shè)我們?cè)谵D(zhuǎn)發(fā)過(guò)程中是使用NSURLSession發(fā)送的網(wǎng)絡(luò)請(qǐng)求,那么在NSURLSession的回調(diào)方法中,我們做相應(yīng)的處理即可。并且我們也可以對(duì)這些返回,進(jìn)行定制化處理。

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

結(jié)束:

在一個(gè)網(wǎng)絡(luò)請(qǐng)求完全結(jié)束以后,NSURLProtocol回調(diào)用到

- (void)stopLoading

在該方法里,我們完成在結(jié)束網(wǎng)絡(luò)請(qǐng)求的操作。以NSURLSession為例:

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

以上便是NSURLProtocol的基本流程。


應(yīng)用:

既然NSURLProtocol功能非常強(qiáng)大,那么在具體開(kāi)發(fā)中,會(huì)有哪些應(yīng)用呢?

  • 網(wǎng)絡(luò)請(qǐng)求緩存
  • 網(wǎng)絡(luò)請(qǐng)求mock stub,知名的庫(kù)OHHTTPStubs就是基于NSURLProtocol
  • 網(wǎng)絡(luò)相關(guān)的數(shù)據(jù)統(tǒng)計(jì)
  • URL重定向
  • 配合實(shí)現(xiàn)HTTPDNS
  • ......

坑&注意事項(xiàng):

使用NSURLProtocol碰到的坑也特別多,有的是很少有文檔提及所以沒(méi)有注意到的,有的甚至是至今還沒(méi)解釋的。下面列舉一些我碰到的問(wèn)題:

多個(gè)NSURLProtocol嵌套使用

若一個(gè)項(xiàng)目中存在多個(gè)NSURLProtocol,那么NSURLProtocol的攔截順序跟注冊(cè)的方式和順序有關(guān)。
*對(duì)于使用registerClass方法注冊(cè)的情況:
多個(gè)NSURLProtocol攔截順序?yàn)樽?cè)順序的反序,即后注冊(cè)的的NSURLProtocol先攔截。
*對(duì)于通過(guò)配置NSURLSessionConfiguration對(duì)象的protocolClasses屬性來(lái)注冊(cè)的情況:
protocolClasses這個(gè)數(shù)組里只有第一個(gè)NSURLProtocol會(huì)起作用。
所以我們看到OHHTTPStubs庫(kù)在注冊(cè)的時(shí)候進(jìn)行了這樣的處理:

+ (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的第一個(gè),進(jìn)行攔截。攔截完成之后,又進(jìn)行移除。

關(guān)于不能攔截WKWebView

原因是WKWebView 在獨(dú)立于 app 進(jìn)程之外的進(jìn)程中執(zhí)行網(wǎng)絡(luò)請(qǐng)求,請(qǐng)求數(shù)據(jù)不經(jīng)過(guò)主進(jìn)程,因此,在 WKWebView 上直接使用 NSURLProtocol 無(wú)法攔截請(qǐng)求。
具體可以參考 wkwebview的那些坑這篇文章。文章也給出了不算完美的解決方案。

canInitWithRequest方法多次調(diào)用

偶爾會(huì)出現(xiàn)canInitWithRequest方法多次調(diào)用的情況,這個(gè)問(wèn)題出現(xiàn)非常的奇怪,目前還不清楚原因。但是因?yàn)槲覀冊(cè)赾anInitWithRequest方法中會(huì)判斷是否攔截過(guò)的標(biāo)記。所以這個(gè)問(wèn)題不會(huì)影響到正常使用。另外還發(fā)現(xiàn),當(dāng)我們?cè)谶M(jìn)行網(wǎng)絡(luò)請(qǐng)求之前把緩存清除掉,也不會(huì)出現(xiàn)這個(gè)問(wèn)題。

使用NSURLSession的坑

在NSURLProtocol中使用NSURLSession有很多莫名其妙的問(wèn)題,基本上都是系統(tǒng)的bug。
我們可以在http://www.openradar.me/search?query=nsurlprotocol 這里看到關(guān)于NSURLProtocol的系統(tǒng)bug,基本都與NSURLSession有關(guān)。比較明顯的就是:

  • 攔截到的Request中的HTTPBody為nil;
  • startLoading在某些特殊情況會(huì)出現(xiàn)死鎖;
  • 關(guān)于注冊(cè)registerClass方法只適用于sharedSession創(chuàng)建的網(wǎng)絡(luò)請(qǐng)求;
  • ……

這些問(wèn)題都是在使用NSURLProtocol需要特別注意的。


總結(jié):

NSURLProtocol的強(qiáng)大功能,為iOS網(wǎng)絡(luò)開(kāi)發(fā)提供了非常大的可操作空間。在商業(yè)項(xiàng)目中,也得到了廣泛的應(yīng)用,但我們?cè)趹?yīng)用的同時(shí),也要注意避免NSURLProtocol存在的問(wèn)題。不過(guò)好在隨著iOS系統(tǒng)的發(fā)展,關(guān)于NSURLProtocol的系統(tǒng)bug已經(jīng)越來(lái)越少。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 228,461評(píng)論 6 532
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場(chǎng)離奇詭異,居然都是意外死亡,警方通過(guò)查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 98,538評(píng)論 3 417
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái),“玉大人,你說(shuō)我怎么就攤上這事。” “怎么了?”我有些...
    開(kāi)封第一講書人閱讀 176,423評(píng)論 0 375
  • 文/不壞的土叔 我叫張陵,是天一觀的道長(zhǎng)。 經(jīng)常有香客問(wèn)我,道長(zhǎng),這世上最難降的妖魔是什么? 我笑而不...
    開(kāi)封第一講書人閱讀 62,991評(píng)論 1 312
  • 正文 為了忘掉前任,我火速辦了婚禮,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 71,761評(píng)論 6 410
  • 文/花漫 我一把揭開(kāi)白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上,一...
    開(kāi)封第一講書人閱讀 55,207評(píng)論 1 324
  • 那天,我揣著相機(jī)與錄音,去河邊找鬼。 笑死,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,268評(píng)論 3 441
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼!你這毒婦竟也來(lái)了?” 一聲冷哼從身側(cè)響起,我...
    開(kāi)封第一講書人閱讀 42,419評(píng)論 0 288
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒(méi)想到半個(gè)月后,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 48,959評(píng)論 1 335
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 40,782評(píng)論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 42,983評(píng)論 1 369
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,528評(píng)論 5 359
  • 正文 年R本政府宣布,位于F島的核電站,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 44,222評(píng)論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開(kāi)封第一講書人閱讀 34,653評(píng)論 0 26
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)。三九已至,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開(kāi)封第一講書人閱讀 35,901評(píng)論 1 286
  • 我被黑心中介騙來(lái)泰國(guó)打工, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 51,678評(píng)論 3 392
  • 正文 我出身青樓,卻偏偏與公主長(zhǎng)得像,于是被迫代替她去往敵國(guó)和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 47,978評(píng)論 2 374

推薦閱讀更多精彩內(nèi)容