說明
一位著名的iOS大神Mattt Thompson在http://nshipster.com/nsurlprotocol/
博客裏說過,說“NSURLProtocol is both the most obscure and the most powerful part of the URL Loading System.”NSURLProtocol是URL Loading System中功能最強大也是最晦澀的部分。
NSURLProtocol作爲URL Loading System中的一個獨立部分存在,能夠攔截所有的URL Loading System發出的網絡請求,攔截之後便可根據需要做各種自定義處理,是iOS網絡層實現AOP(面向切面編程)的終極利器,所以功能和影響力都是非常強大的。
什麼是 NSURLProtocol
NSURLProtocol是URL Loading System的重要組成部分。
首先雖然名叫NSURLProtocol,但它卻不是協議。它是一個抽象類。我們要使用它的時候需要創建它的一個子類。
NSURLProtocol在iOS系統中大概處於這樣一個位置:
NSURLProtocol能攔截哪些網絡請求
NSURLProtocol能攔截所有基於URL Loading System的網絡請求。
這裏先貼一張URL Loading System的圖:
可以攔截的網絡請求包括NSURLSession,NSURLConnection以及UIWebVIew。
基於CFNetwork的網絡請求,以及WKWebView的請求是無法攔截的。
現在主流的iOS網絡庫,例如AFNetworking,Alamofire等網絡庫都是基於NSURLSession或NSURLConnection的,所以這些網絡庫的網絡請求都可以被NSURLProtocol所攔截。
還有一些年代比較久遠的網絡庫,例如ASIHTTPRequest,MKNetwokit等網路庫都是基於CFNetwork的,所以這些網絡庫的網絡請求無法被NSURLProtocol攔截。
應用例子
NSURLProtocol是iOS網絡加載系統中很強的一部分,它其實是一個抽象類,我們可以通過繼承子類化來攔截APP中的網絡請求。
舉幾個例子:
我們的APP內的所有請求都需要增加公共的頭,像這種我們就可以直接通過
- NSURLProtocol來實現,當然實現的方式有很多種
- 再比如我們需要將APP某個API進行一些訪問的統計
- 再比如我們需要統計APP內的網絡請求失敗率
- 網絡請求緩存
- 網絡請求mock stub,知名的庫OHHTTPStubs
就是基於NSURLProtocol - 網絡相關的數據統計
- URL重定向
- 配合實現HTTPDNS
等等,都可以用到
NSURLProtocol是一個抽象類,我們需要子類化才能實現網絡請求攔截。
使用 NSURLProtocol
如上文所說,NSURLProtocol是一個抽象類。我們要使用它的時候需要創建它的一個子類。
@interface CustomURLProtocol : NSURLProtocol
使用NSURLProtocol的主要可以分爲5個步驟:
註冊—>攔截—>轉發—>回調—>結束
註冊:
對於基於NSURLConnection或者使用[NSURLSession sharedSession]創建的網絡請求,調用registerClass方法即可。
[NSURLProtocol registerClass:[NSClassFromString(@"CustomURLProtocol") class]];
對於基於NSURLSession的網絡請求,需要通過配置NSURLSessionConfiguration對象的protocolClasses屬性。
NSURLSessionConfiguration *sessionConfiguration = [NSURLSessionConfiguration defaultSessionConfiguration];
sessionConfiguration.protocolClasses = @[[NSClassFromString(@"CustomURLProtocol") class]];
攔截
子類化NSURLProtocol
在NSURLProtocol中,我們需要告訴它哪些網絡請求是需要我們攔截的,這個是通過方法canInitWithRequest:來實現的,比如我們現在需要攔截全部的HTTP和HTTPS請求,那麼這個邏輯我們就可以在canInitWithRequest:中來定義
/**
需要控制的請求
@param request 此次請求
@return 是否需要監控
*/
+ (BOOL)canInitWithRequest:(NSURLRequest *)request {
if (![request.URL.scheme isEqualToString:@"http"] &&
![request.URL.scheme isEqualToString:@"https"]) {
return NO;
}
return YES;
}
在方法canonicalRequestForRequest:中,我們可以自定義當前的請求request,當然如果不需要自定義,直接返回就行
/**
設置我們自己的自定義請求
可以在這裏統一加上頭之類的
@param request 應用的此次請求
@return 我們自定義的請求
*/
+ (NSURLRequest *)canonicalRequestForRequest:(NSURLRequest *)request {
return request;
}
避免死循環
對於每個NSURLProtocol的子類,都有一個client,通過它來對iOS的網絡加載系統進行一系列的操作,比如,通知收到response或者錯誤的網絡請求等等
這樣,我們通過這兩個方法,就已經能夠攔截住iOS的網絡請求了,但是這裏有個問題
在我們上層業務調用網絡請求的時候,首先會調用我們的canInitWithRequest:方法,詢問是否對該請求進行處理,接着會調用我們的canonicalRequestForRequest:來自定義一個request,接着又會去調用canInitWithRequest:詢問自定義的request是否需要處理,我們又返回YES,然後又去調用了canonicalRequestForRequest:,這樣,就形成了一個死循環了,這肯定是我們不希望看到的。
有個處理方法,我們可以對每個處理過的request進行標記,在判斷如果這個request已經處理過了,那麼我們就不再進行處理,這樣就有效避免了死循環
在我們自定義request的方法中,我們來設置處理標誌
+ (NSURLRequest *)canonicalRequestForRequest:(NSURLRequest *)request {
NSMutableURLRequest *mutableReqeust = [request mutableCopy];
[NSURLProtocol setProperty:@YES
forKey:"zgpeaceInterceptor"
inRequest:mutableReqeust];
return [mutableReqeust copy];
}
然後在我們的詢問處理方法中,通過判斷是否有處理過的標誌,來進行攔截
+ (BOOL)canInitWithRequest:(NSURLRequest *)request {
if (![request.URL.scheme isEqualToString:@"http"] &&
![request.URL.scheme isEqualToString:@"https"]) {
return NO;
}
//如果是已經攔截過的 就放行
if ([NSURLProtocol propertyForKey: "zgpeaceInterceptor" inRequest:request] ) {
return NO;
}
return YES;
}
這樣,我們就避免了死循環
發送
接下來,就是需要將這個request發送出去了,因爲如果我們不處理這個request請求,系統會自動發出這個網絡請求,但是當我們處理了這個請求,就需要我們手動來進行發送了。
我們要手動發送這個網絡請求,需要重寫startLoading方法
- (void)startLoading {
NSURLRequest *request = [[self class] canonicalRequestForRequest:self.request];
self.connection = [[NSURLConnection alloc] initWithRequest:request delegate:self startImmediately:YES];
self.pps_request = self.request;
}
解釋一下上面的代碼,因爲我們攔截的這個請求是一個真實的請求,所以我們需要創建這樣一個真實的網絡請求,在第二行代碼中,將我們自定義創建的request發了出了,第三行是爲了保存當前的request,作爲我們後面的處理對象。
當然,有start就有stop,stop就很簡單了
- (void)stopLoading {
[self.connection cancel];
}
回調:
既是面向切面的編程,就不能影響到原來網絡請求的邏輯。所以上一步將網絡請求轉發出去以後,當收到網絡請求的返回,還需要再將返回值返回給原來發送網絡請求的地方。
主要需要需要調用到
[self.client URLProtocol:self didFailWithError:error];
[self.client URLProtocolDidFinishLoading:self];
[self.client URLProtocol:self didReceiveResponse:response cacheStoragePolicy:NSURLCacheStorageNotAllowed];
[self.client URLProtocol:self didLoadData:data];
這四個方法來回調給原來發送網絡請求的地方。
這裏假設我們在轉發過程中是使用NSURLSession發送的網絡請求,那麼在NSURLSession的回調方法中,我們做相應的處理即可。並且我們也可以對這些返回,進行定製化處理。
- (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];
}
結束:
在一個網絡請求完全結束以後,NSURLProtocol回調用到
- (void)stopLoading
在該方法裏,我們完成在結束網絡請求的操作。以NSURLSession爲例:
- (void)stopLoading {
[self.session invalidateAndCancel];
self.session = nil;
}
以上便是NSURLProtocol的基本流程。
接口方法
在startLoading中,我們發起了一個NSURLConnection的請求,因爲NSURLProtocol使我們自己定義的,所以我們需要將網絡請求的一系列操作全部傳遞出去,不然上層就不知道當前網絡的一個請求狀態,那我們怎麼將這個網絡狀態傳到上層?在之前,我們說過每個protocol有一個NSURLProtocolClient實例,我們就通過這個client來傳遞。
傳遞一個網絡請求,無外乎就是傳遞請求的一些過程,數據,結果等等。 發起了發起了一個NSURLConnection的請求,實現它的delegate就能夠知道網絡請求的一系列操作
#pragma mark - NSURLConnectionDelegate
- (void)connection:(NSURLConnection *)connection didFailWithError:(NSError *)error{
[self.client URLProtocol:self didFailWithError:error];
}
- (BOOL)connectionShouldUseCredentialStorage:(NSURLConnection *)connection{
return YES;
}
- (void)connection:(NSURLConnection *)connection didReceiveAuthenticationChallenge:(NSURLAuthenticationChallenge *)challenge{
[self.client URLProtocol:self didReceiveAuthenticationChallenge:challenge];
}
- (void)connection:(NSURLConnection *)connection
didCancelAuthenticationChallenge:(NSURLAuthenticationChallenge *)challenge {
[self.client URLProtocol:self didCancelAuthenticationChallenge:challenge];
}
#pragma mark - NSURLConnectionDataDelegate
-(NSURLRequest *)connection:(NSURLConnection *)connection willSendRequest:(NSURLRequest *)request redirectResponse:(NSURLResponse *)response{
if (response != nil) {
self.pps_response = response;
[self.client URLProtocol:self wasRedirectedToRequest:request redirectResponse:response];
}
return request;
}
- (void)connection:(NSURLConnection *)connection
didReceiveResponse:(NSURLResponse *)response {
[[self client] URLProtocol:self didReceiveResponse:response cacheStoragePolicy:NSURLCacheStorageAllowed];
self.pps_response = response;
}
- (void)connection:(NSURLConnection *)connection didReceiveData:(NSData *)data {
[self.client URLProtocol:self didLoadData:data];
[self.pps_data appendData:data];
}
- (NSCachedURLResponse *)connection:(NSURLConnection *)connection
willCacheResponse:(NSCachedURLResponse *)cachedResponse {
return cachedResponse;
}
- (void)connectionDidFinishLoading:(NSURLConnection *)connection {
[[self client] URLProtocolDidFinishLoading:self];
}
其實從上面的代碼,我們可以看出,我們就是在我們自己自定義的protocol中進行了一個傳遞過程,其他的也沒有做操作
這樣,基本的protocol就已經實現完成,那麼怎樣來攔截網絡。我們需要將我們自定義的PPSURLProtocol通過NSURLProtocol註冊到我們的網絡加載系統中,告訴系統我們的網絡請求處理類不再是默認的NSURLProtocol,而是我們自定義的InterceptorProtocol
我們在InterceptorProtocol暴露兩個方法
#import <Foundation/Foundation.h>
@interface InterceptorProtocol : NSURLProtocol
+ (void)start;
+ (void)end;
@end
然後在我們的APP啓動的時候,調用start,就可以監聽到我們的網絡請求。
+ (void)start {
PPSURLSessionConfiguration *sessionConfiguration = [PPSURLSessionConfiguration defaultConfiguration];
[NSURLProtocol registerClass:[PPSURLProtocol class]];
}
+ (void)end {
PPSURLSessionConfiguration *sessionConfiguration = [PPSURLSessionConfiguration defaultConfiguration];
[NSURLProtocol unregisterClass:[PPSURLProtocol class]];
}
目前爲止,我們上面的代碼已經能夠監控到絕大部分的網絡請求,但是呢,有一個卻是特殊的。
對於NSURLSession發起的網絡請求,我們發現通過shared得到的session發起的網絡請求都能夠監聽到,但是通過方法sessionWithConfiguration:delegate:delegateQueue:得到的session,我們是不能監聽到的,原因就出在NSURLSessionConfiguration上,我們進到NSURLSessionConfiguration裏面看一下,他有一個屬性
@property(nullable, copy)NSArray<Class>*protocolClasses;
我們能夠看出,這是一個NSURLProtocol數組,上面我們提到了,我們監控網絡是通過註冊NSURLProtocol來進行網絡監控的,但是通過sessionWithConfiguration:delegate:delegateQueue:得到的session,他的configuration中已經有一個NSURLProtocol,所以他不會走我們的protocol來,怎麼解決這個問題呢? 其實很簡單,我們將NSURLSessionConfiguration的屬性protocolClasses的get方法hook掉,通過返回我們自己的protocol,這樣,我們就能夠監控到通過sessionWithConfiguration:delegate:delegateQueue:得到的session的網絡請求
-(void)load {
self.isSwizzle=YES;
Class cls =NSClassFromString(@"__NSCFURLSessionConfiguration")?:NSClassFromString(@"NSURLSessionConfiguration");
[self swizzleSelector:@selector(protocolClasses) fromClass:cls toClass:[selfclass]];
}
-(void)unload {
self.isSwizzle=NO;
Class cls =NSClassFromString(@"__NSCFURLSessionConfiguration")?:NSClassFromString(@"NSURLSessionConfiguration");
[self swizzleSelector:@selector(protocolClasses) fromClass:cls toClass:[selfclass]];
}
-(void)swizzleSelector:(SEL)selector fromClass:(Class)original toClass:(Class)stub {
Method originalMethod =class_getInstanceMethod(original,selector);
Method stubMethod =class_getInstanceMethod(stub,selector);
if (!originalMethod ||!stubMethod){
[NSExceptionraise:NSInternalInconsistencyExceptionformat:@"Couldn't load NEURLSessionConfiguration."];
}
method_exchangeImplementations(originalMethod,stubMethod);
}-(NSArray*)protocolClasses{
return @[[PPSURLProtocol class]];
//如果還有其他的監控protocol,也可以在這裏加進去
}
在啓動的時候,將這個方法替換掉,在移除監聽的時候,恢復之前的方法
至此,我們的監聽就完成了,如果我們需要將這所有的監聽存起來,在protocol的start或者stop中獲取到request和response,將他們存儲起來就行,需要說明的是,據蘋果的官方說明,因爲請求參數可能會很大,爲了保證性能,請求參數是沒有被攔截掉的,就是post的HTTPBody是沒有的,我沒有獲取出來,如果有其他的辦法,還請告知
demo代碼:
https://github.com/RicardoFerreira10/InterceptorSampleProject
https://github.com/yangqian111/PPSNetworkMonitor
參考
https://nshipster.cn/nsurlprotocol/
https://www.jianshu.com/p/02781c0bbca9
https://juejin.im/entry/58ed8c6344d904005772e8c7
https://www.raywenderlich.com/2509-nsurlprotocol-tutorial
https://www.raywenderlich.com/2292-using-nsurlprotocol-with-swift
https://blog.codavel.com/how-to-intercept-http-requests-on-an-ios-app