
聊到Android ANR问题全链路分析与测试解决方案很多做过性能优化的同学第一反应是ANR嘛就是主线程卡了看堆栈不就行了。真经历过几轮线上ANR治理的人都知道这句话只对了一半。主线程卡是表象系统的判定时机、日志抓取是否完整、锁与Binder的等待关系才决定你能不能在最短时间内把问题定死。这篇文章我会按“判定—抓取—定位—修复—回归”这条线完整走一遍既讲清楚系统底层怎么判断无响应也给出可落地的日志分析方法和测试防线。如果你正在被线上偶发无响应折磨或者刚接手性能治理想搭一套测试方案这篇内容应该能帮你省下几个月的弯路。里面涉及的知识不深但都是实战里真正用得上的东西。1. ANR判定与全链路定位思路先搞懂系统在查什么1.1 四类常见ANR的超时规则ANR全称是Application Not Responding直译就是“应用没有响应”。系统其实并不实时盯着你的代码卡不卡而是靠一套超时机制来判定。不同场景下的超时上限不一样了解这些规则能帮我们在拿到日志时快速锁定方向。ANR类型超时时间关键判定依据Input事件5秒主线程在5秒内没有处理完输入事件广播Receiver前台10秒 / 后台60秒onReceive()执行时间超过阈值Service前台20秒 / 后台200秒onCreate()、onStartCommand()执行时间过长ContentProvider启动20秒左右视系统版本Provider首次创建过程超时这里的“前台”和“后台”按应用是否有可见Activity区分。系统给的时间是一种底线不是让你卡满这个时间再去优化。平时压测时主线程单次消息执行超过2秒就该警惕因为这类问题往往会叠加其他卡顿一起出现。Input类型的ANR是最常见的。你正在滑动列表、点击按钮系统把事件投递到应用主线程的消息队列主线程却忙着解析JSON或者等锁5秒没有回应系统就直接判定无响应。注意这里判的不是“某段代码有没有写完”而是“事件有没有被及时消费”。1.2 为什么主线程一卡整个App就“没脸见人”Android的主线程同时干两件核心事情UI绘制和输入事件分发。用户点击屏幕产生的触摸事件并不是直接进到App内部而是由系统InputDispatcher先统一接收再投递到目标应用的主线程消息队列。主线程只有一个Looper事件是一条一条排队执行的。如果主线程被一个耗时操作占住后面的所有触摸事件、绘制请求都得排队。客人点菜后5秒没人理餐厅经理为了避免客人情绪爆发直接挂出“暂停营业”的牌子这就是ANR弹窗的由来。有一个容易混淆的点主线程“忙”不一定就是CPU在高速运转。等锁、等Binder返回、等磁盘I/O这些本质上都是阻塞同样会让事件队列停滞。后面排查时不能只盯着代码执行耗时还要看“主线程究竟在等什么”。2. 现场证据采集ANR日志从哪来、怎么读2.1 第一时间应该拿到手的三种现场资料ANR发生后系统其实会留下不少现场证据关键是你知道去哪拿。第一份是/data/anr/目录下的trace文件。Android 8.0以下版本通常叫traces.txt8.0以上会在这个目录生成带时间戳的anr_*文件。普通应用进程没有权限直接读这个目录但我们可以通过adb bugreport把系统日志整体拉出来然后再翻找也可以试试adb shell dumpsys dropbox系统会把每次ANR的关键信息写入DropBox。第二份是Logcat里带有ANR in关键字的日志段。ANR发生时系统会在Logcat打印进程名、PID、超时原因以及当时的CPU使用分布。这些信息虽然不包含完整堆栈但能给出很明确的排查方向。第三份是应用自身的历史采样。这一份往往最容易被忽略但恰恰是最关键的。线上用户没有root系统目录里的traces我们大概率拿不到所以应用内必须自己维护一个“性能现场缓冲”记录最近的主线程消息耗时、周期性堆栈采样、Activity跳转记录、关键函数执行时间。一旦主线程出现长时间未响应就把缓冲内容打包上报。我在实际排查中踩过的坑是每次用户反馈ANR第一反应都是去要手机型号和系统版本。其实ANR跟崩溃不太一样它更多取决于具体代码路径而不是机型。与其反复追问用户设备信息不如先把日志体系和采样通道做扎实。2.2 traces文件这样翻10分钟锁定主线程瓶颈我们拿到一份trace后不要一上来就翻整个文件那样很容易被大量线程堆栈淹没。我的习惯是先搜关键字main锚定主线程那段。一份典型的主线程堆栈长这样main prio5 tid1 Native | groupmain sCount1 dsCount0 flags1 obj0x... | sysTid... nice0 cgrpdefault sched0/0 handle... | stateS schedstat 334535 201212 12 utm11 stm5 core2 at java.lang.Object.wait(Native method) - waiting on 0x0cb40 (a java.lang.Object) at java.lang.Thread.parkFor(Thread.java:...) ... at com.example.demo.ui.MainActivity$onCreate$1.run(...)几个字段要重点看state主线程当前状态。S代表睡眠/等待R代表正在运行。如果ANR时主线程是R通常说明在做密集计算如果看到S多半是在等锁、等I/O。schedstat括号前三个数字分别是CPU运行时间、等待调度时间、线程切换次数。这个数据能辅助判断主线程到底消耗了多少CPU排查时很有用。waiting on 0x0cb40主线程正在等一把锁。可以在整个trace文件里全局搜索这个地址找谁持有了这把锁。有一个非常容易误判的情况主线程堆栈最后停在MessageQueue.next()上面没有任何业务代码。很多人看到这个就蒙了以为主线程“什么都没干”。其实这恰恰说明ANR发生时主线程处于空闲等待消息的状态此时真正的问题可能出在输入事件根本没有被正确分发或者窗口对焦异常。这时候要继续查“InputDispatcher到底在等什么”而不是在主线程堆栈里死磕。2.3 光看堆栈不够把时间线也补上堆栈只能告诉我们“卡住那一刻”停在哪回答不了“从什么时候开始卡的”。要拼出完整时间线得借助系统级的trace工具。操作流程大概是先在开发者选项里开启系统跟踪或者通过adb shell perfetto命令抓一段15秒的trace在复现ANR的过程中保留完整现场然后拉回来用Perfetto UI分析。打开主线程的时间线后重点看有没有长时间空白区间比如主线程长时间没有执行任何绘制和消息处理任务以及有没有Binder transaction没有及时返回。Perfetto比老的Systrace信息更丰富新设备支持也更好。但要注意release包和低版本机型上trace数据的完整度有差异建议先在测试机上验证一遍抓取流程再推广。3. 高频ANR根因复盘修一个问题就少一类问题3.1 四大根因的典型特征和判定法结合我看过的ANR日志大多跑不出下面四类根因。根因典型场景定位依据主线程I/O数据库读写、大文件读取、JSON解析、网络请求堆栈停在FileInputStream、SharedPreferences、JSONObject等锁等待主线程等锁持锁线程又被长I/O卡住堆栈显示waiting on 0x...并且能搜到其他线程Locked同一引用Binder等待主线程调用系统服务或跨进程接口堆栈停在BinderProxy.transactNative、PackageManager等线程池耗尽工作线程全部忙碌任务在队列中堆积主线程等在Future.get()其他工作线程也停在执行状态I/O类是最普遍的尤其被低估的是本地文件读写。很多人以为只有网络请求才算I/O其实主线程里解析几百KB的JSON一样能轻松卡够5秒。锁等待类的难点在于找到持锁者。traces文件里的monitor地址就是线索全局搜一下同一个地址看到哪个线程Locked了它基本就水落石出。曾经有个案例工作线程持锁后在慢速磁盘上写日志主线程刚好也要拿同一把锁刷新UI两边就活活等出了ANR。Binder等待类比较隐蔽。主线程调用系统服务后如果系统服务因为负载过高没及时返回主线程会一直等。这类问题的堆栈往往很短看不到业务代码要结合Binder日志和系统侧状态判断。线程池耗尽是一种“伪安静”状态。主线程可能在等一个Future结果工作线程则全部陷在IO里抽不开身。如果各个线程的堆栈看上去都在“各干各的”但主线程等的那件事迟迟没结果就可以往线程池配置方向排查。3.2 一个启动ANR案例的完整修复过程讲一个典型的启动ANR案例。某App冷启动时用户经常在启动页卡5秒后无响应尤其是低端机。ANR日志中Reason是Input dispatching timed out主线程堆栈停在JSONObject.init后面跟着一个配置解析方法。进一步分析这个“运营配置拉取”的初始化逻辑在启动阶段同步处理了一份约1.5MB的配置字符串。高端机上跑300ms就能解析完但在中低端机器上要3到4秒再叠加其他启动初始化逻辑很容易破5秒。修复思路很直接解析放到IO线程解析结果缓存起来启动页面不等待配置完成就直接跳转等数据到位后再刷新界面。fun loadConfig() { lifecycleScope.launch { val config withContext(Dispatchers.IO) { parseConfig() } configCache config applyConfig(config) } }改动之后启动ANR消失首屏可交互时间反而缩短了几百毫秒。这个案例告诉我们修复ANR不一定要牺牲业务功能很多时候把“必须等”改成“先走再等”效果立竿见影。3.3 代码层面怎么写出“不卡主线程”的实现下面这些规矩大部分团队其实早就定了但在代码评审里还是总被破防。我再强调一遍实操价值非常高。所有I/O一律离开主线程包括网络、文件、数据库、图片加载、大JSON解析。这是最基础的一条。有些同学会把网络请求放到协程里却在拿到响应后直接在主线程做JSON解析等于没改。SharedPreferences不要在主线程同步commit()。commit()是同步写磁盘数据量一大就容易卡改成apply()后由系统异步落盘能避免绝大多数SP相关卡顿。老代码里如果到处都是commit()调用抓紧排期替换。锁的使用要非常克制。不要在持有锁的情况下去做网络、磁盘操作也不要用一个粗粒度锁保护整块业务逻辑。主线程尽量不要主动等待工作线程持有的锁如果实在绕不开加一个超时等待别死等。Future.get()不要无超时调用。很多并发设计里主线程等工作线程返回结果如果工作线程卡住主线程就废了。改成get(long timeout, TimeUnit)超时后走降级逻辑至少保证UI还能操作。线程池要有名字、要有界队列。自定义线程池命名之后看堆栈时一眼就能知道是哪条链路的线程在忙碌。有界队列配合拒绝策略能避免任务无限堆积导致整个App线程资源被拖垮。另外第三方SDK的初始化也是一个重灾区。有些SDK会在Application.onCreate里偷偷做大量工作而且不提供异步初始化开关。接SDK之前最好先看下初始化逻辑能延后就延后能异步就异步不要图省事全堆在主线程上。4. 测试方案与回归监控把ANR堵在发布前4.1 用可复现的卡顿开关验证日志链路很多团队不做ANR专项测试导致线上出了问题时才发现自己的日志采集链路根本没通。所以第一步不是测试更多场景而是先验证“如果ANR真的发生你的监控能不能把它记下来”。测试包里加一个“卡顿模拟”入口点击按钮后让主线程睡6秒模拟一次严重无响应。这个开关有两大作用一是验证主线程监控能否在休眠期间拍到准确的堆栈二是验证后续上报通道、数据展示是否完整。如果模拟卡顿都抓不出来线上真实卡顿就更别指望了。这类开关只能放在debug包release包绝对不能保留。否则被用户误触那真是自找麻烦。4.2 自动化用例里加“主线程可响应性”断言回归测试里除了验证功能正确还应该加“主线程在关键操作后能及时恢复响应”的断言。实现原理不复杂向主线程post一个任务如果在指定时间内任务没有执行说明主线程卡住了。public static boolean mainThreadResponds(long timeoutMs) { final CountDownLatch latch new CountDownLatch(1); new Handler(Looper.getMainLooper()).post(new Runnable() { Override public void run() { latch.countDown(); } }); try { return latch.await(timeoutMs, TimeUnit.MILLISECONDS); } catch (InterruptedException e) { return false; } }在UI测试用例里点击核心入口之后调用一下断言主线程在3秒内恢复响应。这台机器上的回归用例就能提前暴露掉大多数严重卡顿。另外定期跑一轮Monkey压力测试也很有用adb shell monkey -p com.example.demo --pct-touch 50 --pct-motion 30 --pct-appswitch 20 -v 6000跑完后在Logcat里搜ANR关键字。Monkey是随机事件复现概率不保证适合按趋势观察不适合作为唯一的质量门禁。4.3 线上无响应预警不等用户先受到伤害线上方案的核心是做一个轻量的“主线程健康检查”。我推荐两种做法可以组合使用。第一种是给主线程Looper设置消息耗时统计每次dispatch消息都记录执行时间超过2秒就抓一次主线程堆栈上报。这个方案直观且能关联具体代码路径。第二种是独立监控线程周期性采样主线程堆栈。如果连续多次采到的堆栈完全相同并且累计时间超过5秒就判定为疑似卡死上报卡顿时长和堆栈快照。这种做法对主线程几乎没有额外负担适合线上大规模部署。选型时的基本原则是线上监控只抓严重问题不要把阈值定得太敏感否则大量低价值上报会淹没真正重要的信号。我见过一些团队把掉帧监控阈值设为200ms结果一天上报几十万条后端直接被压垮真正需要关注的5秒卡顿反而被淹没了。有了线上采样数据再配合用户反馈很多低端机上的偶发ANR就能在系统弹窗之前被发现。后续优化方向也有了依据。最后分享一个我自己的工作习惯每次修复完一个ANR我都会在维护文档里记录三件事——触发路径、根因类型、监控指标变化。开始觉得麻烦半年后回头看发现这些记录直接形成了一份实用ANR知识库。遇到新问题先查库能省下一大半排查时间。先把日志做全再谈优化这条原则在Android卡顿治理里同样适用。