NSURLProtocol全攻略


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

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

一位著名的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發(fā)出的網(wǎng)絡(luò)請求,攔截之后便可根據(jù)需要做各種自定義處理,是iOS網(wǎng)絡(luò)層實現(xiàn)AOP(面向切面編程)的終極利器,所以功能和影響力都是非常強大的。但是關(guān)于NSURLProtocol的文檔非常少,文檔陳舊,包括蘋果官方的文檔也介紹得比較簡單。而且,對于NSURLProtocol的使用,有坑的地方非常多。所以說它也是晦澀的并且是危險的。


什么是 NSURLProtocol

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

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

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


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


使用 NSURLProtocol

如上文所說,NSURLProtocol是一個抽象類。我們要使用它的時候需要創(chuàng)建它的一個子類。

@interface CustomURLProtocol : NSURLProtocol

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

注冊:

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

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

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

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

攔截:

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

+ (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實例。

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

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

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

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

- (void)startLoading

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

回調(diào):

既是面向切面的編程,就不能影響到原來網(wǎng)絡(luò)請求的邏輯。所以上一步將網(wǎng)絡(luò)請求轉(zhuǎn)發(fā)出去以后,當收到網(wǎng)絡(luò)請求的返回,還需要再將返回值返回給原來發(fā)送網(wǎng)絡(luò)請求的地方。
主要需要需要調(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];

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

- (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é)束:

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

- (void)stopLoading

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

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

以上便是NSURLProtocol的基本流程。


應用:

既然NSURLProtocol功能非常強大,那么在具體開發(fā)中,會有哪些應用呢?

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

坑&注意事項:

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

多個NSURLProtocol嵌套使用

若一個項目中存在多個NSURLProtocol,那么NSURLProtocol的攔截順序跟注冊的方式和順序有關(guān)。
*對于使用registerClass方法注冊的情況:
多個NSURLProtocol攔截順序為注冊順序的反序,即后注冊的的NSURLProtocol先攔截。
*對于通過配置NSURLSessionConfiguration對象的protocolClasses屬性來注冊的情況:
protocolClasses這個數(shù)組里只有第一個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的第一個,進行攔截。攔截完成之后,又進行移除。

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

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

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

偶爾會出現(xiàn)canInitWithRequest方法多次調(diào)用的情況,這個問題出現(xiàn)非常的奇怪,目前還不清楚原因。但是因為我們在canInitWithRequest方法中會判斷是否攔截過的標記。所以這個問題不會影響到正常使用。另外還發(fā)現(xiàn),當我們在進行網(wǎng)絡(luò)請求之前把緩存清除掉,也不會出現(xiàn)這個問題。

使用NSURLSession的坑

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

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

這些問題都是在使用NSURLProtocol需要特別注意的。


總結(jié):

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

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

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