资讯动态

Perfetto 实战:从 ANR 超时机制到调度分析定位方法

发布时间:2026/10/13 11:36:38 来源:尧图企业网站定制
那段时间我们为某个线上 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/ 目录那为什么还要专门上 Perfetto2.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 等。如果锚定 ANRsched 和 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/ 文本文件苦思冥想的日子。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑