那段时间我们为某个线上 ANR 单子折腾了很久,崩溃堆栈里没有明显异常,主线程栈也看不出什么致命调用,可用户侧就是频繁弹出“XX 无响应”。后来把当时抓到的 traces 拉到 Perfetto 里做时间轴分析,才明白问题根本不在主线程代码本身,而是它一直在等待一个迟迟拿不到锁的异步线程,再往深处看,又牵扯到 CPU 调度和 binder 调用节奏。从那次之后,我对“ANR 到底要怎么定位”这套思路彻底换了打法:不是所有 ANR 都能靠崩溃栈解决,凡是要看时序、看等待、看调度竞争的 ANR,基本都得切成 Perfetto 分析。
这篇文章就是把这类“需要 Perfetto 分析的 ANR”梳理成体系。聊清楚系统判定 ANR 的机制,什么场景传统手段看不透,怎么抓一条合格的 trace,以及拿到 trace 后按什么顺序读、怎么还原现场,最后再补充几个我实际踩过的坑。适合正在做 Android 性能优化、做稳定性治理,或者只是被线上 ANR 逼到想换工具的开发者参考。
1. 先还清概念:ANR 是“超时”,不是“崩溃”
正式讲 Perfetto 之前,最好先把 ANR 的判定机制过一遍。我见过不少开发者把 ANR 当成异常捕获的延伸去排查,结果方向完全反了。
1.1 系统怎么判断“你没响应”
Android 的 ANR 全称是 Application Not Responding,本质是一套超时机制。系统在几个关键入口点了“计时器”,看你是否在规定时间内完成某个动作,没完成就判定 ANR。这套机制和 JVM 的异常是两回事,它不靠抛异常来通知你,而是靠 ActivityManagerService 来回查“你到底干完了没有”。
常见的有四类超时窗口:
- 输入事件分发超时,也就是 InputDispatchingTimeout,通常是 5 秒。你点了屏幕,系统把事件送到应用主线程,主线程在规定时间内既没处理完,也没再次派发,就触发输入型 ANR。
- 广播超时,BroadcastTimeout,前台广播大概 10 秒,后台广播 60 秒,具体还和队列类型有关。onReceive 里做重活很容易踩这条线。
- 服务超时,ServiceTimeout,前台服务 20 秒,后台 200 秒,主要看 onCreate、onStartCommand 这些回调是否超时。
- 内容提供者超时,ContentProviderTimeout,这类相对少见,但一出现往往就和冷启动阶段 Provider 初始化耗时有关。
这几个超时类型有一个共同点:最终都落在主线程上。系统不会因为一个普通后台线程死循环就弹 ANR,除非它导致主线程无法及时处理关键消息,或者系统层面的进程通信迟迟得不到响应。
所以很多新手会把“ANR 修复思路”理解为“别在主线程做耗时操作”,这句话没有错,但它只覆盖了一半场景。我用 Perfetto 分析的 ANR 里,有一大半主线程本身没做多少耗时事,而是被调度、锁、binder、GC 这些外力按住了,主线程栈干干净净,但系统层面早就超时了。
1.2 只看主线程栈会漏掉什么
传统定位 ANR 是看系统落盘的主线程调用栈,如果能抓到的话,它确实告诉你主线程当时“停在哪”。但问题在于,它给不了你“怎么停在那的”信息。
举个例子,主线程栈停在 Object.wait 上,看起来是在等某个锁。接下来你自然要问:这个锁被谁持有?对方线程当时在运行还是被阻塞?它在等 CPU 吗?还是它也被另一把锁卡死了?这些问题靠一张静态栈是回答不了的。时间轴工具才能真正回答——哪个线程从什么时刻开始持锁,哪个线程在什么时间段内处于 Runnable 状态却没被执行,是不是 CPU 满负荷导致它抢不到核。
另外还有一类尤其隐蔽的 ANR:主线程状态是 Running,栈里的方法也没问题,但它执行了太长时间,或者说它执行过程中被高优先级任务反复打断,完成输入事件分发已经超过 5 秒。这种“主线程一直有事做,但节奏被拖垮”的场景,完全没有静态栈可以告诉你真正原因。
2. 为什么传统定位看不透,非得拉到 Perfetto
按说系统已经会在 ANR 发生时生成 traces 文件,把主线程和部分活跃线程的调用栈落盘到 /data/anr/ 目录,那为什么还要专门上 Perfetto?
2.1 /data/anr/ 这层最多能告诉你“主线程在哪”
实际生产的 ANR traces 文件能不能读、值不值得读,存在很多变数。
第一,用户设备没有 root,很多厂商还会限制 /data/anr/ 的导出权限。你拿到一份 DropBox 里的系统报告,可能只有简单文本,而且捕获过程本身会再次打断现场,丢失关键上下文。第二,traces 文件是快照,它只反映某个瞬间的线程停靠点,看不到线程状态变化过程。第三,我见过不少 traced 文件的主线程栈停在标准库方法上,比如 MessageQueue.next 或者 Looper.loop,看起来“什么都没干”,实际上问题出在别的线程或系统服务里,这种栈对定位几乎没有帮助。
不是说 traces 完全没用。它适合快速确认主线程卡在哪个业务模块,适合拿到第一手“案发文件名”。但一旦你发现栈顶是系统框架方法、锁等待、binder 调用,或者主线程明明在跑却肉眼看不出来问题,就该立刻转 Perfetto。
2.2 Perfetto 能还原哪些传统手段看不到的现场
Perfetto 是一套跨版本、跨组件的数据采集和可视化方案,在 Android 场景下可以把内核调度信息、CPU 频率、binder 事务、系统服务调用、应用线程栈、电源状态等采集到同一条时间轴上。它核心价值可以总结成三个词:时序、关联、量化。
时序上,你能看到 ANR 触发窗口内每一毫秒发生的事件。主线程在 10:00:01.200 被唤醒,10:00:01.350 进入可运行状态,但实际拿到 CPU 跑到 10:00:01.820,这中间的 470 毫秒就是调度延迟,静态栈完全看不出来。
关联上,你可以把应用线程栈、CPU 调度切片、binder 事务、锁等待放在一条时间线上对齐看。主线程为什么没拿到锁?看持锁线程的颜色、优先级、运行状态,可能 200 毫秒前它被一个系统进程打断,也可能它自己在等待另一个 binder 回调。
量化上,Perfetto 的 UI 里可以直接拖选区看某个线程的状态占比。Runnable 状态多久,Sleeping 多久,Uninterruptible Sleep 多久,还能算 CPU 空闲率、各线程 CPU 占用,这些是判断“主线程被抢”还是“主线程在等锁”的关键证据。
2.3 什么时候必须直接用 Perfetto
以我的经验,出现下面几种迹象之一,就不必依赖 ANR 快照了,直接开 Perfetto:
- 主线程栈停在 Object.wait、LockSupport.park、epollWait、binderTransaction 这类系统调用上。
- 主线程栈是 Runnable 状态,但看不出它在跑什么业务,或者说它一直在跑但响应超时。
- 怀疑是 CPU 抢占、温度限频、GC 卡顿导致的主线程变慢。
- 需要排查 binder 调用是否拖长了系统服务的响应时间。
- 需要对比 ANR 发生前后几秒的调度行为,确认是不是偶发资源竞争。
项目里经常遇到的情况是,组员花了两小时看 traces 文本,最后结论是“主线程卡在等待某个 response 回调”,但这个回调到底是系统客户端 SDK 内部逻辑,还是某个异步线程被压住没及时干活,文本根本给不出来。这类问题拿 Perfetto 看,往往几分钟就能把因果链理顺。
3. 抓一张合格 ANR 现场:从设备设置到触发命令
Perfetto 好用的前提是能抓到完整的 trace。很多同学不是不会分析,而是抓到的数据残缺不全,要么没开线程调度,要么 buffer 太小把 ANR 窗口冲掉了,要么抓了调度却没带栈。
3.1 开发者选项里的系统追踪
在日常调试机上,最简单的方式是开开发者选项里的“系统追踪”。入口一般在“开发者选项 - 系统追踪”,不同 Android 版本叫法略有区别。开启后,你可以选择要采集的数据类别,同时设定缓冲区大小和时长上限。
每次调试的时候,我通常会勾选这些核心类别:sched、freq、idle、binder_driver、binder_lock、gfx、view、input、am、activity、sysui、network、disk、ion 等。如果锚定 ANR,sched 和 binder_driver 是必须的,它们决定了你能不能看到线程调度和内核级 binder 调用。freq 和 idle 用来识别 CPU 限频问题。view 和 input 用于分析输入事件分发链路。
不过开发者选项里的“开始录制”是手动点按的,还有一个隐藏功能是长按电源菜单里的“BugReport”或“日志记录器”,但那些生成的是普通系统报告,不一定包含 Perfetto 的完整调度数据。最可控的方式还是命令行。
3.2 用命令行控制抓取范围
命令行方式可以精确控制时长、buffer 和事件类别,也方便自动化复现。最基础的一条命令像这样:
adb shell perfetto -o /data/local/tmp/anr_trace.perfetto-trace -t 30s \ sched freq idle binder_driver binder_lock am input view \ --app 你的包名这条命令的含义是:采集 30 秒,输出到 /data/local/tmp/ 下面,后台抓取完成后自动结束。--app 你的包名会在采集期间自动为该进程启用应用的额外跟踪数据源,也就是 Java 栈和 native 栈。
但要注意,-t 30s是总时长,从命令执行那一刻就开始计时。更常见的是先用一个配置文件来控制什么时候开始结束,或者配合一段自动化脚本:先启动追踪,然后执行复现脚本,最后再停止追踪。
配置文件方式一般这样写:
duration_ms: 30000 buffers { size_kb: 65536 fill_policy: DISCARD } data_sources { config { name: "linux.process_stats" target_buffer: 0 } } data_sources { config { name: "android.surfaceflinger.frametimeline" target_buffer: 0 } }实际项目上我是用一个 Python 脚本驱动的,把“开启追踪、sleep 2 秒、执行复现路径、再 sleep 2 秒、关闭追踪、拉取 file”串起来。脚本的作用不是省那几步 adb 命令,而是保证每次抓取都在相同节奏下进行,方便多轮对比。
提示:如果只关心 ANR 那几秒,建议把 buffer 设大而不是把时长设短。ANR 现场往往需要前后各保留几秒才看得清因果,buffer 太小会导致 trace 循环覆盖,案发时刻恰恰没留下来。
3.3 复现的节奏和关键注意点
抓 ANR 现场比抓普通性能问题更讲究“预埋”。ANR 是超时,不是崩溃,你不可能等它发生时再去拉 trace。所以我的做法是:
- 先把设备状态调整干净。重启一次,关闭后台推送,关闭可能干扰 CPU 的动画和同步任务。
- 先做一次小规模预演,确认 ANR 在什么操作后、大约多少秒出现,记录下来。
- 开启系统追踪后,从预演路径的入口开始操作,而不是从操作中途开始。
- 在 ANR 对话框弹出后,再等待 2 到 3 秒再停止追踪,把弹窗阶段的系统活动也收进来。
- 如果复现概率不高,就加长 duration,或者开启“持续追踪”模式,用一段循环脚本反复触发。
此外,测试机上一定要关掉“显示 ANR 并提示用户”这类会影响时序的机制吗?不能完全关。但可以临时把 ANR 判定时间调成更宽松,保证 trace 有足够窗口捕获现场,比如某些厂商调试选项里可以设置“不显示 ANR 对话框”,让系统直接发生 ANR 而不弹窗,这样 trace 会以纯后台方式记录,少掉弹窗本身带来的干扰。如果设备没有这个选项,就在弹窗出现时立即停止抓取,然后从 trace 里找对应时段的 ActivityManager 日志。
还有一个小细节:抓 trace 的过程中,尽量保证设备不被充电唤醒,也没有别的进程在跑重负载任务。否则你的 trace 会混入大量无关调度信息,且 CPU 竞争模式会失真。
4. 拿到 trace 后应该怎样看,而不是瞎滑
抓完一条完整 trace,很多人会犯同一个毛病:直接搜主线程 utid,然后顺着时间轴拖来拖去,看到一堆线程状态切片就头大。这里我建议按固定顺序去读,效率会高很多。
4.1 第一眼:左侧线程状态总览
Perfetto UI 打开 trace 后,左侧会列出所有进程和线程。先别急着找主线程,先看一眼全局状态。把时间范围拖到 ANR 出现前后那几秒,观察:
- 系统 CPU 整体利用率是多少?
- 有没有哪条线程长时间占据大核?
- 全局有没有大量线程处于 Runnable 但执行时间很少的拥挤状态?
这步是建立背景。如果全局 CPU 利用率接近 100%,那后面分析主线程时,重点就是“资源竞争”;如果全局很空闲,但主线程依然睡很久才被唤醒,那重点就是“唤醒机制”或“锁等待”。
4.2 找主线程并看它的调度切片
在 Process 信息里找到你的包名,主线程通常叫 “main” 或者以 “tid 1” 形式出现。点进去后,可以看到一段彩色条带,每种颜色代表一种线程状态:
- 绿色表示 Running,正在核上执行;
- 蓝色代表 Runnable,想跑但没核可跑;
- 灰色是 Sleeping,一般是主动 sleep、wait、epoll 等;
- 红色是 Uninterruptible Sleep,通常是内核里等 IO 或等锁,这类最值得警惕。
ANR 窗口里,你要重点区分主线程到底是“久久没有被唤醒”还是“被唤醒后一直拿不到 CPU”。前者看灰色段和蓝色段的比例,后者看主线程附近还有哪些线程在抢占 CPU。
注意,Perfetto 里线程状态颜色和传统 top 里的 R/S/D 不是一一对应,但语义差不多。我习惯先点击某一段,看详情里的实际状态字符串和 util 值,再决定下一步。
4.3 Runnable 被卡和 Running 耗时长的判断区别
这是最容易困惑的地方。主线程长时间处于 Runnable 但执行时间很短,说明系统调度层有问题。常见原因有:
- 高优先级线程持续霸占某一个核;
- 设备限频导致所有核都处于低主频,任务排队;
- 同进程多个线程竞争,导致主线程迟迟分不到时间片。 这个时候看 Running 切片本身没有意义,得看旁边其他线程的调度记录,尤其是那些高优先级、长时间持锁的线程。
主线程长时间 Running 的情况则又不同。它确实一直在执行代码,但输入分发还是超时,说明某个方法内部执行时间过长,比如说在缓存热路径里做了一堆 IO,或者一次性处理太多数据。这时候你在线程栈里能看到长时间停留的某个方法,再结合调用频率来定位。
4.4 用 Track 图看 binder 和系统服务等待
Perfetto 在抓 binder_driver 和 binder_lock 数据之后,会给 binder 事务单独生成 track。你可以看到每个 binder 调用的方向、延迟、目标服务。排查 ANR 的时候,我经常这么问:
- 主线程这次 binder 调用等了多久?
- 它调的服务是不是出现了明显排队?
- 是否有跨进程递归调用,导致对方也在等主线程?
这些信息在静态 traces 里完全看不到。理论上你只知道主线程栈停在 binder 相关帧,但从 Perfetto 的 track 上能轻松看出,比如主线程向 activity 服务发了一个 binder 事务,对方花了 800 毫秒才回复,而这 800 毫秒里对方也在等待另一个像素合成服务的返回。这种系统服务之间互相等待的长链路,往往是多窗口切换和小内存设备上的 ANR 元凶。
注意:binder track 的数据量很大,容易撑爆 buffer。如果空间紧张,优先保 sched 和栈数据,binder 数据可以降级使用更高 level 的 category 或者单独抓一次。
5. 几类让我绕远路的 ANR 现场复盘
下面这几类 ANR 是我在项目里真实分析过的典型,全部脱敏之后整理出来,方法和思路可以直接复用。
5.1 主线程等待锁,锁的持有者偏要在大核上排队
当时某个模拟项目里出现点击按钮后偶发无响应,复现概率不高。Perfetto 抓到现场后,主线程栈停在某个 SDK 内部的锁等待上,代码路径看起来是等待某个异步初始化任务完成。但让我惊讶的是,持有那把锁的线程本身并不是主线程,它是一个后台初始化线程,状态长期为 Runnable,却一直在小核上排队,几乎没有得到执行。
原因很快水落石出:这个后台线程被设成了较低优先级,同时该设备上另一个高优先级进程持续占用系统大核,导致这个小线程根本无法及时执行。于是一环套一环:初始化线程执行不了,锁无法释放,主线程只能死等,最终 ANR。
这种场景修复方式不是去改主线程,也不是单纯加锁超时,而是要把初始化工作前置、放到空闲时间执行,或者调整线程优先级策略,避免在关键交互路径上依赖一个低优先级线程的即时完成。Perfetto 的价值在于把“锁持有者无法被调度”这一层原委彻底摊开。
5.2 线程活着,时间片却被高优先级任务抢走
还有一次 ANR 是开机后的首屏启动场景。主线程看起来一直在 Running,栈显示在加载首屏数据,但输入事件仍然超时。Perfetto 给出关键证据:主线程的实际 Running 时长只有很小一段,大量时间处于 Runnable,而另一个系统的媒体服务线程在同一时刻几乎独占了大核优先级。
主线程一直在被抢占。它不是没事干,而是总在“想跑”但“跑不动”。那个高优先级线程其实是另一个进程的媒体播放相关任务,按理说不应该长时间霸占 CPU。最后通过限制媒体服务在该场景下的运行时长,或者错开首屏渲染和系统扫描任务,问题就消失了。
如果没有 Perfetto 的调度对比,开发者大概率会把主线程栈里的数据加载方法抠出来优化,优化了也没用,因为问题不在那段代码。
5.3 CPU 高峰段导致主线程赶上垃圾回收
小内存设备上经常出现一类老生常谈的 ANR:主线程在 GC 期间被暂停过久。Perfetto 的 CPU track 上可以清楚看到某段时间主线程所在核的频率被拉低,而且出现连续几段“Uninterruptible Sleep”——这些往往是 GC 并发标记阶段导致的内存访问延迟放大。
这种场景,底层代码调用栈可能只是简单的集合遍历或对象创建,看起来毫无问题。但如果同一时间 App 整体内存分配速率过高,触发频繁 GC,主线程就会周期性停顿。Perfetto 能让你把内存分配曲线和 CPU 调度曲线对齐,从而判断“主线程停顿是不是 GC 周期性行为导致”,而不是在代码里盲目怀疑某个业务函数。
修复上,常见手段是减少主线程路径上的临时对象分配、把部分解析工作挪到子线程,或者在低内存设备上动态调低缓存容量。力度最大的做法甚至连关键路径的集合容器都换掉。
5.4 表面卡死实际上 binder 长链阻塞
还有一个案例非常有意思。主线程栈停在 binder 调用,看起来像卡在某种系统服务查询里。我拉了 binder_driver 的 track 才发现,主线程的 binder 请求发到了一个自带的快捷键服务组件,该组件又反向调用了另一个系统服务,而那个系统服务正好处在全局 binder 锁竞争激烈的时间段。于是主线程是在等另一个等待者返回,链路超长。
这类问题靠修改应用逻辑往往收效甚微,因为根因在其他系统组件和当前系统状态共同作用。实际修复方案变成了:减少在关键输入路径上的 binder 调用次数、尽量缓存系统服务反查结果,以及在全局 binder 竞争严重时降级功能。Perfetto 的贡献是把一条看不见的跨进程等待链变成了可见的 dispatcher 区间。
6. 用 Perfetto 排查 ANR 时的高频坑
工具用多了,多少都会踩到坑。下面几条是我觉得最值得分享的,希望你能绕开。
6.1 buffer 溢出:很多问题其实出在抓取配置
我第一次抓 ANR trace 时,采用的是长时长加小 buffer 的方案,结果复现成功但 trace 里根本找不到 ANR 那几秒。原因是 buffer 写满后默认是 DISCARD 模式,新数据会把旧数据冲掉,而 ANR 恰好发生在采集末期,前面的调度记录早就被覆盖了。
解决方案是两种:一是增大 buffer,同时把时长控制在必要范围内;二是改成持续追踪模式,设置一个环形 buffer,等 ANR 发生后手动停止,这样能保证案发时段完整保留。Perfetto 的 UI 里也可以直接看 buffer 占用率。
6.2 别只看栈,记得算 OFF-CPU 等待时间
很多分析新手会陷入一个误区:主线程栈停在某个方法,就觉得是这个方法执行慢。但 Peffetto 里真正要算的是——主线程在这个方法上停留期间,它的状态是 Running 还是 Runnable 还是 Sleeping。如果是 Sleeping 或 Uninterruptible Sleep,问题大概率不在方法实现本身,而在“谁没及时唤醒它”。
我的习惯是把主线程时间选中,看状态占比。如果 Runnable 占比超过 30%,优先查调度竞争和优先级;如果 Sleeping 占比很高,优先查锁、IO、binder 和 epoll 等待源。盲目优化方法实现,经常白费力气。
6.3 注意 logcat 时间轴和 Perfetto 时间轴的对齐问题
Perfetto 采集的数据来自系统 trace 数据源,它的时间和 logcat 打印日志用的时间戳可能存在偏差,尤其在不同版本和不同设备上。你在 logcat 里看到的 ANR 消息发生在 10:00:05,顺手就去 Perfetto 里找 10:00:05 附近的数据,结果发现主线程已经空闲好久了,这常常是时间对齐问题造成的假象。
建议不要依赖 logcat 的具体时间点,而是拿一个明确的系统事件片段来对齐,比如某个 Activity 切换、某个关键 binder 事务调用,或者 trace 里自带的 marker。这样跨时间轴比对时才不会张冠李戴。
6.4 复现时尽量把常用数据源一起拉上
很多人为了减小 trace 体积,只开 sched,结果定位到锁竞争时没有锁名,定位到 binder 过长时没有事务详情。我实际项目里的经验是,ANR 分析的 trace 至少要带:sched、freq、binder_driver、binder_lock 和栈数据。体积确实会大,但分析时不用来回补抓。
如果担心体积过大,可以先抓完整数据定位到可疑窗口,再针对可疑窗口抓一次精简但仍然包含关键数据源的 trace。Perfetto 支持只保存局部范围,也可以用工具裁剪,正式分析时不至于被无关数据淹没。
我个人现在做 ANR 治理,默认流程已经固定成:先通过稳定性平台粗判进程和主线程,再针对高危场景预埋 Perfetto 抓取,最后把所有现场信息汇总到一张时间轴里还原。这套流程帮我在不少疑难问题上少走了弯路,也希望这篇内容能帮你早点告别对着 /data/anr/ 文本文件苦思冥想的日子。