iOS 開發中使用 NSURLProtocol 攔截 HTTP 請求

intercept

這篇文章會提供一種在 Cocoa 層攔截全部 HTTP 請求的方法,其實標題已經說明了攔截 HTTP 請求須要的瞭解的就是 NSURLProtocolhtml

因爲文章的內容較長,會分紅兩部分,這篇文章介紹 NSURLProtocol 攔截 HTTP 請求的原理,另外一篇文章如何進行 HTTP Mock 介紹這個原理在 OHHTTPStubs 中的應用,它是如何 Mock(僞造)某個 HTTP 請求對應的響應的。ios

NSURLProtocol

NSURLProtocol 是蘋果爲咱們提供的 URL Loading System 的一部分,這是一張從官方文檔貼過來的圖片:git

URL-loading-syste

官方文檔對 NSURLProtocol 的描述是這樣的:github

An NSURLProtocol object handles the loading of protocol-specific URL data. The NSURLProtocol class itself is an abstract class that provides the infrastructure for processing URLs with a specific URL scheme. You create subclasses for any custom protocols or URL schemes that your app supports.緩存

在每個 HTTP 請求開始時,URL 加載系統建立一個合適的 NSURLProtocol 對象處理對應的 URL 請求,而咱們須要作的就是寫一個繼承自 NSURLProtocol 的類,並經過 - registerClass: 方法註冊咱們的協議類,而後 URL 加載系統就會在請求發出時使用咱們建立的協議對象對該請求進行處理。網絡

這樣,咱們須要解決的核心問題就變成了如何使用 NSURLProtocol 來處理全部的網絡請求,這裏使用蘋果官方文檔中的 CustomHTTPProtocol 進行介紹,你能夠點擊這裏下載源代碼。session

在這個工程中 CustomHTTPProtocol.m 是須要重點關注的文件,CustomHTTPProtocol 就是 NSURLProtocol 的子類:app

@interface CustomHTTPProtocol : NSURLProtocol

...

@end

如今從新回到須要解決的問題,也就是 如何使用 NSURLProtocol 攔截 HTTP 請求?,有這個麼幾個問題須要去解決:socket

  • 如何決定哪些請求須要當前協議對象處理?ide

  • 對當前的請求對象須要進行哪些處理?

  • NSURLProtocol 如何實例化?

  • 如何發出 HTTP 請求而且將響應傳遞給調用者?

上面的這幾個問題其實均可以經過 NSURLProtocol 爲咱們提供的 API 來解決,決定請求是否須要當前協議對象處理的方法是:+ canInitWithRequest

+ (BOOL)canInitWithRequest:(NSURLRequest *)request {
    BOOL shouldAccept;
    NSURL *url;
    NSString *scheme;
    
    shouldAccept = (request != nil);
    if (shouldAccept) {
        url = [request URL];
        shouldAccept = (url != nil);
    }
    return shouldAccept;
}

由於項目中的這個方法是大約有 60 多行,在這裏只粘貼了其中的一部分,只爲了說明該方法的做用:每一次請求都會有一個 NSURLRequest 實例,上述方法會拿到全部的請求對象,咱們就能夠根據對應的請求選擇是否處理該對象;而上面的代碼只會處理全部 URL 不爲空的請求。

請求通過 + canInitWithRequest: 方法過濾以後,咱們獲得了全部要處理的請求,接下來須要對請求進行必定的操做,而這都會在 + canonicalRequestForRequest: 中進行,雖然它與 + canInitWithRequest: 方法傳入的 request 對象都是一個,可是最好不要在 + canInitWithRequest: 中操做對象,可能會有語義上的問題;因此,咱們須要覆寫 + canonicalRequestForRequest: 方法提供一個標準的請求對象:

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

這裏對請求不作任何修改,直接返回,固然你也能夠給這個請求加個 header,只要最後返回一個 NSURLRequest 對象就能夠。

在獲得了須要的請求對象以後,就能夠初始化一個 NSURLProtocol 對象了:

- (id)initWithRequest:(NSURLRequest *)request cachedResponse:(NSCachedURLResponse *)cachedResponse client:(id <NSURLProtocolClient>)client {
    return [super initWithRequest:request cachedResponse:cachedResponse client:client];
}

在這裏直接調用 super 的指定構造器方法,實例化一個對象,而後就進入了發送網絡請求,獲取數據並返回的階段了:

- (void)startLoading {
    NSURLSession *session = [NSURLSession sessionWithConfiguration:[[NSURLSessionConfiguration alloc] init] delegate:self delegateQueue:nil];
    NSURLSessionDataTask *task = [session dataTaskWithRequest:self.request];
    [task resume];
}

這裏使用簡化了 CustomHTTPClient 中的項目代碼,能夠達到幾乎相同的效果。

你能夠在 - startLoading 中使用任何方法來對協議對象持有的 request 進行轉發,包括 NSURLSessionNSURLConnection 甚至使用 AFNetworking 等網絡庫,只要你能在回調方法中把數據傳回 client,幫助其正確渲染就能夠,好比這樣:

- (void)URLSession:(NSURLSession *)session dataTask:(NSURLSessionDataTask *)dataTask didReceiveResponse:(NSURLResponse *)response completionHandler:(void (^)(NSURLSessionResponseDisposition))completionHandler {
    [[self client] URLProtocol:self didReceiveResponse:response cacheStoragePolicy:NSURLCacheStorageAllowed];
    
    completionHandler(NSURLSessionResponseAllow);
}

- (void)URLSession:(NSURLSession *)session dataTask:(NSURLSessionDataTask *)dataTask didReceiveData:(NSData *)data {
    [[self client] URLProtocol:self didLoadData:data];
}

固然這裏省略後的代碼只會保證大多數狀況下的正確執行,只是給你一個對獲取響應數據粗略的認知,若是你須要更加詳細的代碼,我以爲最好仍是查看一下 CustomHTTPProtocol 中對 HTTP 響應處理的代碼,也就是 NSURLSessionDelegate 協議實現的部分。

client 你能夠理解爲當前網絡請求的發起者,全部的 client 都實現了 NSURLProtocolClient 協議,協議的做用就是在 HTTP 請求發出以及接受響應時向其它對象傳輸數據:

@protocol NSURLProtocolClient <NSObject>
...
- (void)URLProtocol:(NSURLProtocol *)protocol didReceiveResponse:(NSURLResponse *)response cacheStoragePolicy:(NSURLCacheStoragePolicy)policy;

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

- (void)URLProtocolDidFinishLoading:(NSURLProtocol *)protocol;
...
@end

固然這個協議中還有不少其餘的方法,好比 HTTPS 驗證、重定向以及響應緩存相關的方法,你須要在合適的時候調用這些代理方法,對信息進行傳遞。

若是你只是繼承了 NSURLProtocol 而且實現了上述方法,依然不能達到預期的效果,完成對 HTTP 請求的攔截,你還須要在 URL 加載系統中註冊當前類:

[NSURLProtocol registerClass:self];

須要注意的是 NSURLProtocol 只能攔截 UIURLConnectionNSURLSessionUIWebView 中的請求,對於 WKWebView 中發出的網絡請求也無能爲力,若是真的要攔截來自 WKWebView 中的請求,仍是須要實現 WKWebView 對應的 WKNavigationDelegate,並在代理方法中獲取請求。
不管是 NSURLProtocolNSURLConnection 仍是 NSURLSession 都會走底層的 socket,可是 WKWebView 可能因爲基於 WebKit,並不會執行 C socket 相關的函數對 HTTP 請求進行處理,具體會執行什麼代碼暫時不是很清楚,若是對此有興趣的讀者,能夠聯繫筆者一塊兒討論。

總結

若是你只想瞭解如何對 HTTP 請求進行攔截,其實看到這裏就能夠了,不過若是你想應用文章中的內容或者但願瞭解如何僞造 HTTP 響應,能夠看下一篇文章如何進行 HTTP Mock

References


+ NSURLProtocol

Github Repo:iOS-Source-Code-Analyze

Follow: Draveness · Github

Source: http://draveness.me/intercept

相關文章
相關標籤/搜索