最近在极客时间学习,做一下总结。
Android卡顿排查工具
常用的排查工具分为两种类型:
- instrument类型。获取一段时间内所有函数的调用情况,可以通过分析这段时间内函数调用流程,再进一步分析待优化的点
- sample类型。有选择性或者采用抽样的方式观察某些函数调用过程,可以通过这些有限的信息推测出流程中的可疑点,然后再继续分析。
1. TraceView
原理是利用Android的Runtime函数调用的event事件,将函数运行的耗时和调用关系写入trace文件中。属于instrument类型,可以查看整个函数调用流程,但是工具本身带来的性能开销过大,有时候无法反映真实情况。在Android5.0后增加了startMethodTracingSampling方法,可以使用基于样本的方式进行分析,以减少分析对运行时的性能影响。
对release包的支持不好,例如无法反混淆。
2. Nanoscope
Uber开源,instrument类型,对性能损耗比较小。其实现原理是直接修改Android虚拟机源码,在ArtMethod执行入口和执行结束位置增加埋点,将所有信息先写入内存,等trace结束后才统一生成结果文件。支持分析任意一个应用,可用于作竞品分析。
3. systrace
通常可以用来作为跟踪系统等I/O操作、CPU负载,Surface渲染,GC等事件。其原理是在系统各个关键位置都添加了一些性能探针,也就是在代码里加了一些性能监控等埋点。
只能监控特定系统调用的耗时情况,所以它属于sample类型,而且性能开销非常低。如果需要支持应用程序耗时分析,需要配合Trace.beginSection()
和Trace.endSection()
在重要函数的入口和出口埋点这种编译时给每个函数插桩的方式实现。出于性能考虑,我们会过滤大部分指令数较少的函数,这样就实现了在systrace基础上增加应用程序耗时的监控。这样做还有以下好处:
- 可以看到整个流程系统和应用程序的调用流程。包括系统关键线程的函数调用,如渲染耗时、线程所、GC耗时等。
- 性能损耗可以接受。由于过滤了大部分的短函数,而且没有放大I/O,所以整个运行耗时不到原来的两倍,基本可以反映真实情况。
4. Simpleperf
主要用于分析Native函数的调用,原理是利用CPU的性能监控单元(PMU)提供的硬件perf事件。
在Android Studio 3.2 以上的Profiler中集成了几种分析工具,其中
- Sample Java Methods 类似于Traceview的sample类型
- Trace Java Methods 类似于Traceview的instrument类型
- Trace System Calls 类似于systrace
- SampleNative(API Level 26+)类似于Simpleperf
总的来说,如果需要分析Native代码的耗时,可以用Simpleperf;如果想分析系统调用,可以选择systrace;如果想分析整个程序自行流程的耗时,可以选择TraceView或者插桩版本的systrace。
卡顿监控
设计监控工具
-
消息队列
通过监控线程,每隔1秒向主线程消息队列头部插入一条空消息。假设1秒后该消息仍然没有被主线程消费掉,说明阻塞消息运行的时间在0~1秒之间。换句话说,如果我们需要监控3s卡顿,那么在第4次轮询中头部消息依然没有被消费的话,就可以确定主线程出现了一次3s以上的卡顿。但是该监控并不准确,正在运行的函数并不一定是耗时函数,下面这张图就是其中一种情况,错误地把C函数认为是卡顿函数:
但是,对于线上大数据来说,因为A和B函数相对耗时,所以抓取到它们的概率更大,通过后台聚合捕获到A和B的卡顿日志也相对更多一些。
插桩
类似systrace可以通过插桩自动生成Trace Tag,我们也可以在函数入口和出口加入耗时监控代码。
- 避免方法数暴增。在函数的入口和出口应该插入相同的函数,在编译时提前给代码中的每个函数分配一个独立的ID作为参数。
-
过滤简单的函数。过滤一些类似直接return、i++这样的简单函数,并且支持黑名单配置。对于一些调用非常频繁的函数,需要添加到黑名单中来降低整个方案对性能的损耗。
不过该方法只能监控应用内对函数耗时,无法监控系统的函数调用。
- Profile
Facebook开源性能监控工具Profilo,目前已投入到线上使用。
主线程耗时外的监控
- 帧率
使用Choreographer来监控。跟卡顿不同的是,需要排除页面没有操作的情况,我们应该只在界面存在绘制的时候才做统计。监听addOnDrawListener实现界面是否存在绘制行为:
getWindow().getDecorView().getViewTreeObserver().addOnDrawListener
出现丢帧的时候,获取当前页面信息、View信息和操作路径上报后台,降低二次排查的难度。
生命周期监控
对于组件生命周期应该采用更加严格地监控,可以全量上报。在后台我们可以看到各个组件各个生命周期的启动时间和启动次数。
此外,还需要监控各个进程生命周期的启动次数和耗时,可以看出某些进程是否频繁地拉起。线程监控
- 线程数量。监控数量多少,创建方式。主要用于进行线程的收敛,减少线程数量。
- 线程时间。监控用户时间utime和系统时间stime和优先级。主要看哪些线程utime+stime时间比较多,占用了过多的CPU。
线上卡顿分析
Java实现
- 获取线程状态:
Thread.getState()
。对于线程状态的定义以及转换可参考《Java线程Dump分析》。 - 获取所有线程堆栈:
Thread.getAllStackTraces()
。需要注意Android 7.0,getAllStackTraces
是不会返回主线程的堆栈的。
SIGQUIT信号实现
通过kill -3 pid可以获得ANR日志,里面包含了所有线程的状态所有线程的状态、CPU 时间片、优先级、堆栈和锁的信息应有尽有。其中 utm 代表 utime,HZ 代表 CPU 的时钟频率,将 utime 转换为毫秒的公式是“time * 1000/HZ”。例子中 utm=218,也就是 218*1000/100=2180 毫秒。
// 线程名称 ; 优先级 ; 线程 id; 线程状态
"main" prio=5 tid=1 Suspended
// 线程组 ; 线程 suspend 计数 ; 线程 debug suspend 计数 ;
| group="main" sCount=1 dsCount=0 obj=0x74746000 self=0xf4827400
// 线程 native id; 进程优先级 ; 调度者优先级 ;
| sysTid=28661 nice=-4 cgrp=default sched=0/0 handle=0xf72cbbec
// native 线程状态 ; 调度者状态 ; 用户时间 utime; 系统时间 stime; 调度的 CPU
| state=D schedstat=( 3137222937 94427228 5819 ) utm=218 stm=95 core=2 HZ=100
// stack 相关信息
| stack=0xff717000-0xff719000 stackSize=8MB
其中日志中的线程状态是Native线程的状态:
线程间的死锁和热锁分析可参考《手QAndroid 线程死锁监控与自动化分析实践》
Hook实现
SIGQUIT信号量获取ANR日志会有获取traces文件的权限问题,同时获取所有线程堆栈以及各种信息非常耗时。
可以通过以下两个步骤获得线程堆栈方法:
- 通过libart.so、dlsym调用ThreadList::ForEach
方法,拿到所有的Native线程对象。 - 遍历线程对象列表,调用Thread::DumpState
方法
同时,结合fork子进程,避免影响主进程。