這篇文章會提供一種在 Cocoa 層攔截所有 HTTP 請求的方法卖擅,其實標(biāo)題已經(jīng)說明了攔截 HTTP 請求需要的了解的就是 NSURLProtocol
在刺。
由于文章的內(nèi)容較長决摧,會分成兩部分空免,這篇文章介紹 NSURLProtocol
攔截 HTTP 請求的原理顽染,另一篇文章如何進行 HTTP Mock 介紹這個原理在 OHHTTPStubs
中的應(yīng)用草丧,它是如何 Mock(偽造)某個 HTTP 請求對應(yīng)的響應(yīng)的狸臣。
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 加載系統(tǒng)創(chuàng)建一個合適的 NSURLProtocol
對象處理對應(yīng)的 URL 請求烛亦,而我們需要做的就是寫一個繼承自 NSURLProtocol
的類,并通過 - registerClass:
方法注冊我們的協(xié)議類懂拾,然后 URL 加載系統(tǒng)就會在請求發(fā)出時使用我們創(chuàng)建的協(xié)議對象對該請求進行處理煤禽。
這樣,我們需要解決的核心問題就變成了如何使用 NSURLProtocol
來處理所有的網(wǎng)絡(luò)請求岖赋,這里使用蘋果官方文檔中的 CustomHTTPProtocol 進行介紹檬果,你可以點擊這里下載源代碼。
在這個工程中 CustomHTTPProtocol.m
是需要重點關(guān)注的文件唐断,CustomHTTPProtocol
就是 NSURLProtocol
的子類:
@interface CustomHTTPProtocol : NSURLProtocol
...
@end
現(xiàn)在重新回到需要解決的問題选脊,也就是 如何使用 NSURLProtocol 攔截 HTTP 請求?脸甘,有這個么幾個問題需要去解決:
- 如何決定哪些請求需要當(dāng)前協(xié)議對象處理恳啥?
- 對當(dāng)前的請求對象需要進行哪些處理?
-
NSURLProtocol
如何實例化丹诀? - 如何發(fā)出 HTTP 請求并且將響應(yīng)傳遞給調(diào)用者钝的?
上面的這幾個問題其實都可以通過 NSURLProtocol
為我們提供的 API 來解決,決定請求是否需要當(dāng)前協(xié)議對象處理的方法是:+ 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
實例,上述方法會拿到所有的請求對象枚荣,我們就可以根據(jù)對應(yīng)的請求選擇是否處理該對象碗脊;而上面的代碼只會處理所有 URL
不為空的請求。
請求經(jīng)過 + canInitWithRequest:
方法過濾之后棍弄,我們得到了所有要處理的請求望薄,接下來需要對請求進行一定的操作疟游,而這都會在 + canonicalRequestForRequest:
中進行,雖然它與 + canInitWithRequest:
方法傳入的 request 對象都是一個痕支,但是最好不要在 + canInitWithRequest:
中操作對象颁虐,可能會有語義上的問題;所以卧须,我們需要覆寫 + canonicalRequestForRequest:
方法提供一個標(biāo)準(zhǔn)的請求對象:
+ (NSURLRequest *)canonicalRequestForRequest:(NSURLRequest *)request {
return request;
}
這里對請求不做任何修改另绩,直接返回,當(dāng)然你也可以給這個請求加個 header花嘶,只要最后返回一個 NSURLRequest
對象就可以笋籽。
在得到了需要的請求對象之后,就可以初始化一個 NSURLProtocol
對象了:
- (id)initWithRequest:(NSURLRequest *)request cachedResponse:(NSCachedURLResponse *)cachedResponse client:(id <NSURLProtocolClient>)client {
return [super initWithRequest:request cachedResponse:cachedResponse client:client];
}
在這里直接調(diào)用 super
的指定構(gòu)造器方法椭员,實例化一個對象车海,然后就進入了發(fā)送網(wǎng)絡(luò)請求,獲取數(shù)據(jù)并返回的階段了:
- (void)startLoading {
NSURLSession *session = [NSURLSession sessionWithConfiguration:[[NSURLSessionConfiguration alloc] init] delegate:self delegateQueue:nil];
NSURLSessionDataTask *task = [session dataTaskWithRequest:self.request];
[task resume];
}
這里使用簡化了 CustomHTTPClient 中的項目代碼隘击,可以達(dá)到幾乎相同的效果侍芝。
你可以在 - startLoading
中使用任何方法來對協(xié)議對象持有的 request
進行轉(zhuǎn)發(fā),包括 NSURLSession
埋同、 NSURLConnection
甚至使用 AFNetworking 等網(wǎng)絡(luò)庫州叠,只要你能在回調(diào)方法中把數(shù)據(jù)傳回 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];
}
當(dāng)然這里省略后的代碼只會保證大多數(shù)情況下的正確執(zhí)行咧栗,只是給你一個對獲取響應(yīng)數(shù)據(jù)粗略的認(rèn)知,如果你需要更加詳細(xì)的代碼虱肄,我覺得最好還是查看一下
CustomHTTPProtocol
中對 HTTP 響應(yīng)處理的代碼致板,也就是NSURLSessionDelegate
協(xié)議實現(xiàn)的部分。
client
你可以理解為當(dāng)前網(wǎng)絡(luò)請求的發(fā)起者咏窿,所有的 client
都實現(xiàn)了 NSURLProtocolClient
協(xié)議可岂,協(xié)議的作用就是在 HTTP 請求發(fā)出以及接受響應(yīng)時向其它對象傳輸數(shù)據(jù):
@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
當(dāng)然這個協(xié)議中還有很多其他的方法,比如 HTTPS 驗證翰灾、重定向以及響應(yīng)緩存相關(guān)的方法,你需要在合適的時候調(diào)用這些代理方法稚茅,對信息進行傳遞纸淮。
如果你只是繼承了 NSURLProtocol
并且實現(xiàn)了上述方法,依然不能達(dá)到預(yù)期的效果亚享,完成對 HTTP 請求的攔截咽块,你還需要在 URL 加載系統(tǒng)中注冊當(dāng)前類:
[NSURLProtocol registerClass:self];
需要注意的是
NSURLProtocol
只能攔截UIURLConnection
、NSURLSession
和UIWebView
中的請求欺税,對于WKWebView
中發(fā)出的網(wǎng)絡(luò)請求也無能為力侈沪,如果真的要攔截來自WKWebView
中的請求揭璃,還是需要實現(xiàn)WKWebView
對應(yīng)的WKNavigationDelegate
,并在代理方法中獲取請求亭罪。
無論是NSURLProtocol
瘦馍、NSURLConnection
還是NSURLSession
都會走底層的 socket,但是WKWebView
可能由于基于 WebKit应役,并不會執(zhí)行 C socket 相關(guān)的函數(shù)對 HTTP 請求進行處理情组,具體會執(zhí)行什么代碼暫時不是很清楚,如果對此有興趣的讀者箩祥,可以聯(lián)系筆者一起討論院崇。
總結(jié)
如果你只想了解如何對 HTTP 請求進行攔截撕瞧,其實看到這里就可以了竿拆,不過如果你想應(yīng)用文章中的內(nèi)容或者希望了解如何偽造 HTTP 響應(yīng),可以看下一篇文章如何進行 HTTP Mock扭弧。
References
Github Repo:iOS-Source-Code-Analyze
Follow: Draveness · Github
Source: http://draveness.me/intercept