- 基础知识
- CPU 性能
- 卡顿问题分析指标
- 卡顿排查工具
- 卡顿监控
- 消息队列
- 字节码插桩
- 帧率 FPS
- 生命周期监控
- 线程监控
造成卡顿的原因可能有千百种,不过最终都会反映到 CPU 时间上。我们可以把 CPU 时间分为两种:用户时间和系统时间。用户时间就是执行用户态应用程序代码所消耗的时间;系统时间就是执行内核态系统调用所消耗的时间,包括 I/O、锁、中断以及其他系统调用的时间。
CPU 性能
评估一个 CPU 的性能,需要看主频、核心数、缓存等参数,具体表现出来的是计算能力和指令执行能力,也就是每秒执行的浮点数计算数和每秒执行的指令数。
我们可以通过以下方法获得设备的 CPU 信息:
// 获取 CPU 核心数
cat /sys/devices/system/cpu/possible
// 获取某个 CPU 的频率
cat /sys/devices/system/cpu/cpu0/cpufreq/cpuinfo_max_freq
不过在 Android 8 之后,是没权限访问这些文件了。
卡顿问题分析指标
出现卡顿问题后,首先我们应该查看 CPU 的使用率。怎么查呢?我们可以通过 /proc/stat 得到整个系统的 CPU 使用情况,通过 /proc/[pid]/stat 可以得到某个进程的 CPU 使用情况。
其中比较重要的字段有:
proc/self/stat:
utime: 用户时间,反应用户代码执行的耗时
stime: 系统时间,反应系统调用执行的耗时
majorFaults: 需要硬盘拷贝的缺页次数
minorFaults: 无需硬盘拷贝的缺页次数
如果 CPU 使用率长期大于 60%,表示系统处于繁忙状态,就需要进一步分析用户时间和系统时间的比例。对于普通应用程序,系统时间不会长期高于 30%,如果超过这个值,我们就应该进一步检查是 I/O 过多,还是其他的系统调用问题。
除了 CPU 的使用率,我们还需要查看 CPU 饱和度。CPU 饱和度反映的是线程排队等待 CPU 的情况,也就是 CPU 的负载情况。
CPU 饱和度首先会跟应用的线程数有关,如果启动的线程过多,容易导致系统不断的切换执行的线程,把大量的时间浪费在上下文切换,我们知道每一次 CPU 上下文切换都需要刷新寄存器和计数器,至少需要几十纳秒的时间。
我们可以通过 /proc/[pid]/schedstat 文件来查看 CPU 上下文切换次数,特别需要注意被动切换的次数。
proc/self/sched:
nr_voluntary_switches:
主动上下文切换次数,因为线程无法获取所需资源导致上下文切换,最普遍的是 IO。
nr_involuntary_switches:
被动上下文切换次数,线程被系统强制调度导致上下文切换,例如大量线程在抢占 CPU。
se.statistics.iowait_count:IO 等待的次数
se.statistics.iowait_sum: IO 等待的时间
另外一个会影响 CPU 饱和度的是线程优先级,线程优先级会影响 Android 系统的调度策略,它主要由 nice 和 cgroup 类型共同决定。nice 值越低,抢占 CPU 时间片的能力越强。当 CPU 空闲时,线程的优先级对执行效率的影响并不会特别明显,但在 CPU 繁忙的时候,线程调度会对执行效率有非常大的影响。
关于线程优先级,你需要注意是否存在高优先级的线程空等低优先级线程,例如主线程等待某个后台线程的锁。
Traceview、systrace 以及 AS 自带的 Profiler 工具。
Traceview 可以用来查看整个过程有哪些函数调用,但是工具本身带来的性能开销过大,有时无法反应真实的情况。
systrace 可以用来跟踪系统的 I/O 操作、CPU 负载、Surface 渲染、GC 等事件。systrace 工具只能监控特定系统调用的耗时情况,而且性能开销非常低,但是它不支持应用程序代码的耗时分析,所以在使用时有一些局限性。
AS 3.2 自带的 Profiler 中直接集成了几种性能分析工具,其中:
- Sample Java Methods 的功能类似于 TraceView 的 sample 类型
- Trace Java Methods 的功能类似于 Traceview 的 instrument 类型
- Trace System Calls 的功能类似于 systrace
- SampleNative(API Level 26+)的功能类似于 Simpleperf
消息队列
基于消息队列的卡顿监控方案,主要思想是,监控主线程执行耗时,当超过阈值时,dump 出当前主线程的执行堆栈,通过堆栈分析找到卡顿原因。具体做法是,通过替换 Looper 的 Printer 实现,每一个消息的执行,都会在其前后打印日志,我们只需要设置这个 Printer,通过两次调用 println 的时间间隔,就是一个消息执行的耗时,这也是 BlockCanary 的核心实现。而 360 的 ArgusAPM 的做法相对优雅些,它是在消息分发的开始,去 postDelay 一个 Runnable,在消息分发的结束去移除这个 Runnable,如果在指定的 delay 时间内没有移除,就说明是发生卡顿了,这个 Runnable 所做的事情就是把当前线程的堆栈打印出来,这种做法其实就是 View 中判断长按事件的方式一样。
但是这两种方式都有相同的弊端,就是 println 参数有大量的字符串拼接,可能会导致性能损耗严重。
还有一种方案是,可以通过一个监控线程,每隔 1 秒向主线程消息队列的头部插入一条空消息。假设 1 秒后这个消息并没有被主线程消费掉,说明阻塞消息运行的时间在 0~1 秒之间。换句话说,如果我们需要监控 3 秒卡顿,那么在第四次轮询中头部消息依然没有被消费的话,就可以确定主线程出现了一次 3 秒以上的卡顿。
这两种方案,可以较方便的捕捉到卡顿的堆栈,但其最大的不足在于,无法获取到各个函数的执行耗时,对于稍微复杂一点的堆栈,很难找出可能耗时的函数,也就很难找到卡顿的原因。另外,通过其他线程循环获取主线程的堆栈,如果稍微处理不及时,很容易导致获取的堆栈有所偏移,不够准确,加上没有耗时信息,卡顿也就不好定位。还有就是,获取主线程堆栈的代价也是巨大的,它需要暂停主线程的运行。
字节码插桩
Matrix 的做法是,在编译期间收集所有生成的 class 文件,扫描文件内的方法指令进行统一的打点插桩。为了减少插桩量以及性能损耗,通过遍历 class 方法指令集,判断扫描的函数是否只含有 PUT/READ/FIELD 等简单的指令,来过滤一些默认或匿名构造函数,以及 get/set 等简单不耗时的函数。为了方便以及高效记录函数执行过程,会为每个插桩的函数分配一个独立的 ID,在插桩过程中,记录插桩的函数签名以及分配的 ID,在插桩完成后输出一份 mapping,作为数据上报后的解析支持。
通过向 Choreographer 注册监听,在每一帧 doFrame 回调时判断距离上一帧的时间差是否超过阈值,如果超过了阈值即判定发生了卡顿,这时候就会把两帧之间的所有函数执行信息进行上报分析。同时,在每一帧 doFrame 到来时,重置一个计时器,如果 5s 内没有 cancel,则认为是发生了 ANR。
帧率 FPS
FPS 可以衡量一个界面的流畅性,但往往不能很直观的衡量卡顿的发生。一个稳定在 40、50 FPS 的页面,我们不会认为是卡顿的,但一旦 FPS 很不稳定,人眼往往很容易感知到,因此我们可以通过掉帧程度来衡量卡顿。
业界都是使用 Choreographer 来监听应用的帧率,跟卡顿不同的是,需要排除掉页面没有操作的情况,我们应该只在界面存在绘制的时候做统计。那么如何监听界面是否存在绘制行为呢?可以通过 addOnDrawListener 实现:
getWindow().getDecorView().getViewTreeObserver().addOnDrawListener()
关于跳帧的判断,可以直接抄 Choreographer 的 doFrame 实现,源码内部判断跳帧次数 >=30 即意味着发生了卡顿。
生命周期监控
Activity、Service、Receiver 组件生命周期的耗时和调用次数也是我们重点关注的性能问题。例如 Activity 的 onCreate() 不应该超过 1 秒,不然会影响用户看到页面的时间。Service 和 Receiver 虽然是后台组件,不过它们的生命周期也是占用主线程的,也是我们需要关注的问题。
对于组件生命周期我们应该采用更严格的监控,可以全量上报各个组件各个生命周期的启动时间和启动次数。
一般的做法是,通过编译时插桩来做到组件的生命周期监控。
线程监控
线程间的竞争或者锁可能会导致主线程空等,从而导致卡顿。对于线程监控,需要监控以下两点:
-
线程数量
需要监控线程数量的多少,以及创建线程的方式。例如有没有使用统一的线程池,这块可以通过 hook 线程的 nativeCreate() 函数,主要用于进行线程收敛,减少线程数量。
-
线程时间
监控线程的用户时间 utime、系统时间 stime 和优先级。主要是看哪些线程 utime+stime 时间比较多,占用了过多的 CPU。
最后
导致卡顿的原因有很多,比如函数非常耗时、I/O 非常慢、线程间的竞争或者锁等,其实很多时候卡顿问题并不难解决,相较解决来说,更困难的是如何快速发现这些卡顿点,以及通过更多的辅助信息找到真正的卡顿原因。
https://developer.android.com/studio/profile/android-profiler
https://developer.android.com/topic/performance/vitals/anr
https://github.com/markzhai/AndroidPerformanceMonitor