iOS版本的RequestListener

Target

监听 app 内所有的网络请求,并将请求的参数和返回值显示在手机端。相当于自己抓自己的包,这样不在电脑前也能够精确的观察接口动态,或者在接手一个新的项目时,可以清楚的看到某个界面的接口请求情况,帮助理清楚界面逻辑。Demo里面监测的是 UIWebView,实际在项目里面监测api接口效果更佳。

0.png
2.png

原理

原理是使用 NSURLProtocol 拦截所有 URL Loading System 中发出 request 请求。 拦截到之后,以我们的方式发出这个请求,这样这个请求的返回数据就能被我们统一捕获。同时,我们将返回的数据回调给原始发出者,以保证app正常运行。在捕获返回数据和请求本身的参数后就能完整的将一次api调用显示出来了。

NSURLProtocol介绍

NSURLProtocol 是属于 Foundation 框架里的 URL Loading System 的一部分。它是一个抽象类, 需要继承它后, 重写一系列父类的方法, 且在向系统注册后, 就可以拦截到所有来自 URL Loading System 中发出 request 请求, 包括使用 NSURLConnectionNSURLSession 发出去的请求, 使用这两者的第三方框架就也能监听到, 比如 AFNetWorking。而视图方面, 通过UIWebViewWKWebView 发出去的请求也能被监听到(WKWebView 的拦截会有些问题)。

96521-804444072007e819.png

拦截到后我们可以修改原来 requeset,我们可以什么都不做,那么这个请求的行为就会跟之前的一模一样。 有趣的是我们也可以对它进行修改,比如给它添加参数,让这个请求行为发生变化。或者对返回的 response 进行修改,亦或干脆重定向到新的资源,你想要A,我返回给你你B。总之, 是否返回数据, 返回什么数据, 已经由我们决定了。

这里我们让这个请求以我们写的方式发送出去,以便拿到服务端返回的数据。

拦截请求的方式

  • 对于 UIWebViewNSURLConnection 只需要构建 NSURLProtocol 的子类,在子类中重载必要的方法, 并向系统注册[NSURLProtocol registerClass:[SGQURLProtocol class]]; 即可拦截.

      #import <Foundation/Foundation.h>
    
      @interface SGQURLProtocol : NSURLProtocol
    
      @end
    
  • 对于 NSURLSession,需要通过配置 NSURLSessionConfiguration 对象的 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]];
          }
    
  • 对于 WKWebView,除了上述操作外, 由于其基于 wekkit 内核, 使用到了 WKBrowsingContextControllerregisterSchemeForCustomProtocol。 我们需要通过反射的方式拿到了私有的 class & selector。通过 kvc 取到browsingContextController,通过把注册把 httphttps 请求交给 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"];
          }
      }
    

这里需要声明的是,对于 WKWebView里面的请求,这样注册后,虽然可以拦截到,但是由于系统原因,会导致POST请求的请求体会丢失,导致请求本身会失败WKWebView NSURLProtocol问题,所以我们主要还是监听 app 内本身的请求。

NSURLProtocol子类中需要重写的方法

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

static NSString * const kHandedRequestKey = @"kHandedRequestKey";

@implementation SGQURLProtocol

/*
 是否对这个请求进行拦截
 返回YES,则这个request还会进入后续方法调用
 返回NO,则不会对这个request有任何影响了。
 */
+ (BOOL)canInitWithRequest:(NSURLRequest *)request {
    if ([[SGQRequestListener sharedInstance] isRequestURLInBlackList:request]) {
        return NO;
    }
    // 这个标记在 startLoading 方法中打上,是为了防止死循环。因为我们在 startLoading方法发出去的请求也会被拦截到进到这里
    if ([NSURLProtocol propertyForKey:kHandedRequestKey inRequest:request]) {
        return NO;
    }
    
    return YES;
}

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

/// 一般可以在这里copy出一个可变的request,进行属性的修改,然后返回。后续则会这个返回的request发出请求
+ (NSURLRequest*)canonicalRequestForRequest:(NSURLRequest*)request {
    return request;
}


/*
 最终那个被拦截的request或者会进到这里,我们在这里发出请求,获取返回数据。
 同时也将数据回调给原始的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

可以看到,在这里我们成功地拿到了一次请求的参数部分和返回值部分,解析后显示出来就行了。在公司项目中使用,可以在后台界面设置开关打开,打开后就能监测接口返回数据了。

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

附一张我在项目中使用的图

pod 'RequestListener'

// 注意,需要在设置根window后才能调用
[[SGQRequestListener sharedInstance] startMock];
1.png

Github

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,324评论 5 476
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,303评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,192评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,555评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,569评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,566评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,927评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,583评论 0 257
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,827评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,590评论 2 320
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,669评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,365评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,941评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,928评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,159评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,880评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,399评论 2 342

推荐阅读更多精彩内容

  • 前言   因为DNS发生域名劫持,所以需要手动将URL请求的域名重定向到指定的IP地址,但是由于请求可能是通过NS...
    小盟城主阅读 5,094评论 5 21
  • 本文是逐行翻译,便于参照原文,如有歧义或者疑问请阅读原文比较。于 2017.1.25===============...
    Auditore阅读 1,491评论 4 4
  • WKWebView 是苹果在 WWDC 2014 上推出的新一代 webView 组件,用以替代 UIKit 中笨...
    Aiana阅读 4,549评论 1 8
  • iOS开发系列--网络开发 概览 大部分应用程序都或多或少会牵扯到网络开发,例如说新浪微博、微信等,这些应用本身可...
    lichengjin阅读 3,633评论 2 7
  • 我们先看一下AFNetworking.h文件都给了我们什么方法 #import <Foundation/Found...
    潇岩阅读 595评论 0 1