之前接到一个特别棘手的反馈:线上出现一批ANR,logcat里能看到系统弹了“Application Not Responding”,但反复拉取 /data/anr/ 下生成的 traces 文本,主线程的调用栈却平平无奇,看起来都是常态代码。后来我把这类问题的定位方式分享给同事,同事给了一句很精确的概括:“这类ANR,是需要Perfetto分析的ANR。”
这句话点醒了我——普通ANR靠读取系统生成的CPU调用栈就能定案,但有些ANR,问题根本不在“当时主线程正在执行什么Java代码”。它可能卡在锁等待上,线程没有被系统调度进CPU,或者CPU频率过低导致某段耗时操作被放大了。这些信息,传统日志给不了。Perfetto的强项是:把进程、线程、内核调度、CPU频率、Binder调用、锁竞争全部放在同一条时间线上,让你用显微镜看现场。
这篇就来讲讲,什么样的ANR必须要上Perfetto,怎么用Perfetto一次把这类问题查到底,以及我在实操中踩过的坑和沉淀下来的排查套路。内容适合有一定Android经验、正在和线上ANR缠斗的开发者,也适合刚开始接触Perfetto但不知道从哪里下手的同学。
1. 这个项目的起因:那些“一眼看不出问题”的ANR
1.1 什么样的ANR会把我们逼到Perfetto面前
先说我在某业务线遇到的三种典型场景,它们都符合“需要Perfetto分析的ANR”这个定义。
第一种,主线程在等子线程。系统日志里能看到主线程阻塞在 Object.wait 或者 ConditionVariable,持有锁的却是某个工作线程,但那个工作线程本身没有异常,它的调用栈只是停在业务代码的普通位置。这时候你得搞清楚,它到底是被什么阻塞了、等了多久、中间有没有被调度。
第二种,主线程看起来在正常执行,但整体速度奇慢。日志里调用栈是“正在执行某动画”或“正在处理某消息”,可这次执行的时间明显超出预期。CPU频率、热限频、核心迁移这些因素,在主线程调用栈里完全看不出来。
第三种,主线程甚至没有被调度。Perfetto的调度追踪能清楚看到“主线程处于R状态但一直没上CPU”,说明它一直排在runnable队列里,系统根本来不及给它分配运行时间。这种问题,如果你只看ANR文本栈,会以为主线程“消失了”,其实它还在排队。
这三种情况都属于典型的高难度ANR。普通方法排查它们的思路通常是:加大日志、复现、打点、看耗时分布。但打点只能回答“哪里慢了”,回答不了“为什么慢”,更看不到系统层面的调度、负载和锁竞争。
1.2 传统ANR排查工具为什么不够用
Android系统在发生ANR时会把ANR原盘、主线程及各进程的Java栈写到 /data/anr/ 下,Logcat里也会打印部分信息。这套机制对大多数ANR是有效的:
- 主线程无响应,栈里通常会显示主线程正在执行的代码位置;
- 如果是死锁,多线程的栈能直接看到互相持有的锁;
- 如果主线程在做耗时IO,栈里往往能看到文件读取或数据库操作的调用。
可问题是,栈只能说明“那一刻”程序执行到了哪里,它证明不了“为什么停留在这里”。比如:
| 现象 | 文本栈能看到 | 文本栈看不到 |
|---|---|---|
| 主线程等待子线程 | 主线程停在 wait | 子线程为什么慢,子线程是否被抢占了CPU |
| Binder调用阻塞 | 主线程停在 binder transaction | 对端进程是否有响应,Binder线程池是否耗尽 |
| CPU调频降频 | 看不到 | 当前CPU频率、调度器是否发生限频 |
| 线程排队没上CPU | 停在R状态 | 排队时长、被谁抢占、核心是否被隔离 |
当排查方向从“代码执行路径”转向“系统资源搏弈”时,就必须引入Perfetto这类基于内核Trace的系统级工具。
2. 重新认识ANR的判定机制:为什么光看调用栈会有盲区
2.1 ANR的触发标准与系统判定链路
ANR全称Application Not Responding,系统通过“超时+无响应”两个维度来判定。核心超时包括:
- 输入事件分发超时:触摸、按键等事件在5秒内没被处理完;
- 广播超时:前台广播约10秒,后台广播约60秒未执行完;
- 服务超时:前台服务约20秒,后台服务约200秒未完成;
- ContentProvider超时:新版本系统对Provider启动也有阈值约束。
超时发生后,系统会做两件事:一是弹ANR对话框给用户,二是把当前各进程线程的调用栈全部收集起来,生成trace文件。这个机制收集的就是“某一瞬态的观察结果”,相当于事故发生后立刻拍了一张全体快照。
这个快照的问题在于,它把“连续的过程”变成了“离散的某一帧”。如果主线程在等待一个网络请求,栈里永远是wait,不管这个请求是300毫秒还是3秒,你看到的都差不多。快照也区分不了“正在进行的合理等待”和“被外部拖住的无谓等待”。
2.2 从日志看ANR:哪些线索藏在字里行间
Logcat中ANR日志通常包含几段信息:
ANR in com.example.app (com.example.app/.MainActivity) Reason: Input dispatching timed out ... CPU usage from 0ms to 2047ms later (2020-07-01 12:00:00): ...这些信息里面,“Reason”字段和“CPU usage”字段值得仔细看。“CPU usage”一栏会列出各进程的CPU占用,它能告诉你系统整体负载高不高、嫌疑进程是不是在疯狂占CPU。
但日志有一层很难绕过的透明墙:它给不出时间线和因果关系。CPU usage统计的是“某一段窗口内的平均值”,它看不出某个线程在10毫秒级别上的调度抖动。Perfetto的ftrace数据能做到毫秒级切片,能还原每个线程在每个核心上运行了多久、被谁抢占了、休眠后谁唤醒的它。
2.3 为什么Perfetto能弥补调用栈的盲区
Perfetto追踪的数据来源之一是Linux内核的ftrace,它记录的是调度器事件、CPU频率变化、中断、锁等待等底层事件。举个例子,Perfetto能回答这几个关键问题:
- 主线程在ANR前10秒内,到底在CPU上跑了多少毫秒?睡眠了多少毫秒?
- 主线程醒来后,是立刻被调度到了CPU,还是排队排了800毫秒?
- 在那段时间里,系统总负载是多少?是哪个进程占了绝大多数CPU?
- CPU频率有没有被限制?是否因为高负载或温度触发了降频?
这些数据组合在一起,才能把“栈显示的代码”和“真实的机器行为”对应起来。用生活类比来说,普通ANR日志等于目击证人的口头描述:“他当时站在银行柜台前”,Perfetto等于银行的监控录像:“他从9点03分开始排队,9点04分被前面的人挡着没挤进去,9点05分窗口才开放”。后者才能定责。
3. Perfetto工具选型与基本概念:先花10分钟搞懂再上手
3.1 三种典型用法:从“快速看症状”到“精确做手术”
如果你刚接触Perfetto,不建议直接上命令行,先走最简单的路径:开发者选项里的“系统跟踪”功能。具体路径是:设置-开发者选项-系统跟踪,开启后系统会录制一段时间,录完生成一个.perfetto-trace文件。这个功能录制的数据源覆盖了CPU调度、内存、显示等常见模块,足够完成初步诊断。
如果你想针对特定路径做精确复现,那就用命令行的perfetto工具。它能自定义数据源、buffer大小和录制时长,适合固定场景。
更进阶的用法是用trace_processor做SQL查询,对所有trace数据进行结构化分析。它可以回答“这个线程总共跑了多久”这类聚和性问题,还能配合脚本批量处理多个trace文件。三种用法是层层递进的关系:先用系统跟踪快速定位你要看的大方向,再用命令行做精确采集,最后用SQL把关键数据量化成证据。
3.2 核心概念:Track、Slice、Packet、Thread
Perfetto界面本质上是一个“多轨时间线”。
- Track:一条条横轴轨道,可以理解成“一路机位”。每个进程、每个线程、每个CPU核心都有自己的track。
- Slice:轨道上一段段彩色矩形块,代表某个函数或事件的持续区间。比如主线程执行某个方法的耗时,就画成一个slice。
- Packet:Trace文件中携带的一条条原始数据记录。包括sched_switch(线程切换)、cpu_frequency(CPU频率变化)等。
- 线程状态:分为Running(在CPU上跑)、Runnable(可运行但正在排队)、Sleeping(阻塞或休眠)、Uninterruptible Sleep(不可中断的眠,经常对应磁盘IO)。
第一次打开trace难免眼花缭乱,我的建议是压根不要管全貌,先找到目标线程自己的track,把其他轨折叠起来,只看这一个线程在时间维度上的状态变化。
3.3 为什么选Perfetto而不是老式systrace或atrace
老项目里可能会有systrace,这时候有同学会问,systrace继续用不行吗?systrace的底层数据采集也依赖ftrace,和Perfetto在很多基础事件上同源,但Perfetto在几个重要方向上是完全碾压式的:
- 采集端的配置能力,可以精确控制数据源、buffer大小、丢事件策略;
- 分析端支持SQL查询,可以进行程序化分析而不是肉眼盯图;
- 自带强大的UI,支持轨迹缩放、搜索、对比甚至collision检查;
- 对现代多核CPU、多进程、GPU等复杂场景支持更好。
如果你已经熟悉systrace的工作方式,转Perfetto成本很低,因为很多术语是相通的。时间线思维和调度分析的方向是完全一样的。
4. 实操全流程:用Perfetto完整定位一次ANR
这一章我用一次实操来串讲。假设场景:某App有一个页面打开偶发ANR,用户反馈“有时候点进去就卡死”,logcat显示的Reason是“Input dispatching timed out”,但ANR文本栈看起来正常。我们怀疑是调度或锁竞争,但现在用Perfetto把它查清楚。
4.1 环境准备与数据源配置
先用命令行采集。连接设备后,确认perfetto可用:
adb shell perfetto --version如果设备系统版本较新,自带perfetto基本都能用。接着准备一份精确的采集配置,比如下面的pbtx文本:
buffers { size_kb: 262144 fill_policy: RING_BUFFER } data_sources { config { name: "linux.ftrace" ftrace_config { ftrace_events: "sched/sched_switch" ftrace_events: "sched/sched_wakeup" ftrace_events: "sched/sched_blocked_reason" ftrace_events: "task/task_newtask" ftrace_events: "power/cpu_frequency" ftrace_events: "power/cpu_idle" ftrace_events: "irq/irq_handler_entry" ftrace_events: "irq/irq_handler_exit" } } } data_sources { config { name: "linux.process_stats" } } data_sources { config { name: "android.log" android_log_config { log_ids: LID_DEFAULT } } } data_sources { config { name: "linux.sys_stats" sys_stats_config { meminfo_period_ms: 1000 } } }这段配置我解释一下:buffer大小设成256MB,用环形缓冲,录制时间到了之后保留最后一段数据;ftrace事件里重点要了sched_switch、sched_wakeup和sched_blocked_reason,这是还原线程状态切换的关键;android.log可以把Logcat日志一并写到trace里,方便后续把业务日志和调度时间线对齐。
录制指令采用后台方式:
adb shell perfetto -o /data/misc/perfetto-traces/xxx.perfetto-trace \ -c /data/misc/perfetto-traces/config.pbtx \ -t 30s这里有个细节:-t 30s表示录制30秒,先在后台启动录制,再开始复现操作,顺序非常重要。如果你的操作脚本需要打桩或者预热,建议在录制启动前就先完成,不然前面几十秒全是无效空档。
4.2 触发一次ANR并观察现场
启动录制后,立刻去操作页面,建议用脚本做连续点击或刷新,把触发概率抬到100%。如果运气好一次就触发,可以等ANR对话框出现后再等2~3秒,确保系统把ANR的栈信息dump完毕,再停止录制:
adb shell perfetto --stop把产物拉到本地:
adb pull /data/misc/perfetto-traces/xxx.perfetto-trace .如果是在线上环境,很多时候拿不到这种主动录制的trace。但系统会在ANR发生时自动生成一份perfetto trace,位置可能由于版本不同而分布在不同目录。你可以先去/data/anr/目录找找,有些系统会把trace以.trace或.perfetto-trace形式保留。线下定位阶段,主动录制仍然是效率最高的方式。
4.3 打开trace,四个视角排优先级
把trace拖进官方Perfetto UI后,第一件事不是乱翻,而是从下面四个视角一层层看。
第一个视角:“进程CPU总览”。看可疑App进程在整个录制时间窗里,CPU占用是偏高还是偏低。如果全场只有5%的CPU,说明主线程基本在高频等待;如果CPU占用在ANR前后冲到90%以上,就要怀疑是不是CPU被打满导致排队。
第二个视角:“主线程状态流”。找到主线程的track,缩小时间窗到ANR前5秒,看主线程的颜色状态。Perfetto里Running是绿色,Runnable是蓝色,Sleeping是白色。为什么建议看这里?因为只有这里能区分“卡在锁”和“排队没上CPU”这两种完全不同的故障模式。
第三个视角:“关键Slice耗时”。如果主线程在运行,看它执行了哪个slice、要多久。比如某种资源加载的slice从50ms变成2000ms,说明是在业务代码里被拖住,需要继续看它内部有没有更细的sleep。
第四个视角:“CPU调度真相”。把目标App所有线程的track都展开,查看CPU的sched_switch事件,找出是谁抢占了主线程、或者主线程等待的某个锁持有线程一直没有运行的原因。这一步能定位到跨线程锁竞争。
4.4 用SQL把结论变成硬证据
肉眼看完时间线之后,建议用trace_processor把关键指标提取出来,做数值上的确认。
trace_processor --query "SELECT utid, state, SUM(dur) AS total_dur FROM thread_state WHERE state != 'R' AND utid = 123 GROUP BY state"上面这个SQL的意思可以理解为:把tid为123这个线程,按线程状态分组,算它每个状态各花了多长时间。如果Running的时间占比只有20%,而Runnable占到40%,那么主线程“排队没上CPU”的概率就非常大。再配合一段SQL找到主线程最后一次唤醒到实际运行之间的间隔:
SELECT ts, name FROM sched WHERE utid = 123 ORDER BY ts DESC LIMIT 20如果发现主线程在好几毫秒后才被调度执行,那就说明调度器响应慢了,通常是系统负载过高。这些查询结果保留下来,可以和业务同学、系统开发同学对线,比张口就说“感觉卡了”有说服力得多。
4.5 从trace到修复方案的三级决策
看完数据,修复方向大致分三级。
一级是业务层修复:如果结论是主线程本身执行了耗时操作,比如文件读写、复杂计算,那就把操作拆到子线程,或者加缓存。二级是并发层修复:如果结论是跨线程锁竞争,比如主线程在等某个工作线程释放锁,而该工作线程又被另一个线程阻塞,那么优化方向是对锁的顺序、粒度做调整。三级是系统层问题:如果结论是CPU没空、频率太低、核心被占用,那要做的往往是错峰降载、压低某些后台任务的优先级,甚至调整系统端的CPU配置。
真实情况往往是混合的——既有业务耗时,又有调度排队。这时不要贪快,拿Perfetto证据里最占比重的那一条主线先去改,改完再复测一次流程,你会发现决策效能完全不一样。
5. 常见问题与排查技巧实录:这些坑我替你踩过了
5.1 问题与定位速查表
| 现象 | 第一时间看的Track | 常见根因 | 确认方法 |
|---|---|---|---|
| 主线程状态大部分是白色Sleeping | 主线程track | 等锁、等Binder、等IO | 看等待唤醒的事件来源 |
| 主线程蓝色Runnable频繁 | CPU核心track、其他线程track | 系统负载高、被抢占 | 看sched_switch中的prev_comm |
| 主线程绿色Running但slice特别长 | CPU频率track、主线程slice | 降频、代码执行慢、JIT | 对比前后频率变化 |
| 子线程持有锁但长时间没运行 | 子线程track、CPU负载 | 子线程优先级低、被压榨 | 看子线程整体运行时间占比 |
| ANR前后CPU占用全程偏低 | 系统进程track | 进程被冻结、后台限制 | 看进程的freeze状态 |
这个速查表不是万能钥匙,但它能帮你在打开trace后先走一遍最稳妥的排查顺序。实际执行时,我会建议先看“主线程的Runnable状态比例”这个核心指标,它最能区分两类最不同的根因。
5.2 避坑经验和独家心得
第一个坑是buffer开得太大或太小。太小导致关键事件被丢掉,尤其是ftrace事件密集时,ANR前后那段最关键的调度数据反而被环形缓冲覆盖了。我一般的建议是,起手256MB,临时录制场景小到64MB也够,但如果录到系统级trace,不要在同一份trace里同时录太多数据源,否则大文件打开直接卡爆内存。
第二个坑是过度依赖Up解“线程长时间没Running就等于卡死”的判断。线程状态需要和CPU频率、系统进程负载联合起来看。比如同一个线程在低负载状态下跑得好好的,但系统一旦高负载,它就永远排不上队,这种根因完全在系统侧,不应只优化业务代码。
第三个坑是本来要复现问题,结果因为操作太快或太慢,导致问题堆叠。举个例子,连续点击导致主线程消息队列里堆积了二三十个事件,ANR前5秒的trace里全是待处理的消息,这看起来是“主线程慢”,实际上是你复现时的操作手法放大了干扰。所以操作手法要做到既有节奏感,又能精确触发,不能狂点到底。
第四个坑是拿线上自动生成的trace直接跟线下对比时说“对不上”。线上环境的数据源、路径、调度器配置可能都有差异。我会建议先在测试环境里用同一套配置跑到稳定复现,再回到线上抽查,不要一把梭地只看线上trace。
5.3 线上ANR怎么办:没有主动录制也能用Perfetto
很多人问,线上用户设备不可能提前录制,怎么办?其实Android系统在ANR发生时可能自动保存了一份perfetto trace。部分版本会把trace文件放到/data/anr/;另外,系统日志里还可能有“ANR in ... trigger”与其他崩溃信息一起被传回后台。你可以通过以下命令尝试拉取:
adb shell ls -l /data/anr/ adb shell ls -l /data/misc/perfetto-traces/拉回来后,同样用Perfetto UI加载。这些线上trace通常默认只含调度相关的数据,没有业务slice,所以能和代码打点关联起来的点不多。但依然可以看两个指标:主线程的调度延迟和系统负载。如果发现主线程Runnable占比极高,说明线上环境CPU竞争压力确实大,这个结论已经足够驱动优化了。
遇到一次疑难ANR,我个人的习惯是:先把线上自动化trace的数据拿来做宏观判断,把问题定性成“调度问题”或“业务问题”,再决定要不要线下去精确录制。
6. 写在最后:想清楚“要验证什么”再动手录trace
回到“需要Perfetto分析的ANR”这个命题上,我的经验是,Perfetto的适用场景非常明确:它就是给你回答“主线程为什么慢、为什么没运行、为什么运行了还在等”的。但它的门槛也在这里——它给的是高密度底层信息,你要么有清晰的问题,要么会被淹没在数据里。
我在某次排查里,本来想验证“子线程持锁时间太长”,结果Perfetto一跑,发现子线程实际运行时间很少,是它根本没被调度。这个转折完全依靠Perfetto的调度数据才能完成验证,后者用更少时间压到同样的结论,却少走了一半弯路。
给刚上手的同学一个建议:别急着录大trace,先把一条主线程的状态流看顺。能看出Running、Runnable、Sleeping的区别,能看懂sched_switch和wakeup事件,再开始谈复杂排查。Perfetto的核心心法是“先定性,再定量”,先确认方向对不对,再把所有证据拉出来,最后才动手改代码。
如果你现在手里正有一个查不清楚的ANR,别再去猜了。打开Perfetto,录一份30秒的trace,从主线程的状态开始看。很多时候,真相就在你眼前挂着。