iOS NSURLProtocol

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

image.png

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

image.png

當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時,如下圖:


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)建多個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,

image.png

測試Demo

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

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

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