第4章:使用Systrace分析ANR
说到ANR排查,我最常用的工具不是logcat,而是Systrace。为什么?因为logcat只能告诉你“发生了什么”,而Systrace能告诉你“为什么发生”。今天咱们就聊聊怎么用这个神器。
我个人习惯在定位ANR时,先抓一份Systrace,再看logcat。顺序反了反而浪费时间。你想想看,ANR的本质是主线程卡住太久,那到底是CPU抢不到时间片?还是Binder调用超时?或是有个锁被死死拽住?Systrace一打开,这些全都一目了然。
4.1 什么是Systrace?它能看什么?
Systrace是Android平台上的性能跟踪工具。它通过ftrace内核机制,把系统层面和应用层面的活动都记录下来。说白了,就是给Android系统装了一台“行车记录仪”。
它能监控以下几类关键信息:
| 类别 | 具体内容 |
|---|---|
| CPU调度 | 每个CPU核心上跑了哪些线程、运行时长、是否被抢占 |
| Binder通信 | 线程间IPC调用、阻塞状态、等待原因 |
| 锁竞争 | 同步锁(synchronized、Lock)的等待和持有 |
| 输入事件 | 输入分发的延迟、丢帧情况(jank) |
| SurfaceFlinger | 帧合成、屏幕刷新同步 |
我在项目中遇到过这样一个案例:一个ANR日志里主线程一直卡在InputStream.read(),logcat看不出任何端倪。后来抓了Systrace,发现当时CPU 0-7号核心全部满负载跑别的后台线程,主线程根本没分到时间片。这就是典型的“CPU饥饿”——光看logcat永远找不到根因。
4.2 抓取Systrace的三种方式
抓Systrace很简单,但不同场景要用不同方法。我分情况讲:
4.2.1 通过命令行(最通用)
python systrace.py -t 10 -o mytrace.html -a com.example.yourapp \
sched freq idle load am wm gfx view webview
参数说明:
-t 10:抓10秒–o mytrace.html:输出为HTML文件,可直接在Chrome打开-a 包名:指定要跟踪的应用进程- 后面的sched、freq等是跟踪类别,常用sched(调度)、gfx(图形)、view(视图)
freq和idle看CPU频率变化。
4.2.2 通过Android Studio Profiler(适合调试阶段)
Android Studio 3.2以上版本自带CPU Profiler,点“Record”开始录制。优点是可视化好,能看到实时堆栈。缺点是录制时长有限,而且对低端设备有性能开销。我个人只在调试重现问题时用它。
4.2.3 通过Perfetto(新项目推荐)
Perfetto是Systrace的下一代工具,从Android 10开始原生支持。抓取命令稍有不同:
adb shell perfetto -o /data/misc/perfetto-traces/trace.perfetto-trace \
-t 10s sched freq idle am wm view
抓完后用adb pull拉下来,在ui.perfetto.dev上打开分析。Perfetto的UI比Systrace更现代,过滤功能更强。嗯,这里要注意:如果你的APP还在用旧API(比如Trace.beginSection),Perfetto同样兼容。
4.3 读懂Systrace的时间轴
打开抓好的Systrace HTML文件(用Chrome),你会看到密密麻麻的彩色条带。别慌,我教你看核心区域:
上半部分:CPU调度行
每个CPU核心一行,上面跑着不同颜色的线程块。颜色代表不同进程。鼠标悬停能看到线程名、优先级、运行时长。
下半部分:进程内部跟踪
这一层是按进程展开的,你会看到:
- 主线程(通常是“包名”或“main”)
- 渲染线程(RenderThread)
- 其他线程(binder线程池、线程池等)
关键信号:如果主线程的条带出现大面积空白或灰色块,说明它没在运行(被挂起或阻塞)。如果出现红色“S”标记,说明发生了调度延迟。
- 找到ANR发生的时间点(可以在logcat里找到ANR时间戳,或直接在Systrace里搜索“ANR”字符串)
- 放大那个时刻,看主线程在干嘛:是处于Runnable但没被调度?还是处于Blocked等待锁?
- 从主线程的堆栈反推,找到导致阻塞的根因(比如Binder调用、I/O操作、锁竞争)
4.4 实战:用Systrace定位两个典型ANR
案例1:主线程等待Binder响应
Systrace上,主线程条带出现一大段黄色块,上面标着“Binder: waiting for reply”。鼠标点进去看详细信息,会显示正在等待的ServiceName和接口。
这种情况多半是系统服务响应超时,比如PackageManagerService在查询大量应用信息时卡住了。我曾经遇到过某款低端机,系统PackageManager在安装大量应用后查询速度极慢,导致主线程ANR。解决方案是在子线程做这类查询,或者使用缓存。
案例2:Room数据库写操作堵塞主线程
主线程在RoomDatabase.query()或insert()处卡住,Systrace上可以看到一个很宽的灰色块,表明主线程正在等一个锁。同时你会在另一个线程(比如RoomTaskExecutor线程)上看到数据库写操作在持续执行。
这通常是数据库事务冲突或WAL模式下的写放大。我之前遇到一个项目,用户反馈App经常ANR,一查Systrace发现Room在onCreate()里执行了migration操作(表结构变更),主线程等了一两秒才返回。
query()或insert()——即使你用了LiveData,Room在首次观察数据时也会在主线程做一次同步查询来获取初始值。我用allowMainThreadQueries()测试过,结果就是稳定ANR。
4.5 用Systrace对比“ANR窗口期”和“正常期”
很多同学只看ANR发生时刻的Systrace,但我建议你再加一步:抓一份正常操作下的Systrace做对比。比如用户在列表滑动时不卡,但点击某个按钮就ANR。对比两份trace,你能清晰看到差异点。
具体做法:
- 正常操作时抓一份trace(比如滑动5秒)
- 重复触发ANR场景抓另一份trace
- 在Chrome里同时打开两个tab,左右对比
重点看主线程的“调度间隙”——正常时每几十微秒就调度一次,ANR时可能出现几百毫秒没有调度。这种差异直接指向问题根因。
4.6 本章知识结构
4.7 避坑清单
- 别只抓一次。我曾经复盘一个ANR,第一次trace没抓到,第二次才成功。无规律性ANR需要多次抓取。
- 注意trace文件大小。长期录制会占几百MB,拉取时很慢。建议用
-t 10限制时间。 - 不要忽略“idle”状态。主线程进入idle不一定就是坏事(比如正常等待用户输入),但如果idle时长>5秒且伴随着CPU空转,那就是问题。
- 结合logcat一起看。Systrace能看到“是什么”,但不知道“为什么会有这个操作”。比如主线程在等
SharedPreferences写磁盘,Systrace只显示线程状态,而logcat里log才告诉你哪个业务触发了写操作。