NSURLPtotocol 網(wǎng)絡(luò)hooker

先說下URL Loading System

如圖所示,URL Loading System是iOS一系列網(wǎng)絡(luò)請求類的集合,包括已經(jīng)過期不用的NSConnection和現(xiàn)在流行的NSURLSession,還包括一些請求認(rèn)證的類,一個sessionConfig的類,還有關(guān)于處理請求緩存的類等,當(dāng)然還包括我們要說的這個NSURLProtocol類。

對,我沒說錯,NSURLPtotocol類并不是一個protocol,他其實就是一個類,而且是一個“虛基類”-虛擬的父類吧。

URL Loading System可以發(fā)出的請求種類有ftp://,http://,https://,file://,data:// 請求。

NSURLProtocol的作用

NSURLProtocol可以攔截監(jiān)聽每一個URL Loading System中發(fā)出request請求,記住是URL Loading System中那些類發(fā)出的請求,也支持AFNetwoking,UIWebView發(fā)出的request。如果不是這些類發(fā)出的請求,NSURLProtocol就沒辦法攔截和監(jiān)聽了。

  • 忽略網(wǎng)絡(luò)請求使用本地緩存
  • 重定向網(wǎng)絡(luò)請求
  • 改變request的請求頭

NSURLProtocol的使用

因為NSURLProtocol是一個虛基類,所以不能直接使用它,要想使用它就必須自定義一個類成為他的子類,然后實現(xiàn)他里面的必須實現(xiàn)的一些方法,那么我們還要告訴系統(tǒng):“喂,你發(fā)出的request,要讓我的子類XXX類過一遍啊!”所以NSURLProtocol有一個register方法告訴系統(tǒng)那個子類要起作用。

- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {

    [NSURLProtocol registerClass:[TFURLProtocol class]];

    return YES;
}

相對應(yīng)的也有unregistClass方法,不讓某個子類起作用,這個起作用的時候并不是一定要在appDelegate中,你想要他在什么時候起作用,某個請求之前注冊他就行,相應(yīng)的不想他起作用就unregist他就行了。

子類必須實現(xiàn)的一些方法

+ (BOOL)canInitWithRequest:(NSURLRequest *)request

每次有一個請求的時候都會調(diào)用這個方法,在這個方法里面判斷這個請求是否需要被處理攔截,如果返回YES就代表這個request需要被處理,反之就是不需要被處理。

+ (BOOL)canInitWithRequest:(NSURLRequest *)request {
    if ([NSURLProtocol propertyForKey:protocolKey inRequest:request]) {
        return NO;
    }

    NSString *scheme = [[request URL] scheme];
    if ([scheme caseInsensitiveCompare:@"http"] == NSOrderedSame ||
        [scheme caseInsensitiveCompare:@"https"] == NSOrderedSame) {
        return YES;
    }

    return NO;
}

+ (NSURLRequest *) canonicalRequestForRequest:(NSURLRequest *)request

這個方法就是返回規(guī)范的request,一般使用就是直接返回request,不做任何處理的

+ (NSURLRequest *)canonicalRequestForRequest:(NSURLRequest *)request {
    return request;
}

- (void)startLoading

這個方法作用很大,把當(dāng)前請求的request攔截下來以后,在這個方法里面對這個request做各種處理,比如添加請求頭,重定向網(wǎng)絡(luò),使用自定義的緩存等。作用非常之大。下面就是一個重定向的例子。

/**
 * 開始請求
 */
- (void)startLoading {
    NSMutableURLRequest *request = [self.request mutableCopy];
    //把訪問百度的request改為訪問Google了
    request.URL = [NSURL URLWithString:@"http://www.google.com"];

    [NSURLProtocol setProperty:@(YES) forKey:protocolKey inRequest:request];

    //使用NSURLSession繼續(xù)把重定向的request發(fā)送出去
    NSURLSessionConfiguration *config = [NSURLSessionConfiguration ephemeralSessionConfiguration];
    NSOperationQueue *mainQueue = [NSOperationQueue mainQueue];

    NSURLSession *session = [NSURLSession sessionWithConfiguration:config delegate:self delegateQueue:mainQueue];

    NSURLSessionDataTask *task = [session dataTaskWithRequest:request];

    [task resume];
}

- (void)stopLoading

相應(yīng)的還有一個停止請求的方法,也是要實現(xiàn)的。

死循環(huán)的坑

有沒有看到這兩句代碼?

if ([NSURLProtocol propertyForKey:protocolKey inRequest:request]) {
    return NO;
}

[NSURLProtocol setProperty:@(YES) forKey:protocolKey inRequest:request];

這兩句是為了防止死循環(huán)的,也是NSURLProtocol里必須寫的方法。試想一下當(dāng)我在startLoading的時候還會繼續(xù)發(fā)出這個request,那么這個時候還是會攔截到這個request,然后進(jìn)行處理,然后再次在startLoading中發(fā)送出去,然后繼續(xù)攔截。。。。。。。。

所以在我們startLoading里面,我們對這個request進(jìn)行標(biāo)記,標(biāo)記他已經(jīng)被處理過了,然后在canInitWithRequest方法中根據(jù)這個標(biāo)記拿到這個request,如果被標(biāo)記了,就不再次進(jìn)行處理了,如果沒有標(biāo)記過就要進(jìn)行處理,這就很好的解決了死循環(huán)問題。

NSURLProtocolClient

如果我們使用UIWebView發(fā)送一個request,攔截以后當(dāng)我們使用NSURLSession發(fā)出了request,那么這個request的response是無法回到這個UIWebView的,因為可以理解成不是同一個地方發(fā)出的request,這個response只能有session來處理,那我們怎么才能讓這個response回到剛開始的UIWebView呢?

NSURLProtocolClient就可以看做是URL Loading System,我們把response告訴client,也就是URL Loading System,讓他來繼續(xù)處理這個response,因為一切都是基于URL Loading System發(fā)生的,所以把response交給他,他會自動處理這個response回到webView。

每一個NSURLProtocol的子類都有一個client對象來處理請求得到的response。其實下面這些寫法都是差不多固定的。

-(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];
}

- (void)URLSession:(NSURLSession *)session dataTask:(NSURLSessionDataTask *)dataTask willCacheResponse:(NSCachedURLResponse *)proposedResponse completionHandler:(void (^)(NSCachedURLResponse *cachedResponse))completionHandler
{
    completionHandler(proposedResponse);
}

總結(jié)

NSURLProtocol的一些坑

  1. 死循環(huán)
  2. 調(diào)試惡心。因為打開一個頁面,里面的每一個請求包括網(wǎng)頁圖片等都會去走一遍子類中請求處理的判斷方法,導(dǎo)致很多想調(diào)試的request找不到。
  3. WKWebView不起作用,因為WKWebView走得是WebKit內(nèi)核,不走蘋果這一套邏輯,目前貌似還沒有有效的解決方法。

注意點

可以注冊多個NSURLProtocol的子類,注冊多個NSURLProtocol子類會逆序去執(zhí)行,也就是先注冊的子類后執(zhí)行。

常見用法總結(jié)

  1. 重定向網(wǎng)絡(luò)請求(已經(jīng)舉過例子了)
  2. 改變request的請求頭
- (void)startLoading {
    NSMutableURLRequest *request = [self.request mutableCopy];

    //給請求頭添加一個請求體
    NSMutableDictionary *headers = [request.allHTTPHeaderFields mutableCopy];
    [headers setObject:@"ttf" forKey:@"i am ttf"];
    request.allHTTPHeaderFields = headers;

    [NSURLProtocol setProperty:@(YES) forKey:protocolKey inRequest:request];

    .....然后使用NSURLSession發(fā)送request
}

  1. 忽略網(wǎng)絡(luò)請求使用本地緩存

首先自定一個URLResponse類,把資源轉(zhuǎn)化為這個自定義類落地持久化,然后把這個類轉(zhuǎn)換成URL Loading System可以接受的NSURLResponse類,發(fā)送給client,其實主要就是startLoading里面。

- (void) startLoading {

    //1\. 獲取緩存的response
    CachedURLResponse *cachedResponse = [self cachedResponseForCurrentRequest];

    //2\. 判斷緩存response是否存在
    if (cachedResponse) {

        NSData *data = cachedResponse.data;
        NSString *mimeType = cachedResponse.mimeType;
        NSString *encoding = cachedResponse.encoding;

        //構(gòu)造一個新的response
        NSURLResponse *response = [[NSURLResponse alloc] initWithURL:self.request.URL
                                                            MIMEType:mimeType
                                               expectedContentLength:data.length
                                                    textEncodingName:encoding];

        //將新的response作為request對應(yīng)的response
        [self.client URLProtocol:self
              didReceiveResponse:response
              cacheStoragePolicy:NSURLCacheStorageNotAllowed];

        //設(shè)置request對應(yīng)的 響應(yīng)數(shù)據(jù) response data
        [self.client URLProtocol:self didLoadData:data];

        //標(biāo)記請求結(jié)束
        [self.client URLProtocolDidFinishLoading:self];

    } else {
        NSMutableURLRequest *newRequest = [self.request mutableCopy];

        [NSURLProtocol setProperty:@YES
                            forKey:MyURLProtocolHandledKey
                         inRequest:newRequest];

        NSURLSessionConfiguration *config = [NSURLSessionConfiguration defaultSessionConfiguration];
        NSOperationQueue *mainQueue = [NSOperationQueue mainQueue];
        NSURLSession *session = [NSURLSession sessionWithConfiguration:config delegate:self delegateQueue:mainQueue];
        NSURLSessionDataTask *task = [session dataTaskWithRequest:newRequest];

        [task resume];
    }

}

另外也可以參考一下“OHHTTPStubs的實現(xiàn)方式”,核心就是使用的NSURLProtocol。

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

相關(guān)閱讀更多精彩內(nèi)容

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