iOS NSURLProtocol

前言:
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處理了一遍,如下圖:

image.png

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

image.png

當(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í),如下圖:


image.png
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,

image.png

測(cè)試Demo

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
.....

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

友情鏈接更多精彩內(nèi)容