NSURLProtocol 詳解

初衷是為了讓 UIWebView 可以攔截 Ajax 請求。研究了一番找到了 NSURLProtocol。

URL loading system 結構圖

NSURLProtocol 是屬于 Foundation 框架里的 URL Loading System 的一部分,它是一個抽象類,不能去實例化它,只能子類化NSURLProtocol,然后使用的時候注冊子類。一個相對晦澀難解的類。

那么如果開發者自定義的一個 NSURLProtocol 并且注冊到 app 中,那么在這個自定義的 NSURLProtocol 中我們可以攔截所有的請求,進行修改,或者修改 response。

NSHipster 上說:「或者這么說吧: NSURLProtocol 就是一個蘋果允許的中間人攻擊。」

能做什么?

  • 重定向網絡請求(可以解決之前電信的 DNS 域名劫持問題)
  • 緩存
  • 自定義 Response (過濾敏感信息)
  • 全局網絡請求設置
  • HTTP Mocking

使用方法

1. 子類化:

由于 NSURLProtocol 是一個抽象類,所以使用的時候必須定義一個它的子類:

#import <Foundation/Foundation.h>

@interface ZDYURLProtocol : NSURLProtocol

@end

2. 注冊:

[NSURLProtocol registerClass:[ZDYURLProtocol class]];

NSURLConnection 發起請求的時候,會讓所有已注冊的 URLProtocol 來“審批”這個請求

注意: 如果是基于 NSURLSession 進行的請求,注冊的時候需要注冊到 NSURLSessionConfiguration 中:

    NSURLSessionConfiguration *configuration = [NSURLSessionConfiguration defaultSessionConfiguration];
    NSArray *protocolArray = @[[ZDYURLProtocol class]];
    configuration.protocolClasses = protocolArray;
    NSURLSession *session = [NSURLSession sessionWithConfiguration:configuration delegate:self delegateQueue:[NSOperationQueue mainQueue]];
    NSURLSessionTask *task = [session dataTaskWithRequest:request];
    [task resume];

記得用完之后注銷:

[NSURLProtocol unregisterClass:[MyURLProtocol class]];

3. 抽象對象必須實現的方法

注冊成功之后,就需要我們的子類去實現抽象方法:

+ (BOOL)canInitWithRequest:(NSURLRequest *)request;
+ (NSURLRequest *)canonicalRequestForRequest:(NSURLRequest *)request;
+ (BOOL)requestIsCacheEquivalent:(NSURLRequest *)a toRequest:(NSURLRequest *)b;
- (void)startLoading;
- (void)stopLoading;
canInitWithRequest

用來審批的方法。

前面說到「NSURLConnection 發起請求的時候,會讓所有已注冊的 URLProtocol 來“審批”這個請求」。這里返回NO代表放過這個請求,不作處理。返回YES,代表需要處理,則會進入后續的流程。

注意:這里需要放過已經處理過的請求:

+ (BOOL)canInitWithRequest:(NSURLRequest *)request {
    
    //處理過的,放過
    if ([NSURLProtocol propertyForKey:URLProtocolHandledKey inRequest:request]) {
        return NO;
    }
    
    // 你的邏輯代碼
    
    return NO;
}

canonicalRequestForRequest

這個方法用來統一處理請求 request 對象的,可以修改頭信息,或者重定向。沒有特殊需要,則直接return request;

如果要在這里做重定向以及頭信息的時候注意檢查是否已經添加,因為這個方法可能被調用多次,也可以在后面的方法中做。

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

判斷網絡請求是否一致,一致的話使用緩存數據。沒需要就調用 super 的方法。

+ (BOOL)requestIsCacheEquivalent:(NSURLRequest *)a toRequest:(NSURLRequest *)b {
    return [super requestIsCacheEquivalent:a toRequest:b];
}
startLoading

子類中最重要的方法就是 -startLoading 和 -stopLoading,實現請求和取消流程。不同的自定義子類在調用這兩個方法是會傳入不同的內容,但共同點都是要圍繞 protocol 客戶端進行操作。

可以在這里修改請求信息,重定向,DNS解析,返回自定義的測試數據。

重點:需要標記已經處理過的 request:

- (void)startLoading {
    NSMutableURLRequest *mutableReqeust = [[self request] mutableCopy];
    //request處理過的放進去
    [NSURLProtocol setProperty:@YES forKey:URLProtocolHandledKey inRequest:mutableReqeust];
    self.connection = [NSURLConnection connectionWithRequest:mutableReqeust delegate:self];
}

URLProtocolHandledKey 是:

static NSString * const URLProtocolHandledKey = @"URLProtocolHandledKey";

舉例:直接在 startLoading 中返回測試數據:

        NSData *data = [@"testData" dataUsingEncoding:NSUTF8StringEncoding];
        NSURLResponse *response = [[NSURLResponse alloc] initWithURL:mutableReqeust.URL
                                                            MIMEType:@"text/plain"
                                               expectedContentLength:data.length
                                                    textEncodingName:nil];
        [self.client URLProtocol:self
              didReceiveResponse:response
              cacheStoragePolicy:NSURLCacheStorageNotAllowed];

        [self.client URLProtocol:self didLoadData:data];
        [self.client URLProtocolDidFinishLoading:self];
stopLoading
- (void)stopLoading {
    [self.connection cancel];
    self.connection = nil;
}

4. 攔截之后的處理過程

需要注意的是父類中有一個 client 屬性。

/*! 
    @method client
    @abstract Returns the NSURLProtocolClient of the receiver. 
    @result The NSURLProtocolClient of the receiver.  
*/
@property (nullable, readonly, retain) id <NSURLProtocolClient> client;

實現的協議 <NSURLProtocolClient> 如下:

- (void)URLProtocol:(NSURLProtocol *)protocol wasRedirectedToRequest:(NSURLRequest *)request redirectResponse:(NSURLResponse *)redirectResponse;

- (void)URLProtocol:(NSURLProtocol *)protocol cachedResponseIsValid:(NSCachedURLResponse *)cachedResponse;

- (void)URLProtocol:(NSURLProtocol *)protocol didReceiveResponse:(NSURLResponse *)response cacheStoragePolicy:(NSURLCacheStoragePolicy)policy;

- (void)URLProtocol:(NSURLProtocol *)protocol didLoadData:(NSData *)data;

- (void)URLProtocolDidFinishLoading:(NSURLProtocol *)protocol;

- (void)URLProtocol:(NSURLProtocol *)protocol didReceiveAuthenticationChallenge:(NSURLAuthenticationChallenge *)challenge;

- (void)URLProtocol:(NSURLProtocol *)protocol didCancelAuthenticationChallenge:(NSURLAuthenticationChallenge *)challenge;

對于需要處理的 connection,可以在下的 NSURLConnectionDataDelegate 中進行操作:

- (void) connection:(NSURLConnection *)connection didReceiveResponse:(NSURLResponse *)response {
    [self.client URLProtocol:self didReceiveResponse:response cacheStoragePolicy:NSURLCacheStorageNotAllowed];
}

- (void) connection:(NSURLConnection *)connection didReceiveData:(NSData *)data {
    [self.client URLProtocol:self didLoadData:data];
}

- (void) connectionDidFinishLoading:(NSURLConnection *)connection {
    [self.client URLProtocolDidFinishLoading:self];
}

- (void)connection:(NSURLConnection *)connection didFailWithError:(NSError *)error {
    [self.client URLProtocol:self didFailWithError:error];
}

注意事項:

如果我們順序注冊 A B C 三個 Protocol,那么一個 connection 在發送的時候,處理的順序是 C B A,而且最多只有一個 Protocol 會觸發處理。

攔截 UIWebview 的請求,會有被拒的風險。

注意標記處理過的,具體做法在本文搜關鍵詞 URLProtocolHandledKey

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。