前言:
NSURLProtocol是NSURLConnection的handle類, 它更像一套協(xié)議,如果遵守這套協(xié)議,網(wǎng)絡請求Request都會經(jīng)過這套協(xié)議里面的方法去處理.
再說簡單點,就是對上層的URLRequest請求做攔截,并根據(jù)自己的需求場景做定制化響應處理
NSURLProtocol 能在系統(tǒng)執(zhí)行 URLRequest前先去將URLRequest處理了一遍,如下圖:

NSURLProtocol能夠讓你去重新定義蘋果的URL加載系統(tǒng) (URL Loading System)的行為,URL Loading System里有許多類用于處理URL請求,比如NSURL,NSURLRequest,NSURLConnection和NSURLSession等,如下圖:

當URL Loading System使用NSURLRequest去獲取資源的時候,它會創(chuàng)建一個NSURLProtocol子類的實例,你不應該直接實例化一個NSURLProtocol,NSURLProtocol看起來像是一個協(xié)議,但其實這是一個類,而且必須使用該類的子類,并且需要被注冊。
URL loading system 原生已經(jīng)支持了http,https,file,ftp,data這些常見協(xié)議,當然也允許我們定義自己的protocol去擴展,或者定義自己的協(xié)議。當URL loading system通過NSURLRequest對象進行請求時,將會自動創(chuàng)建NSURLProtocol的實例(可以是自定義的)。這樣我們就有機會對該請求進行處理
當我創(chuàng)建多個session時,如下圖:

Each session is associated with a delegate to receive periodic updates (or errors). The default delegate calls a completion handler block that you provide; if you choose to provide your own custom delegate, this block is not called.
在創(chuàng)建多個Session時,每個session 都會通過一個協(xié)議進行接受更新或者錯誤信息。
使用場景
不管你是通過UIWebView, NSURLConnection 或者第三方庫 (AFNetworking, MKNetworkKit等),他們都是基于NSURLConnection或者 NSURLSession實現(xiàn)的,因此你可以通過NSURLProtocol做自定義的操作。
- 重定向網(wǎng)絡請求
- 忽略網(wǎng)絡請求,使用本地緩存
- 自定義網(wǎng)絡請求的返回結(jié)果
- 一些全局的網(wǎng)絡請求設置
攔截網(wǎng)絡請求
定義協(xié)議
自定義協(xié)議類并繼承NSURLProtocol,然后在application:didfinishLaunchingWithOptions:方法中注冊該自定義的協(xié)議,一旦注冊完畢后,它就可以來處理所有交付給URL Loading system的網(wǎng)絡請求
@interface CustomURLProtocol : NSURLProtocol
@end
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
//注冊protocol
[NSURLProtocol registerClass:[CustomURLProtocol class]];
return YES;
}
實現(xiàn)協(xié)議
+ (BOOL)canInitWithRequest:(NSURLRequest *)request;
這個方法主要是說明你是否打算處理對應的request,如果不打算處理,返回NO,URL Loading System會使用系統(tǒng)默認的行為去處理;如果打算處理,返回YES,然后你就需要處理該請求的所有東西,包括獲取請求數(shù)據(jù)并返回給 URL Loading System。網(wǎng)絡數(shù)據(jù)可以簡單的通過NSURLConnection去獲取,而且每個NSURLProtocol對象都有一個NSURLProtocolClient實例,可以通過該client將獲取到的數(shù)據(jù)返回給URL Loading System
當你去加載一個URL資源的時候,URL Loading System會詢問CustomeURLProtocol能否處理該請求,如果返回YES,URL Loading System會創(chuàng)建一個CustomeURLProtocol實例然后調(diào)用NSURLConnection去獲取數(shù)據(jù),然而這個也會調(diào)用URL Loading System,而你在+canInitWithRequest:方法中又總是返回YES,這樣URL Loading System又會創(chuàng)建一個CustomeURLProtocol實現(xiàn)循環(huán),為了保證每個request只被處理一次,應該在+canInitWithRequest:方法中查詢request是否已經(jīng)處理過,如果處理過,則返回NO
系統(tǒng)給我們提供了+ (void)setProperty:(id)value forKey:(NSString *)key inRequest:(NSMutableURLRequest *)request;和+ (id)propertyForKey:(NSString *)key inRequest:(NSURLRequest *)request;這兩個方法進行標記和區(qū)分。
+ (BOOL)canInitWithRequest:(NSURLRequest *)request
{
NSString *scheme = [[request URL] scheme];
//只處理http和https請求
if ([scheme caseInsensitiveCompare:@"http"] == NSOrderedSame || [scheme caseInsensitiveCompare:@"https"] == NSOrderedSame)
{
//看看是否已經(jīng)處理過了,防止無線循環(huán)
if ([NSURLProtocol propertyForKey:URLProtocolHandledKey inRequest:request]) {
return NO;
}
return YES;
}
return NO;
}
+ (NSURLRequest *)canonicalRequestForRequest:(NSURLRequest *)request
通過該方法你可以簡單的直接返回request,但可以在這里修改request,比如修改header,修改host等,并返回一個新的request,這是一個抽象方法,子類必須實現(xiàn)
+ (NSURLRequest *)canonicalRequestForRequest:(NSURLRequest *)request
{
NSMutableURLRequest *mutableRequest = [request mutableCopy];
mutableRequest = [self redirectHostInRequest:mutableRequest];
return mutableRequest;
}
+ (NSMutableURLRequest *)redirectHostInRequest:(NSMutableURLRequest *)request
{
if ([request.URL host].length == 0) {
return request;
}
NSString *originUrl = [request.URL absoluteString];
NSString *originHost = [request.URL host];
NSRange hostRange = [originUrl rangeOfString:originHost];
if (hostRange.location == NSNotFound) {
return request;
}
//定向到bing搜索主頁
NSString *ip = @"cn.bing.com";
//替換域名
NSString *urlString = [originUrl stringByReplacingCharactersInRange:hostRange withString:ip];
NSURL *url = [NSURL URLWithString:urlString];
request.URL = url;
return request;
}
+ (BOOL)requestIsCacheEquivalent:(NSURLRequest *)a toRequest:(NSURLRequest *)b
主要判斷兩個request是否相同,如果相同的話可以使用緩存數(shù)據(jù),通常只需要調(diào)用父類的實現(xiàn)
+ (BOOL)requestIsCacheEquivalent:(NSURLRequest *)a toRequest:(NSURLRequest *)b
{
return [super requestIsCacheEquivalent:a toRequest:b];
}
- (void)startLoading;
- (void)stopLoading;
這兩個方法主要是開始和取消相應的request,而且需要標識哪些已經(jīng)處理過的request
- (void)startLoading
{
NSMutableURLRequest *mutableRequest = [[self request] mutableCopy];
//標示改request已經(jīng)處理過了,防止無限循環(huán)
[NSURLProtocol setProperty:@YES forKey:URLProtocolHandledKey inRequest:mutableRequest];
self.connection = [NSURLConnection connectionWithRequest:mutableRequest delegate:self];
}
- (void)stopLoading
{
[self.connection cancel];
}
在協(xié)議的NSURLConnectionDataDelegate 方法中,處理網(wǎng)絡請求的時候會調(diào)用到代理方法,我們需要將收到的消息通過client返回給 URL Loading System
如果注冊了兩個NSURLProtocol,執(zhí)行順序是怎樣?###
Protocols的遍歷是反向的,也就是最后注冊的Protocol會被優(yōu)先判斷。
如下圖, 先注冊AAAA,再注冊BBBB的話優(yōu)先判斷的是BBBB,

API
NSURLProtocol
- (instancetype)initWithRequest:(NSURLRequest *)request cachedResponse:(NSCachedURLResponse *)cachedResponse client:(id<NSURLProtocolClient>)client
初始化并返回這個類;
request:NSURLProtocol對象的URL請求。 此request:NSURLProtocol被retain。
cachedResponse:請求響應的緩存; 如果請求沒有現(xiàn)有的緩存,則可能為nil。
client:調(diào)用者用來與URL加載系統(tǒng)通信實現(xiàn)了的NSURLProtocolClient協(xié)議的對象。 此client被retain。
子類應該覆蓋此方法以執(zhí)行任何自定義初始化操作。 應用程序永遠不應該顯式調(diào)用此方法。
這是NSURLProtocol的指定的初始化方法。
- (BOOL)registerClass:(Class)protocolClass
嘗試注冊NSURLProtocol的子類,使其對URL加載系統(tǒng)可見。唯一的失敗情況是如果protocolClass不是NSURLProtocol的子類。
當URL加載系統(tǒng)開始加載請求時,依次查詢每個注冊的協(xié)議類,以查看是否可以使用指定的請求進行初始化。 當?shù)谝粋€NSURLProtocol子類的canInitWithRequest:方法返回YES時,這個子類將用于執(zhí)行URL加載。不能保證所有注冊的協(xié)議類都被查詢。
所有的子類按照注冊的相反順序進行查詢。
- (void)unregisterClass:(Class)protocolClass
注銷NSURLProtocol的指定子類。
調(diào)用此方法后,URL加載系統(tǒng)不再查詢protocolClass。
- (BOOL)canInitWithRequest:(NSURLRequest *)request
返回NSURLProtocol的子類是否可以處理指定的請求。
子類應檢查請求,并確定此方法是否可以執(zhí)行該請求的加載。
這是一個抽象的方法,子類必須實現(xiàn)此方法。
- (id)propertyForKey:(NSString *)key inRequest:(NSURLRequest *)request
返回與指定的請求中指定的關鍵字關聯(lián)的屬性。如果沒有該key,返回nil;
該方法為協(xié)議實現(xiàn)者提供了訪問與NSURLRequest對象相關的特定于協(xié)議的信息的接口
- (void)setProperty:(id)value forKey:(NSString *)key inRequest:(NSMutableURLRequest *)request
給指定的請求設置與指定鍵相關聯(lián)的屬性。
該方法用于為協(xié)議實現(xiàn)者提供一個用于定制與NSMutableURLRequest對象相關聯(lián)的協(xié)議特定信息的接口。
- (void)removePropertyForKey:(NSString *)key inRequest:(NSMutableURLRequest *)request
移除給指定的請求的指定key相關聯(lián)的屬性。
- (NSURLRequest *)canonicalRequestForRequest:(NSURLRequest *)request
返回指定請求的規(guī)范版本。即返回通過request來自定義的請求
每個具體的協(xié)議實現(xiàn)都是由“規(guī)范”所指定的。 協(xié)議應該保證相同的輸入請求總是產(chǎn)生相同的規(guī)范形式。
在實現(xiàn)此方法時應特別注意,因為請求的規(guī)范形式用于查找URL緩存中的對象,這是在NSURLRequest對象之間執(zhí)行相等檢查的進程。
這是一個抽象的方法,子類必須提供一個實現(xiàn)。
一般情況下我們會直接返回request或者是修改請求頭部信息后再返回;
- (BOOL)requestIsCacheEquivalent:(NSURLRequest *)a toRequest:(NSURLRequest *)b
返回兩個請求在做緩存的時候是否相同;
如果aRequest和bRequest在緩存的時候相同,則為YES,否則為NO。 當且僅當這些請求將被相同的協(xié)議處理并且該協(xié)議在執(zhí)行特定于檢查之后它們?nèi)允堑刃У?,才認為他們相等。
該方法的實現(xiàn)用來確定請求是否應被視為等效的。 子類可以覆蓋此方法以提供協(xié)議特定的比較
@property(readonly, copy) NSCachedURLResponse *cachedResponse
調(diào)用者緩存的響應數(shù)據(jù);
如果不在子類中覆蓋,則此方法返回在初始化時存儲的緩存響應
@property(readonly, retain) id<NSURLProtocolClient> client
調(diào)用者用來與URL加載系統(tǒng)通信的對象
- (instancetype)initWithTask:(NSURLSessionTask *)task cachedResponse:(NSCachedURLResponse *)cachedResponse client:(id<NSURLProtocolClient>)client
這個方法是在其NSURLSessionTaskAdditions分類中定義的方法;文檔和頭文件中并沒有介紹;與該方法類似的還有+ (BOOL)canInitWithTask:(NSURLSessionTask *)task方法和@property(readonly, copy) NSURLSessionTask *task方法;
這里我就不妄自猜測這些方法的作用,應該主要用在與session相關的操作上的,如果以后碰到了用法再回來添加進來
NSURLProtocol (NSURLSessionTaskAdditions)
NSURLProtocolClient協(xié)議提供NSURLProtocol子類與URL加載系統(tǒng)進行通信的接口。 應用程序永遠不需要實現(xiàn)此協(xié)議
- (void)URLProtocol:(NSURLProtocol *)protocol cachedResponseIsValid:(NSCachedURLResponse *)cachedResponse
向URL加載系統(tǒng)發(fā)送緩存響應有效的消息
- (void)URLProtocol:(NSURLProtocol *)protocol didCancelAuthenticationChallenge:(NSURLAuthenticationChallenge *)challenge
向URL加載系統(tǒng)發(fā)送身份認證已被取消的消息
- (void)URLProtocol:(NSURLProtocol *)protocol didFailWithError:(NSError *)error
發(fā)送加載任務因為error而失敗
- (void)URLProtocol:(NSURLProtocol *)protocol didReceiveAuthenticationChallenge:(NSURLAuthenticationChallenge *)challenge
向URL加載系統(tǒng)發(fā)送指示已接收到身份驗證。
- (void)URLProtocol:(NSURLProtocol *)protocol didReceiveResponse:(NSURLResponse *)response cacheStoragePolicy:(NSURLCacheStoragePolicy)policy
向URL加載系統(tǒng)發(fā)送協(xié)議實現(xiàn)已經(jīng)為請求創(chuàng)建了響應對象的消息。
實現(xiàn)中應該使用提供的緩存策略來確定是否將響應存儲在高速緩存中。
- (void)URLProtocol:(NSURLProtocol *)protocol wasRedirectedToRequest:(NSURLRequest *)request redirectResponse:(NSURLResponse *)redirectResponse
向URL加載系統(tǒng)發(fā)送協(xié)議實現(xiàn)已被重定向的消息。
- (void)URLProtocolDidFinishLoading:(NSURLProtocol *)protocol
向URL加載系統(tǒng)發(fā)送協(xié)議實現(xiàn)已經(jīng)完成加載的消息
NSURLProtocolClient
上面的NSURLProtocol定義了一系列加載的流程。而在每一個流程中,我們作為使用者該如何使用URL加載系統(tǒng),則是NSURLProtocolClient中幾個方法該做的事情。
//請求重定向
- (void)URLProtocol:(NSURLProtocol *)protocol wasRedirectedToRequest:(NSURLRequest *)request redirectResponse:(NSURLResponse *)redirectResponse;
// 響應緩存是否合法
- (void)URLProtocol:(NSURLProtocol *)protocol cachedResponseIsValid:(NSCachedURLResponse *)cachedResponse;
//剛接收到Response信息
- (void)URLProtocol:(NSURLProtocol *)protocol didReceiveResponse:(NSURLResponse *)response cacheStoragePolicy:(NSURLCacheStoragePolicy)policy;
//數(shù)據(jù)加載成功
- (void)URLProtocol:(NSURLProtocol *)protocol didLoadData:(NSData *)data;
//數(shù)據(jù)完成加載
- (void)URLProtocolDidFinishLoading:(NSURLProtocol *)protocol;
//數(shù)據(jù)加載失敗
- (void)URLProtocol:(NSURLProtocol *)protocol didFailWithError:(NSError *)error;
//為指定的請求啟動驗證
- (void)URLProtocol:(NSURLProtocol *)protocol didReceiveAuthenticationChallenge:(NSURLAuthenticationChallenge *)challenge;
//為指定的請求取消驗證
- (void)URLProtocol:(NSURLProtocol *)protocol didCancelAuthenticationChallenge:(NSURLAuthenticationChallenge *)challenge;
參考:
NSURLProtocol官方文檔閱讀
iOS中的 NSURLProtocol
iOS H5容器的一些探究(二):iOS下的黑魔法NSURLProtocol
Document
.....