卡顿监控

2020-09-12

一、背景描述

卡顿就是在应用使用过程中出现界面不响应或者界面渲染粘滞的情况。而应用界面的渲染以及事件响应是在主线程完成的,出现卡顿的原因可以归结为主线程阻塞。在开发过程中,遇到的造成主线程阻塞的原因可能是:

  • 主线程在进行大量I/O操作:为了方便代码编写,直接在主线程去写入大量数据;
  • 主线程在进行大量计算:代码编写不合理,主线程进行复杂计算;
  • 主线程在等锁:主线程需要获得锁A,但是当前某个子线程持有这个锁A,导致主线程不得不等待子线程完成任务。
    针对这些问题,如果我们能够捕获得到卡顿当时应用的主线程堆栈,那么问题就迎刃而解了。有了堆栈,就可以知道主线程在什么函数哪一行代码卡住了,是在等什么锁,还是在进行I/O操作,或者是进行复杂计算。有了堆栈,就可以对问题进行针对性解决。

二、常用方案

1. 起一个子线程”ping”主线程
Doraemon使用此方法。
原理:
我们经常会通过如下方法:
dispatch_async(dispatch_get_main_queue(), ^{});
向主线程派发任务,正常情况下,派发的block会在下一次runloop中执行,但如果主线程被卡住,则该block不会被执行,由此来判断主线程是否出现了卡顿存在的问题:
获取的堆栈可能不准确。因为检测到卡顿时获取的堆栈只能表示当前执行到最后一个调用时超过了阈值,但可能是堆栈列表其他方法的执行时间过长。
所以需要根据堆栈信息上下文来分析可能存在的卡顿点,不能只看最后一个调用栈。
2. 通过runloop observer监控
matrix使用此方法。原理:
通过向主线程runloop关键的位置注册observer,当runloop执行到相应位置时,会向注册的observer通知相应的事件。具体主要是两个时机:

  • afterwaiting,此时runloop被唤醒,作为统计本次runloop时长的起始点
  • beforewaiting,此时runloop即将陷入休眠,表示主线程没有处于卡顿的状态,可以清空统计时长
    每隔一定时间间隔(例如1s),获取一次runloop状态,如果获取到本次runloop的运行时长超过一定阈值(例如2s),则认定主线程出现卡顿。
    如果检测到出现卡顿后再获取调用栈,那就存在和”ping”方案相同的问题,为了解决这个问题,matrix使用了如下方法来提高卡顿堆栈的准确性:
    为了准确获取卡顿时的调用栈,子线程每1秒都会保存主线程调用栈,具体是每隔50ms将堆栈保存到一个先进先出的队列中,最多保存20组,当检测到卡顿时,将保存的堆栈队列取出来进行分析,通过判断堆栈顶部重复的次数来判断卡顿出现的位置。例如20次记录中,有连续15个记录的堆栈顶完全相同,则认为此栈顶方法即是出现卡顿的方法。

三、选定方案

因为”ping”的方案中存在问题,目前倾向于使用runloop observer的方案,但也可以考虑将”ping”和matrix中获取堆栈的方法结合起来。

四、数据收集

收集到卡顿的堆栈后,通过接口进行上报,并根据堆栈进行合并,具体做法可以参考crash统计的方法,看看是否可以根据堆栈划分到具体的模块和人员.