卡顿检测系统,用于检测 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,即可,从统计下来的数据来看,卡顿主要体验在;
- 读写文件
- 读写数据库
- 处理图片
- 动画渲染
- 在非主线程读一些主线程才能用的属性(奇怪吧?)
基本上和我们猜想是一致的,接下来就需要跟踪和处理这些卡顿。
目前这个系统有两个缺陷;
- 检查卡顿本身的 runloop 也被认为是卡顿
- 因为
recordCustomExceptionName
接口的限制,所有线程的栈都被合并到一个栈,但不影响核心卡顿代码的阅读。
网友评论