1. 为什么“ANR分析”值得专门梳理成一条Flow
做Android性能优化的同学应该都有过这种经历:线上反馈群里突然有人丢一句“我的App点了没反应”,紧接着是“卡死了”“白屏了”,然后你一脸懵地去扒日志。查来查去,最后要么定位到主线程卡死,要么看到一个不明不白的“Application Not Responding”。ANR这个问题,说难不难,说简单也绝不简单。难的地方在于,它不是靠一两行日志就能立刻判断的,你需要一条完整的、可重复的、能快速定位根因的分析流程。
我这里说的“ANR Analysis Flow”,指的就是把这套分析过程沉淀成标准动作:从收到反馈开始,到抓取trace文件、解析主线程堆栈、核对CPU负载、确认Binder调用、排除系统因素,最后给出发版级别的结论。这套流程看起来像是“看日志”,实际上背后涉及Android系统对输入、广播、服务、Provider的超时判定机制,也涉及对线程调度、锁竞争、系统负载的理解。
我最开始做这块的时候也是零散地在处理,线上出一个ANR就临时抓一次,抓到的文件也不全,经常分析到一半发现关键时间段缺失。后来踩的坑足够多,才慢慢把整个ANR分析流程固化下来。这篇内容就按我现在在项目中实际执行的顺序来写:先讲清楚系统如何判定ANR,再还原完整的证据收集过程,然后手把手拆解trace文件和日志,最后给出归因与避坑清单。不管是刚接触ANR的新手,还是已经被线上ANR折磨过一段时间、想建立规范化分析流程的工程师,这篇文章都可以帮你少走不少弯路。
2. ANR触发机制与判定规则
2.1 系统判定ANR的四种典型超时
ANR的全称是Application Not Responding,很多人理解成“主线程卡了”,这个说法不完全准确。系统判定ANR的逻辑是“超时”,不同类型的超时对应不同的应用行为。
第一种是输入分发超时,也就是我们最常见的Input Dispatching Timed Out。系统派发按键、触摸等输入事件时,如果应用在5秒内没有完成处理并返回,ActivityManager就会收到输入管理器的超时报告,直接弹出或记录ANR。这里要特别注意,5秒指的是超时阈值,但从事件派发到最终判定,中间还有系统的处理周期,实际崩溃报告里记录的“Duration”可能不止5秒。
第二种是广播超时。BroadcastReceiver在onReceive里执行时间过长,前台广播超过10秒、后台广播超过60秒没有执行完,系统会判定广播ANR。很多人以为后台广播有60秒很宽裕,其实不然,如果正好赶上主线程被其他任务占死,onReceive根本没有机会执行,超时判断照样生效。
第三种是服务超时。Service的onCreate、onStartCommand等关键生命周期方法在前台服务20秒、后台服务200秒内没有被执行完,就会被判定为ANR。低版本Android上Service超时的判定条件偏向“没有执行完”,高版本则更侧重于“所在线程无响应”。
第四种是ContentProvider超时,主要是Provider在启动发布过程中超过20秒未完成。这类问题在App冷启动场景里出现频率较高,尤其当Provider初始化涉及大量IO或等待其他进程唤醒时。
还有一种容易被忽略的JobService超时,JobScheduler回调如果长时间未返回,系统也会记录为ANR,但在线上问题里占比相对较低。
理解了这几种判定规则,你才能真正明白为什么有些ANR的主线程堆栈明明停在某个函数上,而有些ANR的主线程状态却是RUNNABLE却什么都没干。前者是应用真的在处理某个耗时任务,后者可能是任务队列被卡住,系统迟迟没有给它派发新的工作。
2.2 代码层面最容易踩的触发场景
超时判定只是系统侧的逻辑,落到我们自己的代码里,触发ANR的场景通常可以归成几类。
主线程执行了耗时操作是最直观的一种。比如在onClick里直接做网络请求、读数据库、解压大文件。严格来说Android官方明令禁止主线程执行超过一定时长的操作,但开发过程中总有一些“侥幸”代码被带上线。这类ANR的堆栈通常非常明确,主线程停在某个高频函数上,比如SharedPreferences的commit、SQLite的query或者Bitmap的decodeFile。
锁竞争是另一类高发原因。主线程等一个子线程持有的锁,子线程又在等主线程的另一个锁,这就形成死锁;如果锁的持有时间过长,主线程长时间阻塞在wait或lock上,同样会触发超时判断。这类问题的特征也很明显,主线程堆栈里的状态往往是WAIT或者BLOCKED,旁边的附属线程会有对应的锁信息。
Binder调用阻塞常常被忽略。主线程调用了某个系统服务接口,比如PackageManager.getPackageInfo、ActivityManager.getAppTasks,如果系统服务执行缓慢,主线程就会卡在Binder代理的transact调用上。这类ANR有相当一部分系统背锅,但应用侧也可以通过缓存结果、避免高频次调用、延迟到子线程等方式来缓解。
还有一类是系统资源问题,比如CPU被其他进程占满、系统处于低内存状态、设备深度休眠等。这类ANR的特征是应用主线程根本不忙,甚至连执行机会都没有,但系统长期无法调度它就判定超时。这类问题往往需要结合系统日志、CPU负载和机型分布来综合判断。
3. 完整ANR分析流程:从发现到复盘
3.1 流程主线与时间线
我现在在项目里执行的ANR分析流程,按时间线可以拆成六个环节:问题受理与信息收集、原始日志保全、trace文件与日志解析、原因归类、修复验证、线上监控回归。
第一步是问题受理。无论来自用户反馈还是监控平台,先要确认设备的Android版本、系统版本、应用版本、机型、出现时间、复现路径。很多时候用户反馈只有一句“卡死了”,这不够。ANR分析最怕信息不完整,尤其是缺少准确的复现时间,因为trace文件和事件日志都依赖时间去对齐。
第二步是日志保全。这一步极其重要,也是很多人容易忽略的。发现ANR后,不要急着去拷data/anr目录下的文件,应该优先抓bugreport或者在系统设置里打开开发者选项的“显示所有ANR”和“后台进程限制”等信息,第一时间保持现场。对于线上问题,一般依赖推送SDK或者崩溃平台自动回传日志,所以要确保产品侧开启了ANR自动采集能力,否则后面的一切分析都将缺少关键证据。
第三步到第五步是核心分析过程。拿到日志后,先看logcat里的ANR描述信息,再到trace文件里找主线程位置,结合CPU负载与系统事件判断根因。第六步是回归验证,通过代码改动或者线上灰度确认问题是否真正缓解。
这个流程看起来冗长,但真实执行中很多步骤是可以并联的。比如拿到日志后,logcat和CPU负载可以一起看,不用等trace完全解析完。关键是永远不要让分析过程中断在“缺少日志”这一步,否则再强的分析技巧也无用武之地。
3.2 前置信息收集清单
我把自己每次分析ANR时固定会收集的信息整理成了一张清单,按照优先级从上到下:
- 应用版本和渠道:灰度包和全量包的问题范围可能完全不同。
- Android系统版本与定制ROM类型:不同厂商的ANR判定和日志输出字段会有差异。
- 机型与芯片平台:低端机和高端机的性能差异直接决定ANR的根因属性。
- ANR发生的准确时间和持续时间:用于对齐trace文件生成时间。
- 用户操作路径:尽量还原出现ANR前的最后一个操作。
- 是否首次出现:首次出现和反复出现,分析思路完全不同。
- 是否有系统负载、内存压力、充电状态等环境信息:这决定了是否属于系统因素。
这些信息可以帮助你把一个模糊的问题一步步收窄。比如同一个ANR只在某个机型上出现,基本可以排除通用代码逻辑问题,转而观察芯片平台、厂商调度策略、屏幕分辨率等因素。如果同一版本在多个机型上大面积出现,那大概率是代码路径里有公共的耗时操作。
3.3 证据收集的关键渠道
ANR的证据来源主要有三个渠道。
第一个是系统生成的trace文件,路径通常在/data/anr/目录下,不同厂商可能略有区别。Android原生系统里,文件名通常是anr_时间戳、trace_时间戳或者类似格式。由于这个目录一般需要root权限才能直接读取,线上问题通常依赖bugreport命令来顺带导出,或者通过adb shell ls /data/anr/查看是否存在文件再尝试读取。如果是开发阶段,用debug版本连接adb,部分厂商系统也允许直接读取。
第二个是logcat中的ANR事件描述。系统在生成trace文件的同时,会在logcat里输出以“ANR in”开头的日志行,这行日志包含了导致ANR接口的判断和具体超时时间,也会输出“Reason:”字段。Reason字段非常有用,比如input dispatching timed out和executing service的应对分析思路就不一样。
第三个是事件日志EventLog,这里会记录am_anr、am_proc_died、am_kill等关键事件,尤其是am_anr事件里的数据格式能告诉你超时类型、进程名、pid、时间。结合系统日志,还能看到ANR发生时设备是否处于高负载状态。
4. trace文件的深度解读
4.1 trace文件里有什么
trace文件是ANR分析的核心材料。拿到文件后不要直接搜“main”线程堆栈就开始看图说话,先看整体结构。
文件头部通常记录进程CPU使用率、线程总数、ANR发生时的系统负载。我见过很多新手一上来就找主线程,忽略了上方的“CPU usage from ...”段落,而这恰恰是判断系统资源问题的重要证据。比如这一段会显示当前进程CPU占用率、用户态与内核态占比,以及各个占比排名的线程。如果整个进程的CPU占用率很低,主线程却出现ANR,基本可以判断问题不在应用自身,而是系统没有及时调度。
接下来是每个线程的详细堆栈。线程信息块里包含线程名、线程优先级、tid、线程状态,以及当前锁信息。主线程在Android里通常叫“main”,通过tid可以直接在EventLog或CPU负载信息里对号入座。
4.2 主线程堆栈分析要点
找到主线程的堆栈后,第一步是看线程状态。Java层线程状态常见的有RUNNABLE、WAIT、TIMED_WAIT、BLOCKED和NATIVE。这些状态对应不同的分析方向。
如果主线程是RUNNABLE且当前的堆栈正停在某个Java函数上,比如BitmapFactory.nativeDecodeStream或者SharedPreferencesImpl.writeToFile,那就是典型的应用侧耗时操作,分析重点在于为什么这个操作这么慢、能否挪到子线程。如果主线程是WAIT或者TIMED_WAIT状态,往往在等锁或者等待某个事件,这时候要结合下面的锁信息找到阻塞源头。
我还要强调一点:不要只看主线程当前的堆栈,因为ANR发生时,主线程的堆栈可能是“过去式”的,它只代表最后采样时刻的状态。系统在写trace文件时会强制dump所有线程的栈信息,但dump过程本身可能引入偏差,比如主线程已经在等待别的资源了。所以我在分析时一定同时看主线程附近几个关键子线程的状态,比如是否有线程持有主线程需要的锁、是否有线程在做Binder调用等待返回。
4.3 全局CPU负载与线程状态解读
ANR发生时,系统并不会只dump应用进程,它会记录一个全局的CPU占用概况。这一部分非常容易被忽略,但它的价值不亚于主线程堆栈。
拿我遇到过的一个典型案例来说:线上反馈App在打开某页面时出现ANR,trace文件里主线程停在Activity.onCreate里,看起来像是在执行页面布局。但是核对全局CPU负载后我发现,当时的设备CPU使用率接近100%,其中系统进程surfaceflinger占了一大半,应用进程的CPU占用反而很小。这就说明主线程卡住的根本原因不是App布局代码本身质量差,而是整个系统正在经历高负载,应用分不到CPU时间片。如果只盯着主线程堆栈去优化布局,方向就完全错了。
线程状态除了Java层的RUNNABLE、WAIT之外,还有native层的调度状态,比如D状态表示不可中断的睡眠,通常与内核态IO有关;S状态表示可中断睡眠;R状态表示正在运行。当主线程处于native的Binder调用中,状态可能显示为S或者D,此时要看调用的是哪个Binder服务。如果是某个系统服务在忙,主线程等Binder返回的过程就会拉长。
5. 常见ANR归因模型与确认手段
5.1 主线程IO与高耗时计算
主线程IO类ANR是最容易定位的类型,也是很多App ANR占比的大头。
具体分析方法是:在trace文件里找到主线程的Java堆栈,如果确认当前正停在IO相关的方法上,比如SQLite的query、SharedPreferences的commit、FileInputStream的read、DiskLruCache的edit,就能比较肯定地下结论。但要进一步确认为什么慢,最好配合CPU数据。如果线程状态是RUNNABLE,CPU时间和墙钟时间差距不大,说明确实是计算或IO量太大;如果线程状态是S或者D,说明系统调度或存储硬件状况出现了问题。
比如有一次ANR主线程停在SQLiteDatabase.query上,看起来简单,但反复出现且只发生在存储空间几乎写满的设备上。后来定位到是数据库文件碎片化严重,查询触发了大量随机IO。这类问题不能单纯用“不要在主线程做数据库查询”来解释,还要考虑存储环境。
5.2 锁竞争与死锁
锁竞争类ANR的分析要更细致。主线程堆栈中会显示类似“waiting to lock <0x...> held by thread xxx”这样的信息。此时要学会顺藤摸瓜,找到持有锁的线程,再分析它为什么迟迟不释放锁。
有一种非常典型的死锁场景:主线程等待一个静态锁,而持有锁的子线程又通过Handler往主线程发消息并等待主线程执行完。如果主线程已经被另一个任务卡住,子线程的等待会一直持续,锁永远不会释放,最终主线程ANR。这种堆栈单独看主线程只能看到“waiting to lock”,必须结合子线程堆栈才能还原完整链路。
我在处理这类问题时,通常会在trace文件里同时搜“held by”和“waiting to lock”这两个关键词,把所有锁关系打印出来,构建一张锁依赖图。这比肉眼一个一个看堆栈高效得多。
5.3 Binder调用阻塞
主线程堆栈停在Binder相关方法时,要分清阻塞发生在调用方还是被调方。Java层常见的Binder调用栈是android.os.BinderProxy.transactNative。此时进程状态通常是S或者D,说明主线程进入了内核态等待Binder返回。
确认阻塞的Binder服务名字很重要。通过跟踪堆栈里的Parcel对象,有时能看出具体调用了哪个接口。如果结合EventLog发现系统进程system_server当时正忙,比如正在执行AMS的某些重量级任务,那大概率是系统侧负载。这种情况下应用只能通过缓存、异步化、限制调用频率等方式规避。
5.4 系统资源耗尽
当trace文件里主线程状态是RUNNABLE但没有明显的高耗时函数,或者主线程停在Looper.loop里根本没执行到业务代码时,就要高度怀疑系统资源问题。
此时必须把CPU负载数据、可用内存状态、前后台进程数、CPU频率信息综合起来看。我遇到过机型上是小核心调度策略激进导致的主线程饥饿问题,也遇到过低内存设备频繁触发lmk杀进程,导致ANR时间点和进程被杀时间接近的案例。系统资源类ANR需要跨团队配合,单纯改业务代码基本无能为力,但你可以通过合理降低业务复杂度、推迟启动任务、减少子线程竞争等手段来降低资源饥饿的触发概率。
6. 避坑清单与工具效率建议
6.1 分析时容易被带偏的错误判断
第一,不要一看到主线程堆栈就关联到最近改动。ANR的发生与代码改动不一定是因果关系,尤其线上环境存在大量变量。正确的做法是先判断线程状态、系统负载、时间线,再回到代码上比对改动面。
第二,不要忽略“ANR in”日志里的Duration和Reason。同一个主线程堆栈,如果Reason是input dispatching timed out,可能是输入事件根本无法派发到应用;如果Reason是broadcast timeout,重点要看广播执行的时限。这两种Reason对应的优化方向和责任范围完全不同。
第三,不要完全信任trace文件里的CPU时间片。trace文件生成时系统会对所有线程做采样,采样时机不一定能反映ANR持续期间的真实状态。如果条件允许,特别是开发阶段,可以在ANR发生时去抓连续的systrace或者simpleperf数据,还原更完整的时间线。
第四,不要急着给系统服务扣锅。Binder调用慢不一定就是系统问题,也可能是应用自身调用频率异常高、参数过大、或者持有了本不该持有的锁,导致一次调用迟迟无法返回。
6.2 适合日常连续监控的方案
线上ANR率如果已经高到需要日常盯防,单靠人工抓trace文件不现实。我建议分三层搭建监控体系。
第一层是系统级监控,直接读取Android系统的ANR事件,在Logcat和EventLog出现am_anr时采集当时的堆栈与CPU信息。很多APM平台已经内置了ANR监控能力,但没有内置的也要抓原始数据留下来。
第二层是运行时监控,在应用内对主线程的执行时间做插桩或采样。排查ANR时,入口方法的执行耗时、主线程消息队列中消息的执行时长,这些数据可以帮你定位到“卡”的触发点。通常的做法是向主线程Looper设置一个PrintMessageLogging拦截器,在每个消息执行前记录时间,执行后计算耗时;超过阈值就上报当时的堆栈与耗时分布。
第三层是线下复现与自动化压测。用自动化测试工具在特定Fragment里做高频点击、快速切换页面、大图加载压测,把系统配置模拟到低端机水平,往往能比较稳定地复现ANR。
6.3 与技术基建的联动
ANR治理到了一定阶段,纯靠“出问题再分析”的模式效率很低。我现在的项目里,已经把ANR分析流程和前端的异常看板、发布系统、AB实验平台做了联动。每次发版之前,针对上一版本的Top ANR做回归用例;灰度期间,如果某个版本的ANR率超过阈值,自动触发告警并拉取该版本近期的日志归档。
作为后续方向,我还在尝试把历史ANR处理记录整理成决策表,将输入特征(Android版本、机型、线程状态、Reason字段、堆栈特征)做成一个简单的LR模型来预估某类ANR的优先级和可能归因。这也算是借鉴了业界把tensor flow这类机器学习框架用于稳定性数据分析的思路。不过说实话,对于大多数业务团队来说,把基础的数据采集和分析流程做扎实,比引入复杂的模型更有效。
最后几个实操体会
按这套ANR Analysis Flow执行下来,最大的收获不是某个具体问题的解决方案,而是遇到ANR不再慌张。再难的问题,只要证据链完整,总能一步步推到根因。
我个人的一个小技巧是:拿到trace文件后,先不要急着读主线程,先花两分钟做三件事——看文件头的CPU usage、看“ANR in”的Reason字段、搜“held by”和“waiting to lock”这两个关键词。三件事做完,大概心里就有底了,剩下的就是往对应的归因方向深挖。
另外多说一句,线上ANR的真实处理率通常不会太高,这跟你分不分析没关系,跟你有没有可持续监控紧密相关。如果现在的项目还没有自动收集ANR日志的能力,先在下一个版本里补上这个能力,比埋头分析已经无法复现的问题更有价值。