NSURLProtocol 攔截 APP網絡請求NSURLConnection, NSURLSession, Alamofire

說明

在這裏插入圖片描述

一位著名的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內的所有請求都需要增加公共的頭,像這種我們就可以直接通過

  1. NSURLProtocol來實現,當然實現的方式有很多種
  2. 再比如我們需要將APP某個API進行一些訪問的統計
  3. 再比如我們需要統計APP內的網絡請求失敗率
  4. 網絡請求緩存
  5. 網絡請求mock stub,知名的庫OHHTTPStubs
    就是基於NSURLProtocol
  6. 網絡相關的數據統計
  7. URL重定向
  8. 配合實現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

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章