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

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

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

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)建多個(gè)Session時(shí),每個(gè)session 都會(huì)通過(guò)一個(gè)協(xié)議進(jìn)行接受更新或者錯(cuò)誤信息。
使用場(chǎng)景
不管你是通過(guò)UIWebView, NSURLConnection 或者第三方庫(kù) (AFNetworking, MKNetworkKit等),他們都是基于NSURLConnection或者 NSURLSession實(shí)現(xiàn)的,因此你可以通過(guò)NSURLProtocol做自定義的操作。
- 重定向網(wǎng)絡(luò)請(qǐng)求
- 忽略網(wǎng)絡(luò)請(qǐng)求,使用本地緩存
- 自定義網(wǎng)絡(luò)請(qǐng)求的返回結(jié)果
- 一些全局的網(wǎng)絡(luò)請(qǐng)求設(shè)置
攔截網(wǎng)絡(luò)請(qǐng)求
定義協(xié)議
自定義協(xié)議類并繼承NSURLProtocol,然后在application:didfinishLaunchingWithOptions:方法中注冊(cè)該自定義的協(xié)議,一旦注冊(cè)完畢后,它就可以來(lái)處理所有交付給URL Loading system的網(wǎng)絡(luò)請(qǐng)求
@interface CustomURLProtocol : NSURLProtocol
@end
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
//注冊(cè)protocol
[NSURLProtocol registerClass:[CustomURLProtocol class]];
return YES;
}
實(shí)現(xiàn)協(xié)議
+ (BOOL)canInitWithRequest:(NSURLRequest *)request;
這個(gè)方法主要是說(shuō)明你是否打算處理對(duì)應(yīng)的request,如果不打算處理,返回NO,URL Loading System會(huì)使用系統(tǒng)默認(rèn)的行為去處理;如果打算處理,返回YES,然后你就需要處理該請(qǐng)求的所有東西,包括獲取請(qǐng)求數(shù)據(jù)并返回給 URL Loading System。網(wǎng)絡(luò)數(shù)據(jù)可以簡(jiǎn)單的通過(guò)NSURLConnection去獲取,而且每個(gè)NSURLProtocol對(duì)象都有一個(gè)NSURLProtocolClient實(shí)例,可以通過(guò)該client將獲取到的數(shù)據(jù)返回給URL Loading System
當(dāng)你去加載一個(gè)URL資源的時(shí)候,URL Loading System會(huì)詢問CustomeURLProtocol能否處理該請(qǐng)求,如果返回YES,URL Loading System會(huì)創(chuàng)建一個(gè)CustomeURLProtocol實(shí)例然后調(diào)用NSURLConnection去獲取數(shù)據(jù),然而這個(gè)也會(huì)調(diào)用URL Loading System,而你在+canInitWithRequest:方法中又總是返回YES,這樣URL Loading System又會(huì)創(chuàng)建一個(gè)CustomeURLProtocol實(shí)現(xiàn)循環(huán),為了保證每個(gè)request只被處理一次,應(yīng)該在+canInitWithRequest:方法中查詢r(jià)equest是否已經(jīng)處理過(guò),如果處理過(guò),則返回NO
系統(tǒng)給我們提供了+ (void)setProperty:(id)value forKey:(NSString *)key inRequest:(NSMutableURLRequest *)request;和+ (id)propertyForKey:(NSString *)key inRequest:(NSURLRequest *)request;這兩個(gè)方法進(jìn)行標(biāo)記和區(qū)分。
+ (BOOL)canInitWithRequest:(NSURLRequest *)request
{
NSString *scheme = [[request URL] scheme];
//只處理http和https請(qǐng)求
if ([scheme caseInsensitiveCompare:@"http"] == NSOrderedSame || [scheme caseInsensitiveCompare:@"https"] == NSOrderedSame)
{
//看看是否已經(jīng)處理過(guò)了,防止無(wú)線循環(huán)
if ([NSURLProtocol propertyForKey:URLProtocolHandledKey inRequest:request]) {
return NO;
}
return YES;
}
return NO;
}
+ (NSURLRequest *)canonicalRequestForRequest:(NSURLRequest *)request
通過(guò)該方法你可以簡(jiǎn)單的直接返回request,但可以在這里修改request,比如修改header,修改host等,并返回一個(gè)新的request,這是一個(gè)抽象方法,子類必須實(shí)現(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搜索主頁(yè)
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
主要判斷兩個(gè)request是否相同,如果相同的話可以使用緩存數(shù)據(jù),通常只需要調(diào)用父類的實(shí)現(xiàn)
+ (BOOL)requestIsCacheEquivalent:(NSURLRequest *)a toRequest:(NSURLRequest *)b
{
return [super requestIsCacheEquivalent:a toRequest:b];
}
- (void)startLoading;
- (void)stopLoading;
這兩個(gè)方法主要是開始和取消相應(yīng)的request,而且需要標(biāo)識(shí)哪些已經(jīng)處理過(guò)的request
- (void)startLoading
{
NSMutableURLRequest *mutableRequest = [[self request] mutableCopy];
//標(biāo)示改request已經(jīng)處理過(guò)了,防止無(wú)限循環(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)絡(luò)請(qǐng)求的時(shí)候會(huì)調(diào)用到代理方法,我們需要將收到的消息通過(guò)client返回給 URL Loading System
如果注冊(cè)了兩個(gè)NSURLProtocol,執(zhí)行順序是怎樣?###
Protocols的遍歷是反向的,也就是最后注冊(cè)的Protocol會(huì)被優(yōu)先判斷。
如下圖, 先注冊(cè)AAAA,再注冊(cè)BBBB的話優(yōu)先判斷的是BBBB,

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