
我最早接触ANR是线上用户反馈一个列表页“点了没反应”过一会儿系统弹窗“××进程无响应”。当时拿着一个logcat日志翻来覆去只能看到一行“ANR in”完全不知道怎么下手。后来带测试团队、又参与线上性能治理踩了无数次坑才算把“ANR问题全链路分析与测试解决方案”这套东西磨成型。这篇文章就聊聊我的理解和实操总结。如果你是个Android开发或者正在做性能测试尤其是刚接手稳定性治理的同学这篇文章应该能帮你在碰到ANR时不再只是抓一份trace然后对着主线程发愣。我会从ANR的系统裁定机制、日志分析、测试方案设计三个维度展开最后补一份我实际整理过的排查速查表方便落在团队里用。1. 先搞懂ANR的判定机制系统到底在等什么很多新人第一次看到ANR第一反应是“这不是崩溃吗”。其实不是。ANR的全称是Activity Not Responding放在现在的系统里更准确的叫法是“Application Not Responding”。它和崩溃最大区别是进程还活着只是系统认为你没干完该干的事。1.1 三要素缺一不可超时、主线程阻塞、系统调度判定ANR不是一个简单的时间计数器。系统侧会有一组监控线程定期向应用进程的主线程发送探测消息如果主线程的Looper在超时时间内没有把这条探测消息分发完系统就认定“主线程无响应”。这是三条判断链路的博弈应用侧主线程的MessageQueue里积压了长任务比如网络请求、JSON解析、数据库读写把消息循环堵住了。系统侧system_server里有一个监测线程在等待消息分发结果超时阈值到了就触发ANR。资源侧如果CPU极度繁忙主线程一直处于Runnable但抢不到时间片也会出现超时。所以你会发现同一段代码在高端机和低端机上表现完全不一样。低端机本身CPU调度慢加上多App后台争抢很容易触发ANR。这也是为什么很多ANR只在用户现场出现开发机死活复现不出来。1.2 四类ANR的超时模型别混为一谈我见过不少团队把ANR简单归类为“Input超时”其实系统里有好几套独立的裁定机制。我整理了一个通用版对比表不同厂商ROM会调整数值但总体的裁定逻辑不会变。ANR类型触发场景典型超时时间日志关键特征输入事件超时按键、触摸事件在投递队列里等待过久5秒Input dispatching timed out广播超时前台/后台BroadcastReceiver的onReceive未返回前台10秒、后台60秒Broadcast of intent ... timed out服务超时Service的生命周期回调onCreate/onStartCommand等执行超时前台20秒、后台200秒Service ... is timed out内容提供者超时ContentProvider的启动或访问阻塞超时20秒ContentProvider ... has not published注意广播这一项现在系统对后台广播限制很多如果你在onReceive里做耗时操作哪怕只有几百毫秒在特定机型上也容易被“后台执行限制”掐断进而引发ANR。这在测试环节需要单独设场景不能只盯着主线程一个点。1.3 消息循环机制一条Message堵死一条河Android主线程的运转就是个死循环——Looper不断从MessageQueue里取消息交给Handler分发执行。UI刷新、触摸事件、四大组件的回调、系统消息全部排在这条队列里。只要某一条消息执行时间超过阈值后续所有消息全部堆积。这里有个容易忽略的点系统为了保证UI及时刷新会在高优先级场景加同步屏障跳过异步消息直接执行UI消息。但当UI刷新本身被复杂布局卡住时屏障反而会把其他通道的输入消息全部延后。很多“点了一下没反应、过几秒才开始跳转”的现场就是消息队列被ui线程的测量布局动作堵满造成的。所以分析ANR本质上是分析“主线程消息队列里到底谁在长时间占用”。下文的日志分析就是围绕这个核心展开的。2. 拿到案发现场一张ANR日志把问题定位到代码行ANR发生时系统会自动生成一份“案卷”。这份案卷就是我们定位问题的最重要材料。核心是/data/anr/目录下的trace文件还有logcat里对应时间段的log、以及DropBox里留的异常记录。把这仨拼起来才能还原一条完整的案发时间线。2.1 先学会怎么把trace完整捞出来很多新手第一步就栽了——adb shell拉到一半发现没权限。正确姿势分两步# 1. 先确认有root权限或者使用debug版本 adb shell ls /data/anr/ # 2. 把整个目录拉下来注意用文件方式保存不要直接屏幕输出 adb pull /data/anr/ ./anr_collect/如果是无root的正式版本可以通过“开发者选项里的bug report”来导出把生成的zip包解压后在FS/data/anr路径下也能够获取到trace文件。另外adb shell dumpsys dropbox --print | grep -A 100 system_app_anr也能捞历史信息但内容不如tracer完整。拿到trace后先不看主线程先看文件头部的Cmd line、Build fingerprint和Load参数这些决定了ANR发生时系统整体的繁忙程度。有一个经验如果Load数值远大于CPU核心数说明当时系统级资源已经过载业务代码未必是唯一的甩锅对象。2.2 主线程堆栈判断三步法先找main线程的堆栈一般位于tracer文件靠前的位置。我的定位步骤很固定看线程状态RUNNABLE、BLOCKED、WAIT、TIMED_WAIT。如果主线程是BLOCKED优先看锁如果是RUNNABLE再看CPU占用和当前执行的方法。看堆栈顶的连续几行如果全是IPCThreadState::waitForResponse或者BinderProxy.transact大概率是主线程在等Binder回调。看有没有java.lang.Thread.sleep、IoBridge接口调用、okhttp3的同步请求这些都是最常见的耗时好人选。这里有一个特别容易踩的坑主线程是RUNNABLE状态大概率你会觉得“它正在干活”但再往下翻发现它在Object.wait时状态却是BLOCKED。事实上很多ANR的核心问题是线程状态没变但CPU时间一直为0说明它虽然标记为可运行却被系统基层调度挂起了典型的表现为等锁等得太久、或者block在文件IO的native方法上。这时要配合CPU字段看。2.3 CPU占用和锁等待一眼识破谁是“犯人”Trace文件中每个线程都有CPU占用描述比如cpu1.92s user1.5s system0.4s。拿到主线程的CPU时间后对比ANR发生时段的系统CPU状态主线程CPU时间很高比如2秒真的在跑耗时代码比如复杂计算、大量反射、超大List遍历。主线程CPU时间很低大概率在等待。到底是等锁、等IO还是等Binder需要落到堆栈的具体方法上。系统CPU被占满其他进程runable量非常大系统级挤兑主线程一直分不到时间片。这类“无头案”最容易在低端机上翻车优化方向不是改本线程而是减少全局CPU争抢。锁问题也是重灾区。Trace里会出现Locked字样标志持有某对象的线程后面还会跟着一条waiting to lock两条信息一起看就能拼出完整的锁关系。2.4 用Perfetto/Systrace还原时间线比纯文本直观得多文本trace是静态快照只能告诉我们“某个瞬间在哪儿”无法回答“这5秒内主线程经历了什么”。想要全链路视角推荐用Perfetto分析这段时间的主线程状态。流程很简单复现问题之前先录trace大概率能采集到主线程的调度状态。# 在复现ANR前开启trace可在指定时长后自动停止 perfetto -o /data/misc/perfetto-traces/trace.pb -t 60s sched freq idle am # 结束时拉出来 adb pull /data/misc/perfetto-traces/trace.pb然后在Perfetto网页端打开找到目标应用的main线程看它的cpu slice分布大段灰色表示被卸载或者睡眠对应等锁/等IO。大段蓝色跑满CPU对应耗时计算。频繁的线程切换但单段slice都很短说明cpu调度颠簸线程总在被抢占。把Perfetto的调度结果和文本trace一对照基本可以把ANR原因锁定到“耗时执行”、“IO等待”、“锁等待”或“系统高负载”这四大类里。3. 测试侧怎么设计把ANR按死在发版前定位线上ANR是止损真正有价值的能力是把ANR拦截在发版前。这里要强调的是测试同学不能只盯着“功能对不对”ANR专项需要一整套“监控场景压测”的组合拳。我自己把它们拆成三层。3.1 监控先行线上没有监控线下无从谈起一旦上到线上如果没有监控数据测试就只能靠用户反馈猜。第一步就是在App内嵌一套轻量级的主线程卡顿监控。原理市面上很多框架都在用就是给主线程Looper挂一个Printer系统每条消息执行前后都会回调Printer。我们在这个回调里记录时间差超过阈值就采集主线程堆栈。简单实现思路是这样的public class ANRTracer { private static final long WATCHDOG_THRESHOLD 3000L; private static long lastStartTime; private static boolean canDump; public static void install() { Looper.getMainLooper().setMessageLogging(new Printer() { Override public void println(String x) { if (x.contains( Dispatching to)) { lastStartTime SystemClock.uptimeMillis(); canDump true; } else if (x.contains( Finished to)) { long cost SystemClock.uptimeMillis() - lastStartTime; if (cost WATCHDOG_THRESHOLD) { dumpStack(msg_cost cost ms); } canDump false; } else if (canDump) { // 某条消息执行时若超过单独消息成本则采样 long cost SystemClock.uptimeMillis() - lastStartTime; if (cost WATCHDOG_THRESHOLD) { dumpStack(msg_running cost ms); } } } }); } }这里有个细节真正发生ANR时系统会直接kill进程并生成tracerApp自身很难抓到完整的现场。所以监控的价值更多是“预警”。阈值设多少很讲究——设太低容易大量误报刷掉有效信息设太高又抓不到ANR前兆。我的经验是分档低端机2000ms、中端机3000ms、高端机5000ms重点看“超过阈值但还没真正ANR”的那批慢消息。3.2 专项测试矩阵把高频真实场景建一遍除了监控还需要手动的ANR专项用例。这块最怕的就是“只测主流程”。我做测试矩阵时会刻意把用户易触发、但开发自测时不太可能注意的场景列进去。我把用过的几个高价值场景分享给你场景名称操作步骤回顾预期指标快速连点启动进入页面后连续快速点击入口10次马上退出再进入无ANR弹窗入口响应800ms图片列表滑动在弱网下进入图片瀑布流快速上下滑动中途切换Tab无卡顿超过3秒数据密集型广播在后台发送高频自定义广播同时前台做页面切换前台无卡顿广播收到率99%存储IO重负载先往SD卡写入1GB大文件再进入需要读库的页面页面加载无超时低内存后台恢复开启多个大型App后回到被测App触发Activity重建冷启动恢复正常直播弹幕高频场景大量消息灌入时反复开关评论区、播放、截图消息无堆积无ANR每个场景跑完后如果日志里出现Input dispatching timed out或Broadcast of intent ... timed out基本就意味着找到了一个可以提测给开发的用例。很多线上的ANR都是这些场景的组合叠加——弱网只是基础还要加上频繁操作、后台竞争才真正把主线程拖垮。所以专项测试一定不要“温温柔柔”地测要让系统处于压力状态。3.3 用adb和自动化脚本把压力做起来人工测试很难跑出稳定的压力负载这时候用adb命令做几类“暴力”测试比人肉复现效率高很多。第一类是高频广播轰炸。你可以写一个循环脚本持续发送自定义广播# 一直循环发送广播1000次检验广播接收链路的抗压能力 for i in $(seq 1 1000) do adb shell am broadcast -a com.example.TEST_BROADCAST --es index $i done注意这种测试适合在debug包上跑测试完需要主动把测试广播接收器关闭避免留到线上造成风险。第二类是模拟低端机环境。现在的旗舰SOC性能太强很多问题根本测不出来。可以限制CPU频率和内存压力# 以限制方式模拟低内存环境需要root或厂商测试功能 adb shell echo 1 /proc/sys/vm/drop_caches adb shell stop start也可通过设置里“开发者选项-不保留后台活动”来模拟弱后台场景触发Activity销毁恢复路径。第三类是配合Monkey做随机压测但不要只跑默认配置。我一般是把Monkey事件数设到3万以上速度拉满同时开日志监控事后用关键词捡出ANR关键字。3.4 千万别忘的“竞品对比测试”和回归策略ANR测试有一个很冷门但非常有效的招——竞品对比。把同场景放到竞品App上跑一遍看对方是否也有类似卡顿。比如你做一个图片社区弱网加载图片时自己的瀑布流卡到ANR竞品却能秒开。说明问题很可能不在弱网本身而在缓存策略、图片库选型或者线程调度设计上。这个角度可以帮团队把问题从“复现-修复”的被动思维升级到“体验对标”的主动优化。回归策略也重要。每次修完一个ANR最好把它沉淀成一条自动化monkey脚本或专项用例进到回归包里。否则同一个锁问题三个月后另一个人改代码又会改回来——我见过太多次。4. 常见误判与排查实录别让“假ANR”带偏方向ANR排查里面最容易让人内耗的其实不是难定位而是把时间和精力花在“假ANR”和“伪案”上。以下这几个坑我一个个踩过给你说说怎么绕。4.1 “测试机不卡线上一直弹”的真相线上反馈ANR测试机始终复现不了这种情况90%不是“偶现玄学”而是测试环境压根没模拟到线上的负载特征。常见的差异点线上用户的机型更杂低端机主频低多核调度更保守。线上App运行过程中有大量后台任务、推送、日志SDK、统计SDK在跑抢占cpu和IO。用户习惯是快速切换、连点、边加载边操作测试场景往往更“温柔”。建议是按真实用户的路由比例准备一批中低端测试机内存4GB以内作为专项机测试时先让被测App后台挂几个大应用制造竞争压力再进来操作这种状态能复现出大部分“偶现”ANR。4.2 ANR现场被系统吃掉怎么提前保留证据系统的ANR trace确实会写到/data/anr/但在很多纯线上环境里用户没有root也没有连开发机trace根本捞不回来。这种情况下App内部的监控系统就是最后一道防线。我建议在线上的正式包里不要只做“卡顿检测”而是在检测到主线程超过阈值后主动采集主线程堆栈和其它关键线程堆栈线程总数、运行线程数当前MessageQueue里积压的消息数量内存、进程优先级、CPU占用快照这批数据上报到后台就能和线上系统侧日志对上。哪怕没有tracer原始文件也能很容易从“主线程处于BLOCKED等待某锁”这类信息还原出大致的案发原因。这里注意合规问题堆栈收集做脱敏处理不采集用户身份信息、不采集文本内容只保留类和方法的包名提前做隐私合规评审。4.3 堆栈里找不到业务代码“读三层栈”就够了有时候tracer里主线程堆栈整条链路都是系统代码业务代码一个都没看到。新手最容易直接放弃。实际上Stack Trace是分层的你要读三层Java层看android.os.Binder调用能看出在等什么系统服务。Native层看libc相关调用能判断是等socket、等文件IO还是等锁。Kernel层看kernel stack典型如do_sys_open、do_page_fault分别对应文件打开/内存缺页。我之前排查过一个ANR主线程表面停在Activity.dispatchTouchEvent业务代码完全没有异常但读到kernel层后看到主线程卡在ep_poll系统调用。顺着时间线查才发现是某个后台文件同步服务持有了全局文件锁主线程在打开文件时被强制等待了4秒多。这种案子不读穿三层真的无从下手。4.4 排查速查表遇到ANR先按这个顺序过一遍我总结了一张排查速查表现在团队遇到ANR都按这个顺序走异常表现可能原因优先级动作主线程堆栈在Looper.loop消息队列空转系统调度延迟或CPU抢占先看系统负载再看CPU时间排除系统级原因主线程在BinderProxy.transact等待系统服务返回值检查系统App是否卡顿再查调用方是否频繁调用binder主线程在MessageQueue.next主线程在等消息大概率是同步屏障锁住了结合Perfetto看同步屏障时间轴主线程是BLOCKED等待某monitor锁竞争去trace里找持有锁的线程看持锁线程在干嘛主线程TIMED_WAIT主动sleep或wait常出现在业务空转检查代码里有没有主动Thread.sleep主力线程系统负载高、但主线程调用栈无明显耗时系统级cpu风暴对比线程个数和cpu占比排查第三方SDK无限轮询如果你按这张表排查完还是定位不到再考虑是不是主线程被“别的进程连累”。比如公共目录文件读写出问题、系统服务挂起导致全局Binder阻塞这些通常需要结合系统侧log去看。我自己在实际项目里最常用的“三板斧”——线上监控埋点、专项低端机场景、自动化压力脚本这三样组合起来目前能拦截掉团队里九成以上的ANR回归。最开始那个线上反馈列表页ANR的案子最后就是用弱网加频繁快速切页这个场景复现出来了。问题出在一个图片库在小内存机型上频繁解码大图主线程被多次磁盘IO轮流堵住。修复方式是调整了图片解码的采样策略把解码全部放到子线程触发阈值从一秒降到了两百毫秒以内。所以ANR这件事别怕。从“裁定机制”到“日志解析”再到“测试方案”把这条链路走通了你会发现它本质上是一套可分析、可复现、可预防的系统问题。你现在积累的每一个trace和场景case都是下次让线上安静一点的本钱。