簡書的NSURLProtocol踩坑總結(jié)

本文假設(shè)你已經(jīng)對NSURLProtocol有所了解,已了解的建議閱讀蘋果的Sample Code CustomHTTPProtocol意蛀。
簡書使用NSURLProtocol在請求時(shí)添加ETag頭信息涛碑、替換URL host為HTTPDNS得到的ip颊艳,在返回時(shí)進(jìn)行SSL Pinning的證書校驗(yàn)畜伐,保證了網(wǎng)絡(luò)請求的可用性和安全性吗讶。

簡書的網(wǎng)絡(luò)層結(jié)構(gòu)

由于NSURLProtocol屬于蘋果的黑魔法袖裕,文檔并不詳細(xì)曹抬,有些教程和諸如“NSURLProtocol的坑”的文章本身也是有坑或不完善的,所以我們寫下這篇文章來分享簡書在NSURLProtocol的開發(fā)使用中遇到的誤區(qū)和摸索出的更佳實(shí)踐(注意:可能并不是最佳實(shí)踐)急鳄,歡迎在原文評論區(qū)指正谤民。

+canonicalRequestForRequest:

canonical用于形容詞時(shí)意為典范的、標(biāo)準(zhǔn)的疾宏,也就是說這個(gè)方法更希望返回的是一個(gè)標(biāo)準(zhǔn)的request张足,所以什么才算標(biāo)準(zhǔn)的request,這個(gè)方法到底用來干嘛


我們可以看下蘋果示例CustomHTTPProtocol項(xiàng)目中的CanonicalRequestForRequest.h文件的注釋

The Foundation URL loading system needs to be able to canonicalize URL requests for various reasons (for example, to look for cache hits). The default HTTP/HTTPS protocol has a complex chunk of code to perform this function. Unfortunately there's no way for third party code to access this. Instead, we have to reimplement it all ourselves. This is split off into a separate file to emphasise that this is standard boilerplate that you probably don't need to look at.

簡單說就是要在這個(gè)方法里將request格式化坎藐,具體看它的.m文件为牍,依次做了以下操作

  • 將scheme、host間的分隔符置為://
  • 將scheme置為小寫
  • 將host置為小寫
  • 如果host為空岩馍,置為localhost
  • 如果path為空吵聪,保證host最后帶上/
  • 格式化部分HTTP Header

正如注釋中所表達(dá)的,在我們用NSURLProtocol接管一個(gè)請求后兼雄,URL loading system已經(jīng)幫不上忙了吟逝,需要自己去格式化這個(gè)請求。那么這里就有幾個(gè)問題:
我們實(shí)際項(xiàng)目中到底需不需要在這里去做一遍格式化的工作呢赦肋?
大部分項(xiàng)目中的API請求應(yīng)該都是由統(tǒng)一的基類封裝發(fā)出來的块攒,其實(shí)已經(jīng)保證了request格式的正確和統(tǒng)一,所以這個(gè)方法直接return request;就可以了佃乘。
如果我就是希望在這里格式化一下呢囱井?
如注釋中所說,CanonicalRequestForRequest文件可以視為標(biāo)準(zhǔn)操作趣避,直接拿到項(xiàng)目中用就好庞呕。
我可以在這個(gè)方法里去做HTTPDNS的工作,替換host嗎?
如果使用了NSURLCache住练,這個(gè)方法返回的request決定了NSURLCache的緩存數(shù)據(jù)庫中request_key值(數(shù)據(jù)庫的路徑在app的/Library/Caches/<bundle id>/Cache.db

普通緩存

在此修改過request后的緩存

所以地啰,如果在這里替換為HTTPDNS得到的host,就可能存在服務(wù)端數(shù)據(jù)不變讲逛,但由于ip改變導(dǎo)致request_key不同而無法命中cache的情況亏吝。

-startLoading

這也是個(gè)比較容易出問題的方法,下邊講三個(gè)易錯(cuò)點(diǎn)盏混。
蔚鸥、不要在這個(gè)方法所在的線程里做任何同步阻塞的操作,例如網(wǎng)絡(luò)請求许赃,異步請求+信號(hào)量也不行止喷。具體原因文檔中沒有提及,但這會(huì)使方法里發(fā)出的請求和startLoading本身的請求最終超時(shí)混聊。
弹谁、很多使用NSURLProtocol做HTTPDNS的教程或demo里都教在該方法里直接創(chuàng)建NSURLSession,然后發(fā)出去修改后的請求技羔,類似于

// 注意:這是錯(cuò)誤示范
- (void)startLoading {
    ...
    重新構(gòu)造request
    ...
    NSURLSession *session = [NSURLSession sessionWithConfiguration:[NSURLSessionConfiguration defaultSessionConfiguration] delegate:self delegateQueue:nil];
    NSURLSessionDataTask *task = [session dataTaskWithRequest:request];
    [task resume];
}

當(dāng)然,這個(gè)和NSURLProtocol本身關(guān)系不大了卧抗,而是NSURLSession的用法出現(xiàn)了嚴(yán)重錯(cuò)誤藤滥。對于用途相同的request,應(yīng)該只創(chuàng)建一個(gè)URLSession社裆,可參考AFNetworking拙绊。每個(gè)request都創(chuàng)建一個(gè)URLSession是低效且不推薦的,可能會(huì)遇到各種無法預(yù)知的bug泳秀,而且最致命的是即使你在-stopLoading處調(diào)了finishTasksAndInvalidateinvalidateAndCancel标沪,內(nèi)存在短期內(nèi)還是居高不下。
關(guān)于這個(gè)內(nèi)存泄露的問題嗜傅,推薦閱讀蘋果官方論壇的討論StackOverFlow的回答金句。概括下來就是每個(gè)NSURLSession都會(huì)創(chuàng)建一個(gè)維持10min的SSL cache,這個(gè)cache由Security.framework私有吕嘀,無論你在這里調(diào)什么方法都不會(huì)清掉這個(gè)cache违寞,所以在10min內(nèi)的內(nèi)存增長可能是無限制的。
正確的姿勢應(yīng)該像CustomHTTPProtocol那樣創(chuàng)建一個(gè)URLSession單例來發(fā)送里面的請求偶房,或者像我一樣依舊用NSURLConnection來發(fā)請求趁曼。
、如果問題二最后采用NSURLConnection發(fā)請求棕洋,那么在結(jié)合HTTPDNS獲取ip時(shí)應(yīng)該會(huì)出現(xiàn)形如以下的代碼:

- (void)startLoading {
    ...
    [[HTTPDNSManager manager] fetchIp:^(NSString *ip) {
        ...替換host...
        self.connection = [NSURLConnection connectionWithRequest:theRequest delegate:self];
    }];
}

你會(huì)發(fā)現(xiàn)URLConnection能發(fā)出請求但回調(diào)并不會(huì)走挡闰,這個(gè)很好理解,因?yàn)閁RLConnection的回調(diào)默認(rèn)和發(fā)起的線程相同,而發(fā)起是在-[HTTPDNSManager fetchIp:]的回調(diào)線程中摄悯,這個(gè)線程用完就失活了赞季,所以解決這個(gè)問題的關(guān)鍵在于使URLConnection的回調(diào)在一個(gè)存活的線程中。乍一想有3種方案:1射众、將創(chuàng)建URLConnection放到startLoading所在的線程執(zhí)行碟摆;2、用-[NSURLConnection setDelegateQueue:]方法設(shè)置它的回調(diào)隊(duì)列叨橱;3典蜕、將創(chuàng)建URLConnection放到主線程執(zhí)行,非常暴力罗洗,但是我確實(shí)見過這么寫的愉舔。這3種方案其實(shí)只有第1種可用。先看下CustomHTTPProtocol的Read Me.txt(是的伙菜,NSURLProtocol的文檔還沒這個(gè)Sample Code的Readme詳細(xì))轩缤,中間部分有一段:

In addition, an NSURLProtocol subclass is expected to call the various methods of the NSURLProtocolClient protocol from the client thread, including all of the following:

-URLProtocol:wasRedirectedToRequest:redirectResponse:
-URLProtocol:didReceiveResponse:cacheStoragePolicy:
-URLProtocol:didLoadData:
-URLProtocolDidFinishLoading:
-URLProtocol:didFailWithError:
-URLProtocol:didReceiveAuthenticationChallenge:
-URLProtocol:didCancelAuthenticationChallenge:

方案2的setDelegateQueue:顯然是無法把delegateQueue精確到指定線程的,除非最后把URLConnection回調(diào)里面的方法再強(qiáng)行調(diào)到client線程上去贩绕,那樣的話還不如直接用方案1火的。
繼續(xù)看那個(gè)txt,還是上述引用的位置淑倾,往下幾行有個(gè)WARNING:

WARNING: An NSURLProtocol subclass must operate asynchronously. It is not safe for it to block the client thread for extended periods of time. For example, while it's reasonable for an NSURLProtocol subclass to defer work (like an authentication challenge) to the main thread, it must do so asynchronously. If the NSURLProtocol subclass passes a task to the main thread and then blocks waiting for the result, it's likely to deadlock the application.

HTTPS請求在回調(diào)中需要驗(yàn)證SSL證書馏鹤,離不開SecTrustEvaluate函數(shù)〗慷撸可以看到SecTrustEvaluate的文檔最后有個(gè)特別注意事項(xiàng)湃累,第二段寫道

Because this function might look on the network for certificates in the certificate chain, the function might block while attempting network access. You should never call it from your main thread; call it only from within a function running on a dispatch queue or on a separate thread.

所以使用方案3很有可能在SecTrustEvaluate時(shí)阻塞掉主線程。

看了這么多錯(cuò)誤示范碍讨,下邊來看方案1-startLoading:里做host替換的正確示范:

@property(atomic, strong) NSThread *clientThread;
@property(atomic, strong) NSURLConnection *connection;

- (void)startLoading {
    NSMutableURLRequest *theRequest = [self.request mutableCopy];
    [NSURLProtocol setProperty:@YES forKey:APIProtocolHandleKey inRequest:theRequest];
    
    self.clientThread = [NSThread currentThread];
    [[HTTPDNSManager manager] fetchIp:^(NSString *ip) {
        if (ip) {
            [theRequest setValue:self.request.URL.host forHTTPHeaderField:@"Host"];
            
            NSURLComponents *urlComponents = [NSURLComponents componentsWithURL:theRequest.URL
                                                        resolvingAgainstBaseURL:YES];
            urlComponents.host = ip;
            theRequest.URL = urlComponents.URL;
        }
        
        [self performBlockOnStartLoadingThread:^{
            self.connection = [NSURLConnection connectionWithRequest:theRequest delegate:self];
        }];
    }];
}

- (void)performBlockOnStartLoadingThread:(dispatch_block_t)block {
    [self performSelector:@selector(onThreadPerformBlock:)
                 onThread:self.clientThread
               withObject:[block copy]
            waitUntilDone:NO];
}

- (void)onThreadPerformBlock:(dispatch_block_t)block {
    !block ?: block();
}

request.HTTPBody

在NSURLProtocol中取request.HTTPBody得到的是nil治力,并不是因?yàn)閎ody真的被NSURLProtocol拋棄了之類的,可以看到發(fā)出去的請求還是正常帶著body的勃黍。
除非你的NSURLProtocol是用于Mock時(shí)根據(jù)HTTPBody中的參數(shù)來返回不同的模擬數(shù)據(jù)宵统,否則大多數(shù)情況是不需要在意這點(diǎn)的。這也不是蘋果的bug覆获,只是body數(shù)據(jù)在URL loading system中到達(dá)這里之前就已經(jīng)被轉(zhuǎn)成stream了榜田。如果必須的話,可以在request.HTTPBodyStream中解析它锻梳。

推薦閱讀

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末箭券,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子疑枯,更是在濱河造成了極大的恐慌辩块,老刑警劉巖,帶你破解...
    沈念sama閱讀 219,589評論 6 508
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異废亭,居然都是意外死亡国章,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,615評論 3 396
  • 文/潘曉璐 我一進(jìn)店門豆村,熙熙樓的掌柜王于貴愁眉苦臉地迎上來液兽,“玉大人,你說我怎么就攤上這事掌动∷膯” “怎么了?”我有些...
    開封第一講書人閱讀 165,933評論 0 356
  • 文/不壞的土叔 我叫張陵粗恢,是天一觀的道長柑晒。 經(jīng)常有香客問我,道長眷射,這世上最難降的妖魔是什么匙赞? 我笑而不...
    開封第一講書人閱讀 58,976評論 1 295
  • 正文 為了忘掉前任,我火速辦了婚禮妖碉,結(jié)果婚禮上涌庭,老公的妹妹穿的比我還像新娘。我一直安慰自己欧宜,他們只是感情好坐榆,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,999評論 6 393
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著鱼鸠,像睡著了一般猛拴。 火紅的嫁衣襯著肌膚如雪羹铅。 梳的紋絲不亂的頭發(fā)上蚀狰,一...
    開封第一講書人閱讀 51,775評論 1 307
  • 那天,我揣著相機(jī)與錄音职员,去河邊找鬼麻蹋。 笑死,一個(gè)胖子當(dāng)著我的面吹牛焊切,可吹牛的內(nèi)容都是我干的扮授。 我是一名探鬼主播,決...
    沈念sama閱讀 40,474評論 3 420
  • 文/蒼蘭香墨 我猛地睜開眼专肪,長吁一口氣:“原來是場噩夢啊……” “哼刹勃!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起嚎尤,我...
    開封第一講書人閱讀 39,359評論 0 276
  • 序言:老撾萬榮一對情侶失蹤荔仁,失蹤者是張志新(化名)和其女友劉穎,沒想到半個(gè)月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體乏梁,經(jīng)...
    沈念sama閱讀 45,854評論 1 317
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡次洼,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 38,007評論 3 338
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了遇骑。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片卖毁。...
    茶點(diǎn)故事閱讀 40,146評論 1 351
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖落萎,靈堂內(nèi)的尸體忽然破棺而出亥啦,到底是詐尸還是另有隱情,我是刑警寧澤模暗,帶...
    沈念sama閱讀 35,826評論 5 346
  • 正文 年R本政府宣布禁悠,位于F島的核電站,受9級(jí)特大地震影響兑宇,放射性物質(zhì)發(fā)生泄漏碍侦。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,484評論 3 331
  • 文/蒙蒙 一隶糕、第九天 我趴在偏房一處隱蔽的房頂上張望瓷产。 院中可真熱鬧,春花似錦枚驻、人聲如沸濒旦。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,029評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽尔邓。三九已至,卻和暖如春锉矢,著一層夾襖步出監(jiān)牢的瞬間梯嗽,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 33,153評論 1 272
  • 我被黑心中介騙來泰國打工沽损, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留灯节,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 48,420評論 3 373
  • 正文 我出身青樓绵估,卻偏偏與公主長得像炎疆,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個(gè)殘疾皇子国裳,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 45,107評論 2 356

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

  • 本文是逐行翻譯形入,便于參照原文,如有歧義或者疑問請閱讀原文比較缝左。于 2017.1.25===============...
    Auditore閱讀 1,522評論 4 5
  • Spring Cloud為開發(fā)人員提供了快速構(gòu)建分布式系統(tǒng)中一些常見模式的工具(例如配置管理亿遂,服務(wù)發(fā)現(xiàn)螟蒸,斷路器,智...
    卡卡羅2017閱讀 134,672評論 18 139
  • Swift版本點(diǎn)擊這里歡迎加入QQ群交流: 594119878最新更新日期:18-09-17 About A cu...
    ylgwhyh閱讀 25,395評論 7 249
  • 生命走向終點(diǎn)的人們會(huì)被迫著回顧自己的一生诵原。不過在這兒,回憶即遺忘挽放。你所記得的關(guān)于自己的一切绍赛,當(dāng)你走出這扇門時(shí),都會(huì)...
    Penicillin00閱讀 427評論 0 0
  • 文 艾米 小貓咪 遠(yuǎn)遠(yuǎn)地 你杏目奕奕地望著我 我也望著你 你在好奇 對面的那個(gè)人類拿的什么武器 對著我拍 這兒...
    月影清韻閱讀 1,122評論 93 120