制作API日志阅读器

Posted by 开不了口的猫 on August 15, 2017

前言

当我们用手机连接Xcode或直接打开Xcode的模拟器进行API请求调试时,我们可以通过XcodeConsole实时查看输出的API日志来定位数据问题,自不必说,这算是iOS程序猿的常规操作。让我们想象另一个场景,在测试冒烟阶段我们拿到了一台测试真机,你被抱怨到xxx模块的业务数据显示异常,为了提高冒烟效率,测试并不会第一时间帮你通过抓包工具去定位接口数据异常的原因,此时,如果程序中能有一个实时查看API日志的阅读器,就能省去你连接Xcode进行接口调试的一系列操作了。

API日志阅读器作为子工具组件已被集成到iOS真机桌面级调试工具中,可以直接clone或通过Cocoapods集成,欢迎Star🤓

阅读器的设计

怎样设计一个API日志阅读器更为人性化呢?我认为需要满足以下特点。

  • 阅读器的显示/隐藏交互方式应该更为便捷
  • 对于API请求和响应日志应该通过不同颜色区分开来,失败的响应日志也应该与成功的响应日志区别开
  • 单条API日志应该被格式化为易于阅读的样式
  • 阅读器的日志最好能实时更新而不是需要手动刷新,并且能够自动滚动到最新的日志的位置
  • 阅读器应该内置关键字检索功能
  • 阅读器最好内置一个清空日志的功能
  • 鉴于抓包工具的普及,可以提供一种将整个API作为单位的列表视图,以时间顺序进行排序,点击某一条API后可以查看具体的请求体与响应体

为满足以上特点,在左右斟酌之后,我决定制作能提供两个不同日志视图的阅读器。一个视图以日志作为输出单位,将不同API的请求/响应日志输出在一起(就像研发人员在XcodeConsole查看API日志一样),提供关键字检索和颜色标识,我暂且称之为离散型API日志阅读器。而另一个视图则以API作为输出单位,在二级视图展示已按照时间排序后的API列表,然后就像在抓包工具上查看API日志一样,点击一个感兴趣的API,然后查看详细的请求日志和响应日志,我暂且称之为绑定型API日志阅读器

日志数据源

鉴于我上一篇文章设计与实现了API日志输出工具TDFAPILogger,在制作TDFAPILogger的时候,我刻意保留了两个对外的接口用于实时提供格式化的日志模型。

/**
 API请求日志汇报者,
 会在格式化后(不包括emoji)的请求描述模型通过这个block传给外部
 */
@property (nonatomic,   copy) void(^requestLogReporter)(TDFALRequestModel *requestLogDescription);

/**
 API响应日志汇报者,
 会在格式化后(不包括emoji)的响应描述模型通过这个block传给外部
 */
@property (nonatomic,   copy) void(^responseLogReporter)(TDFALResponseModel *responseLogDescription);

因此我们建立了一个单例类TDFSDAPIRecorder,在启动方法- (void)thaw中监听TDFAPILogger的输出的API日志,并通过数组结构将它们保存下来。

- (void)thaw {
    TDFSDAPIRecorder *recorder = [TDFSDAPIRecorder sharedInstance];
    [TDFAPILogger sharedInstance].requestLogReporter = ^(TDFALRequestModel *requestLogDescription) {
        [recorder storeDescription:requestLogDescription];
    };
    [TDFAPILogger sharedInstance].responseLogReporter = ^(TDFALResponseModel *responseLogDescription) {
        [recorder storeDescription:responseLogDescription];
    };
}

在冻结方法freeze中注销监听。

- (void)freeze {
    [TDFAPILogger sharedInstance].requestLogReporter = NULL;
    [TDFAPILogger sharedInstance].responseLogReporter = NULL;
}

PS:thawfreezeTDFScreenDebugger组件中所有子组件的IO协议方法,用于统一定义子组件的启动冻结操作。

同样地,TDFSDAPIRecorder作为数据源的管理者也提供了清空当前日志的方法。而清空仅仅是将存储日志的数组清空而已。

- (void)clearAllRecords {
    self.descriptionModels = @[];
    self.requestDesModels = @[];
    self.responseDesModels = @[];
}

阅读器视图切换逻辑

在组件TDFScreenDebugger的主框架下,通过摇一摇手势在业务视图与日志视图之间切换,这为我们提供了比抓包工具更便利的阅读方式。其原理是在组件内部申请了一个自定义的window,将window的level调整到比较高的位置,然后通过这个window的隐藏与显示来切换视图。
有一个问题是当阅读器需要检索弹出键盘输入关键字时,我们需要将这个window设置为keywindow来响应键盘的交互事件,而当我们切换到业务视图时,则应该让业务window成为keywindow,我们需要像这样处理。

- (void)viewDidAppear:(BOOL)animated {
    [super viewDidAppear:animated];
    // apply to become keywindow for `TDFSDWindow` instance
    [[TDFSDManager manager] applyForAcceptKeyInput];
}

- (void)dealloc {
    // let `TDFSDWindow` instance gives up becoming keywindow
    [[TDFSDManager manager] revokeApply];
}

TDFSDManager也是在TDFScreenDebugger主框架的核心类之一。

- (void)applyForAcceptKeyInput {
    UIWindow *keyWindow = [[UIApplication sharedApplication] keyWindow];
    
    if (keyWindow != self.screenDebuggerWindow) {
        self.originWindow = keyWindow;
        [keyWindow resignFirstResponder];
        
        self.sd_canBecomeKeyWindow = YES;
        [self.screenDebuggerWindow makeKeyWindow];
    }
}

- (void)revokeApply {
    UIWindow *keyWindow = [[UIApplication sharedApplication] keyWindow];
    
    if (keyWindow == self.screenDebuggerWindow) {
        [keyWindow resignFirstResponder];
        
        self.sd_canBecomeKeyWindow = NO;
        [self.originWindow makeKeyWindow];
    }
}

实时显示API日志

在显示API日志的主控制器TDFSDAPIRecordConsoleController中,我们通过KVO来实时监听TDFSDAPIRecorder的API日志数组的变化并实时渲染到textView控件上,为了便捷实现,TDFScreenDebugger引入了ReactiveObjC来帮我更高效地实现KVO。

- (void)addAPIRecordPortObserve {
    RAC(self.apiOutputView, attributedText) = [[[RACObserve([TDFSDAPIRecorder sharedInstance], descriptionModels)
    skip:1]
    map:^id _Nullable(NSArray<__kindof TDFALBaseModel<TDFSDAPIRecordCharacterizationProtocol> *> *descriptionModels) {
        return [[descriptionModels.rac_sequence
               map:^id _Nullable(__kindof TDFALBaseModel<TDFSDAPIRecordCharacterizationProtocol> * _Nullable descriptionModel) {
                   
                   // mark `TDFALRequestModel` instances messageRead to YES
                   if ([descriptionModel isKindOfClass:[TDFALRequestModel class]]) {
                       [(TDFALRequestModel *)descriptionModel setMessageRead:YES];
                   }
                   return descriptionModel.outputCharacterizationString;
               }]
               foldLeftWithStart:[[NSMutableAttributedString alloc] initWithString:@""]
               reduce:^id _Nullable(NSMutableAttributedString * _Nullable accumulator, NSAttributedString * _Nullable value) {
                   return ((void)([accumulator appendAttributedString:value]),
                           (void)([accumulator appendAttributedString:[[NSAttributedString alloc] initWithString:@"\n\n"]]),
                           accumulator);
               }];
    }]
    deliverOnMainThread];
    
    @weakify(self)
    [[[[[RACObserve(self.apiOutputView, attributedText)
    skip:1]
    distinctUntilChanged]
    delay:0.2f]
    doNext:^(id  _Nullable x) {
        @strongify(self)
        if (self.loadingView.isAnimating) {
            [self.loadingView stopAnimating];
        }
    }]
    subscribeNext:^(id  _Nullable x) {
        @strongify(self)
        [self.apiOutputView scrollRangeToVisible:NSMakeRange(self.apiOutputView.attributedText.length, 1)];
        [self sendClearRemindLabelTextRequestWithContentType:SDAllReadNotificationContentTypeAPIRecord];
    }];
}

通过代码可以看到,使用了NSMutableAttributedString来实现 请求日志/成功响应日志/失败响应日志 的颜色区分,渲染后延迟0.2s执行了

[self.apiOutputView scrollRangeToVisible:NSMakeRange(self.apiOutputView.attributedText.length, 1)];

帮助我们永远滑动到最新的API日志的位置上。

添加关键字检索功能

我们可以通过正则表达式检索出API日志中所有包含关键的部分,然后结合NSTextStorageNSLayoutManager实现高亮效果。为了更加便捷地实现,我们引入了ICTextView,它被设计为一个可定制化的UITextView子类,内置实现了关键字检索和高亮功能。其内部通过给检索后的文字段(Rect)添加一块半透明的背景UIView,然后通过创建一个数组作为缓存池,能达到比较理想的检索性能。

绑定型API日志视图

按照之前的设计,鉴于抓包工具的普及,我们最好另外提供一种将整个API作为单位的列表视图,以时间顺序进行排序,点击某一条API后可以查看具体的请求体与响应体的视图。这种视图可能更加针对测试人员在测试阶段进行更加便捷的抓包行为,日志的可读性才是重中之重。

绑定型API日志视图的实现更为简单,这里就不赘述了。

最终效果

不说废话,直接上屏幕录制。(视频可能需要各位翻个墙,本人上传的Youtube🌚)