卡顿原因
主要是主线程阻塞。在开发过程中,遇到的造成主线程阻塞的原因可能是:
- 主线程在进行大量I/O操作:为了方便代码编写,直接在主线程去写入大量数据
- 主线程在进行大量计算:代码编写不合理,主线程进行复杂计算
- 大量UI绘制:界面过于复杂,UI绘制需要大量时间
- 主线程在等锁:主线程需要获得锁A,但是当前某个子线程持有这个锁A,导致主线程不得不等待子线程完成任务。
- …
业界调研
微信团队(Matrix)
卡顿检测流程图
主线程卡顿表现
- FPS降低
- CPU占用率非常高
- 主线程RunLoop执行时间过长
监控方法
Matrix 卡顿监控在 RunLoop 的起始最开始和结束最末尾位置添加 Observer,从而获得主线程的开始和结束状态。卡顿监控起一个子线程定时检查主线程的状态,当主线程的状态运行超过一定阈值则认为主线程卡顿,从而标记为一个卡顿。
采用两个准则:
- 单核CPU 占用超过了80%
- 主线程 RunLoop 执行了超过2秒
微信公开使用的卡顿监控中,主程序 Runloop 超时的阈值是 2 秒,子线程的检查周期是 1 秒,每隔 1 秒,子线程检查主线程的运行状态
如果检查到主线程 Runloop 运行超过 2 秒则认为是卡顿,并获得当前的线程快照。同时,微信团队也认为 CPU 过高也可能导致应用出现卡顿,所以在子线程检查主线程状态的同时,如果检测到 CPU 占用过高,会捕获当前的线程快照保存到文件中
目前微信应用中认为,单核 CPU 的占用超过了 80%,此时的 CPU 占用就过高了
检测策略
- 内存 dump:每1秒检查一次,如果检查到主线程卡顿,就将所有线程的函数调用堆栈 dump 到内存中
- 文件 dump:如果内存 dump 的堆栈跟上次捕捉到的不一样,则 dump 到文件中;否则按照斐波那契数列将检查时间递增(1,1,2,3,5,8…)直到没有遇到卡顿或卡顿堆栈不一样。这样能够避免同一个卡顿写入多个文件的情况,也能避免检测线程围着同一个卡顿空转的情况
退火算法
为了降低检测带来的性能损耗,为检测线程增加了退火算法:
- 每次子线程检查到主线程卡顿,会先获得主线程的堆栈并保存到内存中(不会直接去获得线程快照保存到文件中);
- 将获得的主线程堆栈与上次卡顿获得的主线程堆栈进行比对:
- 如果堆栈不同,则获得当前的线程快照并写入文件中;
- 如果相同则会跳过,并按照斐波那契数列将检查时间递增直到没有遇到卡顿或者主线程卡顿堆栈不一样。
这样,可以避免同一个卡顿写入多个文件的情况;避免检测线程遇到主线程卡死的情况下,不断写线程快照文件。
耗时堆栈提取
子线程检测到主线程 Runloop 时,会获得当前的线程快照当做卡顿文件,但当前的主线程堆栈不一定是最耗时的堆栈,不一定是导致主线程超时的主要原因。Matrix 卡顿监控通过主线程耗时堆栈提取来解决这个问题
卡顿监控定时获取主