为 Fabric MOD 一个卡顿检测功能

卡顿检测系统,用于检测 App 的主线程运行情况。在追求 N 个9奔溃之外,卡顿也是我们极其重要运行指标。
很遗憾,世界上最好的免费 APM 平台 Fabric 缺没有。而国内的 bugly、网易云捕等,都提供了类似的功能。如云捕的卡顿功能;

卡顿功能

说起来卡顿检测,技术原理很简单, 下面是来自 bugly 的 QA 里的描述

iOS 卡顿检查的依据是监控主线程 Runloop 的执行,观察执行耗时是否超过预定阀值(默认阀值为3000ms) 在监控到卡顿时会立即记录线程堆栈到本地,在App从后台切换到前台时,执行上报。

卡顿检测系统,这个大任务,可以分解为两部分:卡顿的检测 + 卡顿的展示和管理。

第一步,卡顿的收集

卡顿的收集,有现成的代码,核心代码可查看, gist

检测到之后,需要获取当前时刻的堆栈,所有线程的堆栈(其实只需要主线程就够了)。
OK,拿来主义,一个很有名 PLCrashReporter。我自己集成测试,在我的老古董机器,iPhone 6跑,卡顿是能检测到,但是整个软件基本不可用,整个界面全卡住了。PLCrashReporter 生成日志,

NSData *data = [crashReporter generateLiveReport];

性能非常差,完全不可用。

拿不到堆栈信息,无法展示,所以只能采用造轮子的方式。根据戴铭 blog 里的例子,我改造了下,如 gist 所示。我们收集到了所有堆栈的栈顶地址;接下来我们需要将这些栈信息符号化。
很容易想到的方案是传到自己的服务器上,用 Mac 环境处理堆栈的符号化,转换为可读的堆栈数据——代价太大,而且还不经济。

在浏览了 fabric 的各项 API 后,我发现有一个很讨好的接口,recordCustomExceptionName

2. 符号化堆栈,卡顿管理

fabric 提供了 recordCustomExceptionName接口,接口签名是

/**
 *  This method can be used to record a single exception structure in a report. This is particularly useful
 *  when your code interacts with non-native languages like Lua, C#, or Javascript. This call can be
 *  expensive and should only be used shortly before process termination. This API is not intended be to used
 *  to log NSException objects. All safely-reportable NSExceptions are automatically captured by
 *  Crashlytics.
 *
 *  @param name       The name of the custom exception
 *  @param reason     The reason this exception occurred
 *  @param frameArray An array of CLSStackFrame objects
 */
- (void)recordCustomExceptionName:(NSString *)name reason:(nullable NSString *)reason frameArray:(CLS_GENERIC_NSARRAY(CLSStackFrame *) *)frameArray;

我们利用这个接口,将第一步收集的堆栈数据传给 fabric,让 fabric 给我们符号化,而且 fabric 卡顿日志还能够聚合、分类、分组、跟踪。crash 日志的那一套都可以用上, fabric 用户对此是熟悉的。核心代码如下

// 将存储在 buffer 里的所有地址,生成对应的 stackFrame,用于给 crashlytics 来生成日志。
    NSMutableArray *callFrames = [NSMutableArray arrayWithCapacity:10];
    for(int k = 0; k < buffer.count; k++){
        NSUInteger addr = [[buffer objectAtIndex:k] unsignedLongValue];
        CLSStackFrame *sf = [CLSStackFrame stackFrameWithAddress:addr];
        [callFrames addObject:sf];
    }
    NSString *reason = [NSString stringWithFormat:@"卡顿的超时标准是,%ld",(long)limit];
    [[Crashlytics sharedInstance] recordCustomExceptionName:@"主线程卡顿的统计" reason:reason frameArray:callFrames];

至此,我们用很少的代价就做好了一个卡顿检测的系统,并且和奔溃功能一起使用,集中处理 APM 各项指标。

这个方案在月活几十万的 App 应用了快半年了。用户一点都没有感受到有卡顿系统,对现有系统影响很小。


卡顿检测 dashboard

所有的数据都在 crashlytics 里,选择 non-fatal,即可,从统计下来的数据来看,卡顿主要体验在;

  1. 读写文件
  2. 读写数据库
  3. 处理图片
  4. 动画渲染
  5. 在非主线程读一些主线程才能用的属性(奇怪吧?)

基本上和我们猜想是一致的,接下来就需要跟踪和处理这些卡顿。
目前这个系统有两个缺陷;

  1. 检查卡顿本身的 runloop 也被认为是卡顿
  2. 因为 recordCustomExceptionName 接口的限制,所有线程的栈都被合并到一个栈,但不影响核心卡顿代码的阅读。
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。