
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在打开某页面时出现ANRtrace文件里主线程停在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数据。如果线程状态是RUNNABLECPU时间和墙钟时间差距不大说明确实是计算或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日志的能力先在下一个版本里补上这个能力比埋头分析已经无法复现的问题更有价值。