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