這篇文章會提供一種在 Cocoa 層攔截所有 HTTP 請求的方法,其實標題已經說明了攔截 HTTP 請求需要的了解的就是 NSURLProtocol
。
由于文章的內容較長,會分成兩部分,這篇文章介紹 NSURLProtocol
攔截 HTTP 請求的原理,另一篇文章如何進行 HTTP Mock 介紹這個原理在 OHHTTPStubs
中的應用,它是如何 Mock(偽造)某個 HTTP 請求對應的響應的。
NSURLProtocol
NSURLProtocol
是蘋果為我們提供的 URL Loading System 的一部分,這是一張從官方文檔貼過來的圖片:
官方文檔對 NSURLProtocol
的描述是這樣的:
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 進行介紹,你可以點擊這里下載源代碼。
在這個工程中 CustomHTTPProtocol.m
是需要重點關注的文件,CustomHTTPProtocol
就是 NSURLProtocol
的子類:
@interface CustomHTTPProtocol : NSURLProtocol
...
@end
現在重新回到需要解決的問題,也就是 如何使用 NSURLProtocol 攔截 HTTP 請求?,有這個么幾個問題需要去解決:
- 如何決定哪些請求需要當前協議對象處理?
- 對當前的請求對象需要進行哪些處理?
-
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
進行轉發,包括 NSURLSession
、 NSURLConnection
甚至使用 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
只能攔截UIURLConnection
、NSURLSession
和UIWebView
中的請求,對于WKWebView
中發出的網絡請求也無能為力,如果真的要攔截來自WKWebView
中的請求,還是需要實現WKWebView
對應的WKNavigationDelegate
,并在代理方法中獲取請求。
無論是NSURLProtocol
、NSURLConnection
還是NSURLSession
都會走底層的 socket,但是WKWebView
可能由于基于 WebKit,并不會執行 C socket 相關的函數對 HTTP 請求進行處理,具體會執行什么代碼暫時不是很清楚,如果對此有興趣的讀者,可以聯系筆者一起討論。
總結
如果你只想了解如何對 HTTP 請求進行攔截,其實看到這里就可以了,不過如果你想應用文章中的內容或者希望了解如何偽造 HTTP 響應,可以看下一篇文章如何進行 HTTP Mock。
References
Github Repo:iOS-Source-Code-Analyze
Follow: Draveness · Github
Source: http://draveness.me/intercept