iOS版本的RequestListener

Target

監(jiān)聽 app 內(nèi)所有的網(wǎng)絡(luò)請(qǐng)求,并將請(qǐng)求的參數(shù)和返回值顯示在手機(jī)端。相當(dāng)于自己抓自己的包,這樣不在電腦前也能夠精確的觀察接口動(dòng)態(tài),或者在接手一個(gè)新的項(xiàng)目時(shí),可以清楚的看到某個(gè)界面的接口請(qǐng)求情況,幫助理清楚界面邏輯。Demo里面監(jiān)測(cè)的是 UIWebView,實(shí)際在項(xiàng)目里面監(jiān)測(cè)api接口效果更佳。

0.png
2.png

原理

原理是使用 NSURLProtocol 攔截所有 URL Loading System 中發(fā)出 request 請(qǐng)求。 攔截到之后,以我們的方式發(fā)出這個(gè)請(qǐng)求,這樣這個(gè)請(qǐng)求的返回?cái)?shù)據(jù)就能被我們統(tǒng)一捕獲。同時(shí),我們將返回的數(shù)據(jù)回調(diào)給原始發(fā)出者,以保證app正常運(yùn)行。在捕獲返回?cái)?shù)據(jù)和請(qǐng)求本身的參數(shù)后就能完整的將一次api調(diào)用顯示出來了。

NSURLProtocol介紹

NSURLProtocol 是屬于 Foundation 框架里的 URL Loading System 的一部分。它是一個(gè)抽象類, 需要繼承它后, 重寫一系列父類的方法, 且在向系統(tǒng)注冊(cè)后, 就可以攔截到所有來自 URL Loading System 中發(fā)出 request 請(qǐng)求, 包括使用 NSURLConnectionNSURLSession 發(fā)出去的請(qǐng)求, 使用這兩者的第三方框架就也能監(jiān)聽到, 比如 AFNetWorking。而視圖方面, 通過UIWebViewWKWebView 發(fā)出去的請(qǐng)求也能被監(jiān)聽到(WKWebView 的攔截會(huì)有些問題)。

96521-804444072007e819.png

攔截到后我們可以修改原來 requeset,我們可以什么都不做,那么這個(gè)請(qǐng)求的行為就會(huì)跟之前的一模一樣。 有趣的是我們也可以對(duì)它進(jìn)行修改,比如給它添加參數(shù),讓這個(gè)請(qǐng)求行為發(fā)生變化?;蛘邔?duì)返回的 response 進(jìn)行修改,亦或干脆重定向到新的資源,你想要A,我返回給你你B。總之, 是否返回?cái)?shù)據(jù), 返回什么數(shù)據(jù), 已經(jīng)由我們決定了。

這里我們讓這個(gè)請(qǐng)求以我們寫的方式發(fā)送出去,以便拿到服務(wù)端返回的數(shù)據(jù)。

攔截請(qǐng)求的方式

  • 對(duì)于 UIWebViewNSURLConnection 只需要構(gòu)建 NSURLProtocol 的子類,在子類中重載必要的方法, 并向系統(tǒng)注冊(cè)[NSURLProtocol registerClass:[SGQURLProtocol class]]; 即可攔截.

      #import <Foundation/Foundation.h>
    
      @interface SGQURLProtocol : NSURLProtocol
    
      @end
    
  • 對(duì)于 NSURLSession,需要通過配置 NSURLSessionConfiguration 對(duì)象的 protocolClasses 屬性

      NSURLSessionConfiguration *sessionConfiguration = [NSURLSessionConfiguration defaultSessionConfiguration];
      sessionConfiguration.protocolClasses =  @[[SGQURLProtocol class]]; 
    

    這是原理,但是我們不能侵入別人寫好的代碼,在里面加上這句代碼。于是我們使用 method swizzing

      - (void)load {
          Class cls = NSClassFromString(@"__NSCFURLSessionConfiguration") ?: NSClassFromString(@"NSURLSessionConfiguration");
          [self swizzleSelector:@selector(protocolClasses) fromClass:cls toClass:[self class]];
      }
    
       - (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) {
              [NSException raise:NSInternalInconsistencyException format:@"Couldn't load NSURLSession hook."];
           }
           method_exchangeImplementations(originalMethod, stubMethod);
          }
    
          - (NSArray *)protocolClasses {
              return @[[SGQURLProtocol class]];
          }
    
  • 對(duì)于 WKWebView,除了上述操作外, 由于其基于 wekkit 內(nèi)核, 使用到了 WKBrowsingContextControllerregisterSchemeForCustomProtocol。 我們需要通過反射的方式拿到了私有的 class & selector。通過 kvc 取到browsingContextController,通過把注冊(cè)把 httphttps 請(qǐng)求交給 NSURLProtocol 處理.

      + (void)registerForWKWebView {
          Class class = [[[WKWebView new] valueForKey:@"browsingContextController"] class];
          SEL selector = NSSelectorFromString(@"registerSchemeForCustomProtocol:");;
          if ([(id)class respondsToSelector:selector]) {
           [(id)class performSelector:selector withObject:@"http"];
           [(id)class performSelector:selector withObject:@"https"];
          }
      }
    

這里需要聲明的是,對(duì)于 WKWebView里面的請(qǐng)求,這樣注冊(cè)后,雖然可以攔截到,但是由于系統(tǒng)原因,會(huì)導(dǎo)致POST請(qǐng)求的請(qǐng)求體會(huì)丟失,導(dǎo)致請(qǐng)求本身會(huì)失敗WKWebView NSURLProtocol問題,所以我們主要還是監(jiān)聽 app 內(nèi)本身的請(qǐng)求。

NSURLProtocol子類中需要重寫的方法

#import "SGQURLProtocol.h"
#import "SGQRequestListener.h"
#import "SGQMockObject.h"
#import "NSURLRequest+ResponseTime.h"

static NSString * const kHandedRequestKey = @"kHandedRequestKey";

@implementation SGQURLProtocol

/*
 是否對(duì)這個(gè)請(qǐng)求進(jìn)行攔截
 返回YES,則這個(gè)request還會(huì)進(jìn)入后續(xù)方法調(diào)用
 返回NO,則不會(huì)對(duì)這個(gè)request有任何影響了。
 */
+ (BOOL)canInitWithRequest:(NSURLRequest *)request {
    if ([[SGQRequestListener sharedInstance] isRequestURLInBlackList:request]) {
        return NO;
    }
    // 這個(gè)標(biāo)記在 startLoading 方法中打上,是為了防止死循環(huán)。因?yàn)槲覀冊(cè)?startLoading方法發(fā)出去的請(qǐng)求也會(huì)被攔截到進(jìn)到這里
    if ([NSURLProtocol propertyForKey:kHandedRequestKey inRequest:request]) {
        return NO;
    }
    
    return YES;
}

/// cache啥的這里不管
+ (BOOL)requestIsCacheEquivalent:(NSURLRequest *)a toRequest:(NSURLRequest *)b {
    return NO;
}

/// 一般可以在這里copy出一個(gè)可變的request,進(jìn)行屬性的修改,然后返回。后續(xù)則會(huì)這個(gè)返回的request發(fā)出請(qǐng)求
+ (NSURLRequest*)canonicalRequestForRequest:(NSURLRequest*)request {
    return request;
}


/*
 最終那個(gè)被攔截的request或者會(huì)進(jìn)到這里,我們?cè)谶@里發(fā)出請(qǐng)求,獲取返回?cái)?shù)據(jù)。
 同時(shí)也將數(shù)據(jù)回調(diào)給原始的client
 */
- (void)startLoading {
    
    NSMutableURLRequest *request = [self.request mutableCopy];
    request.startDate = [NSDate date];
    [NSURLProtocol setProperty:@(YES) forKey:kHandedRequestKey inRequest:request];
    
    id<NSURLProtocolClient> client = [self client];
    
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Wdeprecated-declarations"
    NSOperationQueue *queue = [[NSOperationQueue alloc] init];
    [NSURLConnection sendAsynchronousRequest:request
                                       queue:queue
                           completionHandler:^(NSURLResponse *response, NSData *data, NSError *error){
                               request.endDate = [NSDate date];
                               if (error) {
                                   [client URLProtocol:self didFailWithError:[NSError errorWithDomain:NSCocoaErrorDomain code:NSUserCancelledError userInfo:nil]];
                               } else {
                                   [client URLProtocol:self didReceiveResponse:response
                                    cacheStoragePolicy:NSURLCacheStorageNotAllowed];
                                   [client URLProtocol:self didLoadData:data];
                                   [client URLProtocolDidFinishLoading:self];
                               }
                               
                               dispatch_async(dispatch_get_main_queue(), ^{
                                   SGQMockObject *loadingObject = [SGQMockObject objectWithRequest:request response:response responseData:data error:error responseTime:request.responseTime];
                                   [[SGQRequestListener sharedInstance] addAnObject:loadingObject];
                               });
                           }];
#pragma clang diagnostic pop 
}
- (void)stopLoading { }

@end

可以看到,在這里我們成功地拿到了一次請(qǐng)求的參數(shù)部分和返回值部分,解析后顯示出來就行了。在公司項(xiàng)目中使用,可以在后臺(tái)界面設(shè)置開關(guān)打開,打開后就能監(jiān)測(cè)接口返回?cái)?shù)據(jù)了。

 SGQMockObject *loadingObject = [SGQMockObject objectWithRequest:request response:response responseData:data error:error responseTime:request.responseTime];
[[SGQRequestListener sharedInstance] addAnObject:loadingObject];
)

附一張我在項(xiàng)目中使用的圖

pod 'RequestListener'

// 注意,需要在設(shè)置根window后才能調(diào)用
[[SGQRequestListener sharedInstance] startMock];
1.png

Github

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

  • 前言 ??因?yàn)镈NS發(fā)生域名劫持,所以需要手動(dòng)將URL請(qǐng)求的域名重定向到指定的IP地址,但是由于請(qǐng)求可能是通過NS...
    小盟城主閱讀 5,295評(píng)論 5 21
  • 本文是逐行翻譯,便于參照原文,如有歧義或者疑問請(qǐng)閱讀原文比較。于 2017.1.25===============...
    Auditore閱讀 1,618評(píng)論 4 5
  • WKWebView 是蘋果在 WWDC 2014 上推出的新一代 webView 組件,用以替代 UIKit 中笨...
    Aiana閱讀 4,808評(píng)論 1 8
  • iOS開發(fā)系列--網(wǎng)絡(luò)開發(fā) 概覽 大部分應(yīng)用程序都或多或少會(huì)牽扯到網(wǎng)絡(luò)開發(fā),例如說新浪微博、微信等,這些應(yīng)用本身可...
    lichengjin閱讀 4,040評(píng)論 2 7
  • 我們先看一下AFNetworking.h文件都給了我們什么方法 #import <Foundation/Found...
    瀟巖閱讀 768評(píng)論 0 1

友情鏈接更多精彩內(nèi)容