
1. ANR的无响应判定机制系统到底在等什么做安卓开发的人几乎都经历过这种场景凌晨两点被线上告警叫醒打开后台一看ANR率飙升。你盯着日志里的Application Not Responding脑子里只有一个问题——它到底卡在哪了ANR不是一个瞬间发生的错误而是一个超时机制的兜底结果。系统在等待某个操作完成等得太久就判定你的应用无响应了。理解这个等待过程比直接背超时时间表重要得多因为定位ANR的本质是回答系统在等什么、谁没给回应。1.1 InputDispatching超时用户操作的卡顿信号最常见也最直观的ANR类型就是InputDispatching超时。用户在屏幕上点了一下系统把触摸事件封装成MotionEvent派发给应用主线程去处理。如果你的主线程正在忙别的事事件队列里的消息得不到执行用户就会感觉界面卡住了。系统这里有个关键判断如果事件在5秒内没有得到处理或者事件处理时间超过了5秒系统就会直接弹ANR对话框。这里的5秒是基于用户体验的折中值——用户能忍受的等待时间大概是3到5秒超过了说明你的应用有严重问题。很多人只盯着5秒这个数字但真正容易忽略的是InputDispatching超时还有一个提前通知机制。系统在派发事件前会设置一个倒计时如果超过2秒还没处理完上一个事件系统会打一个Timeout日志这时候如果有人拉取主线程的栈就能看到卡顿点。也就是说ANR弹窗是最后一步在此之前系统已经给了你很多次机会去发现问题。1.2 Broadcast超时后台任务怎么把主线程拖垮的BroadcastReceiver的超时逻辑稍微复杂一点。无序广播普通广播的超时时间是前台广播10秒、后台广播60秒有序广播的每个接收器也有各自的执行时限。这里有一个非常坑的细节许多开发者以为BroadcastReceiver跑在单独的线程里其实它默认跑在主线程。onReceive()里的代码必须在超时前执行完毕一旦超时系统会判定ANR。更麻烦的是即使你在清单文件里注册了android:process:broadcast来单独开进程也解决不了执行时间过长的问题因为超时机制是系统级的。后台广播60秒的限制在实际线上环境中经常出问题。我见过不少App把网络请求、数据库重操作直接写在onReceive()里的一遇到网络慢一点就ANR。正确做法是广播接收器只负责接收消息和开启任务真正的耗时逻辑应该扔给WorkManager或者前台服务去处理。1.3 Service超时与ContentProvider超时容易被忽略的边界场景Service的超时相对宽松前台服务20秒后台服务200秒。这个超时主要针对onCreate()、onStartCommand()等生命周期回调方法不包含Service内部自己启动的线程。也就是说你在Service里开个线程做耗时任务只要主线程不参与就不会触发Service ANR。ContentProvider的超时是很多大型App的重灾区尤其是在进程启动阶段。系统在发布ContentProvider时如果onCreate()执行时间超过了系统设定的阈值一般是20秒左右会直接ANR。这里的关键是ContentProvider的onCreate()发生在Application的onCreate()之后、主线程的Looper.loop()启动之前一旦卡住整个进程都起不来用户看到的是启动即白屏ANR。1.4 超时阈值与墙钟时间的陷阱系统判断ANR用的时间是从事件开始等待处理到超时时间点的墙钟时间。这意味着即使你的主线程只忙了1秒但如果这段时间内系统负载特别高、CPU在忙着调度其他进程你的应用也可能会被误判为ANR。比如手机厂商深度定制的系统里后台省电策略会限制CPU频率甚至冻结应用进程就很容易出现明明代码跑得很快却被判定ANR的情况。这类问题排查起来非常头疼因为你在本机怎么复现都复现不出来只能在线上通过日志里的系统负载信息去推断。2. 线上ANR类型分布与核心根因为什么代码写得没问题还是挂了我跟很多开发者聊ANR发现一个普遍困惑自己的代码明明没有死循环也没有特别重的操作为什么还是被系统判定为无响应原因在于ANR的根因往往不是单点耗时而是资源竞争、锁等待和系统交互等复合因素叠加出来的结果。2.1 主线程耗时操作的隐性来源代码层面最典型的耗时操作很多人已经背得很熟了主线程做网络请求、主线程执行磁盘读写、主线程做JSON解析大对象。但线上真正高频的其实是下面这些不容易被发现的来源。SharedPreferences的apply()与commit()差异在线上往往被严重低估。apply()虽然是异步写盘但如果写入的数据量比较大框架在QueuedWork.waitToFinish()阶段会阻塞主线程等异步写盘完成。我遇到过的一个线上ANR就是某个模块一次性往SharedPreferences里塞了2MB的数据每次apply()都会让主线程卡上几百毫秒操作频繁时直接触发ANR。Binder调用中的oneway与非oneway语义也是个大坑。跨进程调用系统服务比如ActivityManager、WindowManager时如果调用是同步的主线程就会阻塞在Binder驱动上等待回复。这块最容易出问题的是频繁获取系统信息比如在滑动过程中不断调用WindowManager.getDefaultDisplay()之类的方法。2.2 锁竞争与死锁最隐蔽的定时炸弹锁竞争导致的ANR特征是主线程栈永远停在某个锁的获取上而持有锁的那个线程可能在执行非常慢的I/O操作。这种ANR的隐蔽性在于单独看每个线程的栈都很干净没有任何明显的死循环或耗时调用但你把它们拼起来看就能发现一条完整的阻塞链。我举个真实例子。主线程在执行View.inflate()内部需要获取某个资源锁与此同时一个后台线程在加载图片需要获取同一个锁但它在获取锁之前要先执行一次网络请求来判断图片是否要重新下载。网络一慢图片线程拿不到锁主线程也拿不到锁双方互相等待——而且由于View.inflate()是在主线程执行的前台用户能感知到的就是界面卡死。这种问题的排查难点在于你永远不能在静态代码里一眼看到问题必须结合运行时栈才能还原锁的持有关系。2.3 进程间通信与系统负载的耦合还有一个经常被忽视的ANR来源是你的进程其实是被系统连坐了。当系统服务本身出现异常、系统进程的Binder线程池满掉、或者CPU被其他进程抢占时你的主线程即使执行很快也可能因为拿不到系统服务的响应而无法按时完成工作。这类ANR在日志里有一个显著特征CPU使用率数据中系统CPU占用非常高而你的App CPU占用很低。很多团队看到这种日志就选择忽略直接标记为系统ANR其实这不完全对。合理的做法是检查你的App是否在短时间内频繁调用了系统服务、是否在低内存状态下创建了大量进程这些都可能导致系统服务过载反过来拖累你的主线程。2.4 容易被误判的假ANR厂商ROM的差异化逻辑在排查线上数据时我发现还有一部分ANR其实是假ANR——应用本身没有卡顿但厂商ROM做了额外的超时限制。不同厂商在InputDispatching机制上会有不同的参数调整有些ROM把广播超时从60秒缩到了30秒有些ROM会在低电量模式下直接冻结后台广播。这类问题的特征非常明显同一份代码在A厂商设备上ANR率正常在B厂商设备上ANR率暴涨且ANR日志里的调用栈都停在系统框架层而没有进入你的业务代码。遇到这种情况不要怀疑自己的代码先收集机型分布和系统版本分布再决定是否要针对特定ROM做适配。3. 读透ANR日志从traces文件到CPU报告的关键排查链路ANR的日志文件是所有排查工作的基础。但很多人在拿到日志后的第一反应是搜自己包名下的调用栈这其实是比较低效的方式。正确顺序应该是先看整体信息再判断CPU与负载情况最后才深入调用栈。3.1 日志文件从哪里拿dropbox、/data/anr与Logcat系统记录ANR信息的完整链条包括三个部分/data/anr/目录下的traces文件这是核心信息源记录了ANR发生时各个进程的线程栈。不过只有root过的设备或者带有系统权限的调试机型才能直接读取。线上环境中崩溃收集平台如Firebase Crashlytics、Bugly、自有APM会把traces内容上报到后端通过平台去查更现实。DropBoxManager中的data_app_anr条目系统会把ANR事件的元信息写入DropBox里面包含发生时间、进程名、CPU使用率、系统负载等摘要数据这些信息对于判断是不是系统负载导致的非常关键。Logcat中的ActivityManager日志在ANR发生前系统通常会打出多条关键日志例如ANR in ...、Reason: ...以及CPU usage from ...。这些日志对还原ANR发生前的系统状态很重要。3.2 必须看懂的第一段信息CPU使用率与系统负载打开ANR日志最值得先看的是CPU使用率段落。这部分数据会分两段展示一段是ANR发生时各个进程的CPU占用另一段是ANR之前系统的总体CPU负载统计。举个例子日志里如果显示CPU usage from 5848ms to 0ms ago (2024-06-18 10:23:45.123 to 2024-06-18 10:23:51.971): 45% 1234/com.example.app: 37% user 8.5% kernel / faults: 3821 minor 341 major 12% 567/system_server: 7% user 5% kernel 8% 890/surfaceflinger: 4% user 4% kernel 100% TOTAL: 66% user 31% kernel 2% iowait这段数据包含几个关键信息你的App占了45%的CPU说明它本身一直在运行不是被系统闲置系统整体负载是100%满载说明设备本身压力很大——可能是你的App在疯狂消耗CPU也可能是其他进程导致的系统过载。如果App的CPU占用很低、但系统进程占用很高那排查方向就要转向系统服务调用是否过频。3.3 Reason字段快速判断ANR类型的捷径日志开头会有一行summary信息格式大致是ANR in com.example.app (com.example.app/.MainActivity) Reason: Input dispatching timed out (Because the focused window has not been idle...)Reason字段会直接告诉你具体的超时类型。常见的值包括Input dispatching timed out输入派发超时用户操作无响应。Broadcast of Intent ... timed out广播接收器执行超时。Executing service ... timed out服务生命周期回调超时。ContentProvider ... timed outContentProvider启动超时。看到这个字段后你能快速缩小范围决定接下来该重点看哪个线程的栈。3.4 深入线程栈从主线程扩展到持有锁的线程很多ANR的真正根因不在主线程的栈里而在其他线程的栈里。主线程的栈往往只是显示它正在等待某个条件你要继续找谁应该来满足这个条件它现在卡在哪。我整理了排查过程中需要重点关注的几种线程栈特征主线程栈特征可能根因排查方向停在MessageQueue.next()主线程消息队列本身没有卡顿可能是系统等待超时检查其它进程或系统负载相关日志停在Binder:XXX_XX调用上主线程在跨进程调用系统服务检查调用了哪个系统服务、是否频繁调用停在锁等待如synchronized/LockSupport.parkNanos锁竞争其它线程持有锁不释放找到持锁线程分析它在做什么停在QueuedWork.waitToFinish()SharedPreferences异步写入阻塞主线程检查SP写入数据量、是否在高频调用apply()停在ActivityThread.handleBindApplicationApplication/ContentProvider初始化耗时检查启动阶段的业务代码有一个容易被忽略的点是ANR的traces文件是一个时间切片而不是录像。它只能告诉你这一刻各线程的状态但可能无法反映问题的全貌。所以如果发现某个锁竞争类的ANR需要多收集几次同类ANR的日志做对比看主线程栈是否每次都停在同一个锁上。如果是根因基本就能锁定了。4. 一次线上ANR的完整排查复盘从告警到修复的实战链路理论讲了这么多拿一个真实案例走一遍流程更实用。这个案例发生在某个日活百万级的App上现象是新版发布后启动阶段的ANR率在一个特定机型上从0.4%涨到了3.2%同时用户反馈打开App一直白屏然后提示无响应最后闪退。4.1 初步判断为什么锁定冷启动阶段拿到数据后的第一步是看时间线。ANR率的上涨是在版本灰度期间出现的且集中发生在冷启动后的5秒内。结合用户反馈的白屏现象可以初步判断问题出在Application初始化到首帧渲染之间的某个环节。第二步是看机型分布。数据中80%集中在某款采用特定低功耗处理器、并且底层ROM做了激进后台限制的机型上。这个信息提示问题可能有普遍性但被某个机型的系统策略放大了。4.2 定位过程从CPU报告到锁等待的层层收窄打开该机型上的ANR日志先看summary和CPU数据ANR in com.example.app (com.example.app/.MainActivity) Reason: Input dispatching timed out CPU usage from 4361ms to 0ms ago: 42% 3456/com.example.app: 35% user 7% kernel / faults: 8928 minor 876 major注意这里的8928 minor 876 majormajor faults非常多说明进程在启动阶段有大量缺页中断内存分配压力很大。再看主线程的栈停在了at com.example.framework.ColdStartInitializer.waitForData() at com.example.app.MainApplication.onCreate()但ColdStartInitializer.waitForData()不是在主线程里等待网络请求而是在等一个后台线程加载配置文件。于是转向那个后台线程的栈发现它卡在at libcore.io.Posix.open(Native Method) at java.io.FileInputStream.init(FileInputStream.java)它正在读一个文件而且文件不小。为什么一个文件读这么久因为读取的是从服务器下发的冷启动配置包这个包在某些情况下会非常大。由于网络波动下载的配置包是不完整的本地会反复尝试解析解析失败后触发重试循环导致后台线程一直在做文件I/O和重试始终没把配置加载完主线程一直在等它最终触发了输入超时ANR。4.3 根因确认与修复去掉等待而非优化读取这个案例的修复方案很有代表性。当时讨论过两个解法一是压缩配置文件、提高解析速度二是提升文件读取优先级、降低I/O耗时。但这两个方向都属于优化并不能从根本上解决问题。因为问题的本质是主线程依赖一个不可控的后台任务的完成时长而配置文件的下载与解析天然存在不确定性。最终确定的方案是冷启动阶段不再等待配置文件加载完成。具体调整包括把ColdStartInitializer.waitForData()改为非阻塞回调配置加载完成后通过回调通知业务模块去刷新而不是在onCreate()里同步等待。给配置加载任务设置超时时间超时后使用本地缓存的上一版配置保证App能正常启动。修复配置包下载的校验逻辑避免不完整的文件被反复解析重试。还有一个容易被忽略的配合项由于主线程等待时Looper处于阻塞状态导致Choreographer无法执行帧回调所以用户会看到白屏。即使ANR不弹窗任何长时间的同步等待都会直接影响首帧时间。因此修复这类问题要关注的指标不只是ANR率还包括首帧耗时和进程启动时长。4.4 验证与复盘为什么同一份日志不同人看结论不同修复上线后这一型号的ANR率从3.2%降回0.3%以下首帧耗时也有明显改善。复盘时我们做了个很有意思的测试把原来的ANR日志给两个不同的开发者看一个人说这是主线程等待配置文件的问题另一个人说这是后台线程读文件太慢的问题。两个结论其实指向的是同一个故障链路但因为看日志的切入点不同给出的优化方案完全不同——一个改主线程逻辑一个优化文件读取。这也印证了我一直在强调的一个观点定位ANR先判断阻塞链路的终点在哪再决定从哪一环打断它最合理。5. 从源头治理监控体系与架构层面的ANR防御说完排查案例最后聊聊怎样在问题没发生之前就把它拦住。做ANR治理这么久我最深的体会是ANR问题的修复成本是递增的——在开发阶段发现改一行代码的事在测试阶段发现多花几分钟的事在线上爆发后再定位就需要数小时甚至数天。所以监控和防御体系的建设比排查技巧更值得投入。5.1 主线程卡顿监控在ANR发生前抓住信号实时监听ANR发生的一个有效手段是WatchDog机制。核心思路是在主线程消息队列的头部周期性插入一个任务记录时间戳然后检查这个任务是否在预期时间内被执行。如果长时间没有被执行说明主线程已经卡住。这个方案比单纯等待系统ANR弹窗更能提前发现问题因为系统超时一般是5秒而WatchDog可以做到几百毫秒级别的卡顿检测。实现时有个关键细节主线程的Looper提供了setMessageLogging接口可以监控每条消息的执行耗时。微信团队的Matrix就用了这种方式它通过LooperMonitor统计每条消息的时间识别出idle消息和touch消息的执行耗时从而判断主线程是否处于卡顿状态。线上方案建议结合两种手段Looper消息级监控统计每条消息的执行时长超过阈值就上报能精确定位到具体是哪个Handler的任务。WatchDog线程周期性向主线程post一个Runnable超过一定时间没执行就触发告警用于捕捉消息队列整体阻塞的情况。5.2 启动阶段的架构约束不要让等待成为默认选项冷启动阶段的ANR根因大多是主线程在等待某种资源就绪。这类问题可以通过架构层面的约定来预防比如约定一Application的onCreate()里不执行任何需要等待结果的操作。所有的SDK初始化应该拆成两类一类是无依赖、可立即执行的另一类是有网络/文件依赖的应该异步执行并通过回调通知调用方状态。约定二对进程内全局资源的访问必须要有超时保护。如果某个模块的初始化结果会被其他模块同步获取必须给初始化任务设置一个最大等待时间。就像前面的案例等待配置文件加载时如果设置了3秒的超时最坏情况下主线程也只卡3秒不至于到5秒触发系统ANR。约定三ContentProvider的onCreate()只做轻量初始化。自研的SDK如果使用ContentProvider来实现自动初始化记得onCreate()里的工作一定要轻否则不仅影响这个SDK还会拖慢整个进程的启动。如果确实有重任务可以考虑用一个专门的初始化线程来做不过要记得ContentProvider的onCreate()本身是主线程的。5.3 线程命名与规范为排查省下至少一半的时间一个经验之谈规范每个子线程的名字会显著降低定位问题的成本。系统在traces文件里会显示线程名如果所有后台线程都叫Thread-131你根本分不清谁是谁。改成有业务语义的名字比如image-load-thread-0、network-callback-thread排查时一眼就能找到目标线程。另外如果使用线程池要给线程池设置明确的ThreadFactory方便在栈里识别。这点虽然是基本功但我见过太多团队在压测和排查时才想起来改线程名非常被动。5.4 发布会前的ANR自查清单结合经验这里列一个发布前的自查清单基本能拦截线上常见的ANR问题[ ] 启动阶段是否有网络请求、大文件读取、JSON解析等耗时操作[ ] 主线程是否直接调用了任何可能阻塞的系统服务[ ]onReceive()里是否有超过300ms的逻辑处理[ ] Service的onCreate()/onStartCommand()是否有重I/O[ ] 是否存在apply()高频调用或大对象写入SharedPreferences[ ] 是否有跨进程Binder调用在主线程高频执行[ ] 是否使用了同步锁来等待一个网络或I/O任务的完成[ ] 兜底方案是否对关键资源访问设置了超时这份清单不是让大家在发布会前才开始看而是应该融入日常的Code Review流程。因为ANR问题的特点就是单次代码改动很难直接引发ANR但若干个小问题叠加起来就会触发超时。写在最后ANR这个领域很多技术细节是资料里不会写的。像SharedPreferences的apply()会在waitToFinish()阶段阻塞主线程这个行为在不同Android版本上表现还不完全一样像某些厂商ROM会把后台广播超时时间缩短导致假ANR爆发像InputDispatching的实际超时计算会受到系统负载的干扰——这些坑我都是踩过之后才真正理解的。如果你现在正被某个ANR问题困扰我的建议是先别急着改代码把日志里的CPU使用率、Reason字段和线程栈这三样东西看完搞清楚系统到底在等什么。大多数情况下答案就在这个等待里。排查工具和平台当然重要但真正决定你能不能把问题查清楚的是你对系统在等什么这个问题的理解深度。