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)越少。