Android ANR 到底怎么定位?从触发原理到日志判断的完整方法

线上分析 ANR 时,经常会遇到一份看起来互相矛盾的日志:系统明确记录了 Input dispatching timed out,但 Trace 里的主线程却停在 nativePollOnce,像是在安静地等待下一条消息。

如果只看主线程当前栈,很容易得出"应用没有卡住"的结论;如果只看某个醒目的函数名,又容易把采样时碰巧执行的代码当成根因。我后来把这类现场固定拆成三个连续问题:系统当时在等什么,Trace 拍到的是哪个时刻,现有证据能不能覆盖整个超时窗口。

先从 Reason 确认系统等待的对象,再对齐 ANR 判定与 Trace 采集时间,最后沿主线程、锁、Binder、窗口和系统资源补齐因果链。

一、先别急着看栈:系统当时在等什么

ANR 不是"主线程卡了 5 秒"的同义词。Android 会为不同的系统交互建立不同的响应约束,输入事件、Service 生命周期、前台服务启动、广播和 JobService 的监控者与完成条件都不相同。这些故障可能进入 ANR 处理,也可能以版本特定异常终止应用;排查入口必须由 Reason 或异常类型决定。

触发场景 系统在等待什么 第一条证据
输入调度超时 目标窗口建立、接收并完成输入分发 Input dispatching timed out 后面的具体原因
Service 执行超时 onCreate()onStartCommand()onBind() 等执行完成回执 executing service、ServiceRecord 与主线程调用链
前台服务未及时前台化(结果随版本变化) startForegroundService() 后及时进入前台并展示通知 ForegroundServiceDidNotStartInTimeException、系统版本与 startForeground() 调用时间
广播超时 onReceive() 返回,或 goAsync() 对应的 PendingResult 完成 Receiver 时间线与 finish()
JobService 超时 onStartJob()/onStopJob() 返回及相应通知要求 Android 版本、目标 SDK 与 JobService 回调

输入事件 5 秒是官方文档明确写出的典型超时阈值。前台服务也必须在调用 startForegroundService() 后几秒内通过 startForeground() 进入前台,但失败结果存在版本差异:早期 AOSP 实现会走 ANR,当前官方排障文档则以 ForegroundServiceDidNotStartInTimeException 说明这类失败。Service、广播和 JobService 的期限也会随 Android 版本、前后台状态、目标 SDK 及厂商实现变化。因此,固定超时表只能帮助建立概念,不能替代当次 Reason、异常类型和对应版本实现。

同样是输入 ANR,does not have a focused window 与"窗口已有焦点但事件处理不完"就是两条不同路径。前者意味着 InputDispatcher 一直等不到合法输入目标,主线程即使在采样时空闲也不矛盾;后者才需要优先检查事件消费、长消息、锁、Binder 或绘制。

二、跟着一次 Service 启动,看超时怎样成立

理解 ANR 最有效的方法,不是先背超时时间,而是跟着一次受监控操作走完"登记、调度、执行、回执"四步。以 Service 创建为例,应用调用 startService() 后,真正的超时监控与生命周期调度跨越了应用进程、system_server、Binder 和应用主线程。

text 复制代码
应用进程
ContextImpl.startServiceCommon()
  -> ActivityManager.getService().startService(...)

system_server
ActivityManagerService.startService()
  -> ActiveServices.startServiceLocked()
  -> realStartServiceLocked()
  -> bumpServiceExecutingLocked()       // 登记正在执行并安排超时检查
  -> scheduleCreateService()            // Binder 调度到应用进程

应用主线程
ActivityThread.handleCreateService()
  -> Service.onCreate()
  -> IActivityManager.serviceDoneExecuting(...)

system_server
serviceDoneExecutingLocked()
  -> 执行嵌套归零并移出 executingServices
  -> 进程中无待完成 Service 时取消超时检查

未按时完成
serviceTimeout(...)
  -> 进入 ANR 采集与处理链

第一步是登记执行。 bumpServiceExecutingLocked() 不只是给某个 onCreate() 单独挂一个定时器。系统会维护 Service 的执行嵌套以及进程内正在执行的 Service 集合。创建、启动、绑定和销毁都可能改变执行状态。

第二步是跨进程调度。 scheduleCreateService() 通过 Binder 把工作交给应用进程,随后由 ActivityThread 把生命周期任务放到主线程执行。此时,排在它前面的主线程消息、同步 Binder、类加载、锁竞争或日志阻塞,都可能推迟真正的回调时间。

第三步是完成回执。 应用执行完生命周期后调用 serviceDoneExecuting()。只有对应执行真正结束,嵌套计数和执行集合才会收敛;进程内没有其他待完成 Service 时,系统才取消这条超时监控。

java 复制代码
void beginExecuting(ServiceRecord r) {
    r.executeNesting++;
    r.app.executingServices.add(r);
    scheduleServiceTimeoutLocked(r.app);
}

void doneExecuting(ServiceRecord r) {
    if (--r.executeNesting == 0) {
        r.app.executingServices.remove(r);
    }
    if (r.app.executingServices.isEmpty()) {
        cancelServiceTimeoutLocked(r.app);
    }
}

这段简化逻辑不对应某一 Android 版本的完整源码,但保留了定位时最重要的状态关系。Service ANR 不能被简化成"某个生命周期函数自身执行过久"。需要同时确认监控何时开始、Binder 调度是否到达、主线程何时真正执行、完成回执何时返回,以及同一进程中是否还有其他未完成的 Service。

三、系统已经判定超时,Trace 为什么还能拍到空闲主线程

现在再回到开头的矛盾现场。ANR 条件成立与线程 Trace 完成采集并不是同一个时刻,中间还要经过调度、信号、线程挂起、堆栈生成以及平台整理。

text 复制代码
T0:真正的阻塞、无焦点窗口或系统异常开始
T1:达到系统响应期限,ANR 条件成立
T2:system_server 安排并完成线程 Trace 采集
T3:系统或第三方平台整理、压缩、上报

真正需要解释的是 T0 - T1
单份线程栈主要描述的是 T2

如果阻塞在 T1 之后解除,T2 就可能只拍到主线程重新进入 nativePollOnce。如果监控框架包裹了每次消息分发,采样也可能刚好停在它的公共入口。反过来,即使 T2 的栈顶出现业务函数,也必须确认它不是超时成立后刚开始执行的新消息。

因此,判断 Trace 是否拍中现场,至少要对齐 ANR Reason 的时间、CPU 统计区间、Trace 时间戳和最终上报时间。再把 Activity 生命周期、窗口焦点、消息 dispatch、Binder、GC 和业务日志放到同一时间轴上。时间对不上时,结论必须降级,而不是继续从单点栈里寻找确定答案。

四、拿到一份 ANR 日志,按这个顺序往下查

第一步:固定触发事实和时间。 保存完整 Reason、进程、组件、Android 版本、目标 SDK、系统 uptime、ANR 判定时间和 Trace 时间。第三方平台生成的 ANR_EXCEPTION 只能作为入口,不能替代系统 Reason。

第二步:读主线程头,但不要只读 Java 栈。 线程头同时包含 ART 状态、Linux 调度状态、系统线程 ID、累计运行与排队时间等信息。它们的时间口径不同,必须先读懂再使用。

text 复制代码
"main" prio=5 tid=1 Runnable
  | group="main" sCount=0 dsCount=0 flags=0
  | sysTid=3309 nice=-10 cgrp=default sched=0/0
  | state=R schedstat=( 48552284396 1834170006 63513 )
  | utm=3596 stm=1258 core=4 HZ=100
  | held mutexes= "mutator lock"(shared held)
字段 它能说明什么 不能怎样使用
Runnable / Native / Waiting ART 视角的线程状态 不能仅凭状态名确认正在耗 CPU 或发生死锁
state=R/S/D Linux 调度状态:运行或可运行、睡眠、不可中断等待 不能与 Java Thread.State 混为一谈
sysTid Linux 线程 ID;主线程通常与进程 PID 相同 不能把 ART 的 tid 当作系统 TID
schedstat=(a b c) 累计运行时间、累计 runqueue 等待时间、时间片次数 不能把进程启动以来的累计值当成本次 ANR 窗口
utm/stm 累计用户态与内核态 CPU tick 不能不看 HZ 就直接当成毫秒
core 最近运行所在的 CPU 核 不能推断线程始终固定在该核
mutator lock(shared held) ART 访问托管堆时常见的运行时锁状态 不能直接解释为业务互斥锁未释放

示例中的 schedstat 大约表示主线程累计运行 48.55 秒、累计在运行队列等待 1.83 秒、经历 63513 个时间片。这有助于理解线程生命周期内的调度特征,却不能证明 1.83 秒全部发生在本次 ANR。判断超时窗口内是否长期得不到调度,需要 Perfetto 或相邻多次采样。

第三步:沿依赖继续追。 主线程如果在等待锁,要从 waiting to lockwaiting on 找到对象,再搜索哪个线程持有它,并继续追持有者是否在做 I/O、Binder、计算或等待另一把锁。只有等待图形成环,才能称为死锁;单向等待只是锁竞争。

主线程如果停在 BinderProxy.transactNative,只能确认同步调用尚未返回。下一步要识别具体 AIDL 接口、对端进程、对端 Binder 线程池和同期 system_server 栈。系统服务变慢与应用在主线程发起高风险同步调用可以同时成立,不能只选一边归责。

Reason 如果指向无焦点窗口,依赖对象就不是某把 Java 锁,而是 Activity、WindowManager、InputDispatcher 与窗口焦点状态。此时应检查 resumed/visible、window attach、首帧、转场、锁屏和系统窗口覆盖。

第四步:用 CPU、I/O 和内存数据验证候选。 这些数据的价值是缩小范围和提供反证,不是看到一个高值就直接宣布根因。

text 复制代码
Load: 27.91 / 26.13 / 25.27
CPU usage from 26860ms to 0ms ago:
  63% system_server: 46% user + 16% kernel / faults: ...
  46% surfaceflinger: 27% user + 18% kernel / faults: ...
  5.3% com.example.app: 4% user + 1.3% kernel / faults: ...
41% TOTAL: 23% user + 17% kernel + 0% iowait

Load 表示可运行和不可中断等待任务的平均数量,不是 CPU 百分比。在 8 核设备上,1 分钟 Load 接近 28 说明队列压力明显高于核心数,但它没有告诉我们任务是在争 CPU、等待内核资源,还是统计区间里出现了短时峰值。

41% TOTAL 表明采样区间仍有整体空闲容量;iowait=0% 也不支持"CPU 大部分时间都在等块设备"这一强判断。某进程的 major fault 较多同样只是线索,必须确认它是否集中在 ANR 时间窗,并与 I/O、swap 和调用栈相互印证。

text 复制代码
Free memory until OOME 494MB
Total mutator paused time: 62.994ms
Total time waiting for GC to complete: 85.246ms
Total GC count: 152
Total blocking GC count: 2
Total blocking GC time: 172.679ms

Free memory until OOME 只描述应用堆距离上限的理论余量,不能代表系统整体内存。GC 总次数、累计 pause 和累计 blocking time 通常覆盖进程启动以来的生命周期,也不能直接搬到本次超时窗口。判断内存是否参与因果链,需要同时看 GC 时间线、系统可用内存、swap、major faults、LMKD 和同一时刻其他进程的异常。

第五步:把结论写到证据允许的位置。 Reason、时间窗与调用或依赖链闭环时,才能写"已确认根因";多条同期证据一致但缺一段关键链路时,只能写"高概率";单次栈、累计指标或关键词只能列为候选;Reason 与时间线缺失时,应明确写"现有证据不足"。

五、五种常见主线程现场,下一步应该看哪里

采样现场 当前能确认什么 下一步证据
业务计算、I/O 或日志输出 采样时主线程正在这条路径 消息耗时、连续采样、I/O 轨迹,确认它是否覆盖超时窗口
等待 Java 锁或结果 主线程依赖某个对象或条件 锁持有者、后续依赖与是否形成循环等待
BinderProxy.transactNative 同步 Binder 尚未返回 AIDL 方法、对端进程、Binder 线程池和调用时机
nativePollOnce 采样时 Looper 在等待消息或 native 事件 Reason 与 T0-T2 时间线,判断是恢复后的空闲还是窗口类超时
ConditionVariable::WaitHoldingLocks ART 内部条件变量等待 具体锁对象、持有者和闭环;不能仅凭函数名判定业务死锁

例如主线程出现下面这组日志帧,只能说明采样时正在同步格式化并写日志。大量堆栈拼接、logd 背压或对象锁竞争都可能放大耗时,但仍需消息耗时或连续采样证明它覆盖了 ANR 窗口。

text 复制代码
at com.android.internal.os.LoggingPrintStream.flush(...)
at com.android.internal.os.LoggingPrintStream.println(...)
- locked (a com.android.internal.os.AndroidPrintStream)
at java.lang.Throwable.printStackTrace(...)
at android.os.Handler.dispatchMessage(...)

六、工具不在多,而在于补齐哪一段证据

bugreport 与系统 ANR Trace。 新系统通常在 /data/anr/anr_* 保存多份记录,部分旧版本可能使用 traces.txt。bugreport 还能补充 ActivityManager、WindowManager、InputDispatcher、CPU、内存和系统服务上下文。路径访问受版本、构建类型和权限限制,线上不能假设应用可直接读取。

shell 复制代码
adb bugreport anr-bugreport.zip
adb shell ls /data/anr
adb pull /data/anr ./anr

ApplicationExitInfo。 Android 11(API 30)开始可以查询本应用的历史退出原因,部分 ANR 记录可通过 getTraceInputStream() 读取系统 Trace。Trace 位于有限的历史记录中,可能为空或被后续记录覆盖,应尽早读取、限长并关联准确时间。

kotlin 复制代码
val am = getSystemService(ActivityManager::class.java)
val exits = am.getHistoricalProcessExitReasons(packageName, 0, 20)

exits.filter { it.reason == ApplicationExitInfo.REASON_ANR }
    .forEach { info ->
        info.traceInputStream?.bufferedReader()?.use { reader ->
            val trace = reader.readText()
            // 清理敏感字段、限长并关联发生时间后再持久化
        }
    }

Perfetto/System Trace。 线程 Trace 是单点快照,Perfetto 记录的是一段时间。线程调度、CPU 频率、Binder、I/O、窗口和应用自定义 trace section 可以回答"主线程为什么没有运行""Binder 对端在做什么""慢消息到底覆盖了多久"。Android 10 及以上优先使用 Perfetto。

开发期与线上埋点。 StrictMode 适合提前发现主线程磁盘或网络 I/O;线上慢消息记录至少应包含 dispatch 开始与结束、Message 目标、Activity 生命周期、窗口焦点和关联业务。监控本身必须轻量,不能在主线程同步打印完整堆栈或执行重型序列化,否则工具会成为新的干扰变量。

七、最终判断,只回到三件事

日志现象 容易越界的结论 必须补齐的证据
主线程在 nativePollOnce 应用无责 Reason 与 T0-T2 时间线
两个线程等待同一对象 已经发生死锁 锁持有者与循环等待关系
栈顶是监控框架 监控框架导致 ANR 框架自身耗时覆盖超时窗口
Load 很高 CPU 已经用满 核心数、TOTAL、调度与 I/O 时间线
应用堆余量很大 不存在内存压力 系统内存、swap、LMKD 与 GC 时间线

一份可靠的 ANR 分析,最后只需要回答三件事:系统等待的对象是什么,真正的异常覆盖了哪段时间,哪条依赖链解释了超时。Reason 决定方向,时间线决定 Trace 是否有效,锁、Binder、窗口和系统资源决定因果链能否闭环。

暂时无法定位并不可怕。可怕的是用一个栈顶函数、一个累计指标或一个醒目的关键词制造出过早答案。只要明确现有证据能证明什么、不能证明什么,以及下一次应该补采什么,分析就仍然在向正确答案推进。


继续看案例: 如果你想看这套方法怎样落到真实 Trace 上,可以继续读:主线程明明是空闲的,为什么仍然 ANR?3 个真实 Trace 的证据链复盘

参考资料

相关推荐
阿pin3 小时前
Android随笔-MVI
android·mvi
leobertlan3 小时前
【好玩系列】训练一个神经网络指导小孩玩游戏
android·机器学习·程序员
Android-Flutter4 小时前
android LeakCanary 工作原理 详解
android·kotlin
newcih4 小时前
从原理到实战:LSPosed 模块开发完全指南(2026 修订版)
android·xposed·lsposed
AFinalStone5 小时前
Android 7系统无障碍服务(一)全景图与架构概览
android·无障碍服务
Android-Flutter5 小时前
android 自定义view 详解
android·kotlin
WAsbry6 小时前
协程任务的失败控制:取消、异常传播与Supervisor
android
Android打工仔6 小时前
从 finally 理解程序的控制流:它为什么不是 catch 后面的代码?
android·kotlin
随遇丿而安6 小时前
第15周:Service 全功能 + 后台优化
android