3. ANR 的产生原因与触发机制

上一章我们聊了 Crash,说白了就是应用“挂了”。那 ANR 呢?它更隐蔽 — 应用没死,但卡住不动了。我在项目里见过太多人把 ANR 当成超时异常来处理,其实背后完全是两套逻辑。

咱们先搞清楚 ANR 到底怎么来的。系统有个 WatchDog 在盯着你,一旦主线程在规定时间内没响应某些“关键事件”,它就判定你 ANR 了。触发时间各有不同:

  • Input 事件超时:5 秒。触摸、按键没处理完。
  • BroadcastReceiver 超时:前台 10 秒,后台 60 秒。onReceive 耗时太长。
  • Service 超时:前台 20 秒,后台 200 秒。onCreate/onStartCommand 没返回。
  • ContentProvider 超时:10 秒。publish 或 query 太久。

为什么会超时?原因其实就那几类。我整理了一张脉络图,先看整体,再细聊。

ANR 触发机制与原因全景 ANR 触发判定 Input 5s Broadcast 10s/60s Service 20s/200s Provider 10s 主线程阻塞 死锁 / 锁竞争 GC 频繁 / OOM 耗时 I/O / 网络 复杂的自定义 View Binder 调用阻塞 说明 ANR 不抛异常,只弹“应用无响应”对话框; 日志中查找 “ANR in” + 包名,配合 traces.txt 定位线程堆栈。

3.1 主线程阻塞 — 最常见的坑

说实话,大部分 ANR 都是主线程干了不该它干的活。比如在 onClick 里直接做网络请求、读写本地大文件、或者做数据库事务。我在项目中遇到过这样一个 case:一个列表页从 SharedPreferences 读取 2MB 的 JSON 字符串做解析,每次打开都卡好几秒,用户狂点屏幕,最后弹 ANR。

解决思路很直接:主线程只做 UI 相关的轻活。耗时操作丢给线程池或者协程。嗯,这里要注意 — 即使是 AsyncTask 在旧版也容易出问题,因为默认是串行执行器,前一个任务没跑完后面就得等。我曾经重构过整个 IM 模块,把 AsyncTask 全部换成 ThreadPoolExecutor + Handler,ANR 率直接降了 60%。

3.2 死锁与锁竞争

多线程环境里,两个线程互相持有对方需要的锁,然后一直等。主线程一旦参与进去,必 ANR。我曾经帮一个电商 App 定位线上 ANR,traces.txt 里看到主线程在等一个锁,而这个锁被后台线程持有,后台线程又在等另一个锁 — 典型的死锁。

怎么发现?看 /data/anr/traces.txt,找到“main”线程堆栈,看到 waiting to lock <0x...> 就多半是锁问题。用 jstack 或者 Thread.getAllStackTraces() 也能现场抓到。你想想看,如果锁范围太大,比如用 synchronized 包裹了整个 onBind() 方法,那十有八九要出事。

// 错误示范:主线程里搞锁 + 耗时操作
public void onClick(View v) {
    synchronized (lock) {
        // 网络请求?数据库写入?—— 不行
    }
}

3.3 内存抖动与 GC 卡顿

频繁创建大量短生命周期对象,GC 就会频繁执行。虽然 GC 在后台线程,但那个“Stop The World”阶段会暂停所有线程,包括主线程。一旦 GC 时间超过超时阈值,ANR 就来了。我在项目中遇到过某直播弹幕流里每秒创建上千个 SpannableString,结果 GC 一次要 300+ms,累积起来用户就感觉到卡顿,严重时弹 ANR。

优化方向:对象复用、使用 StringBuilder、减少自动装箱。用 Allocation Tracker 查看分配热点。

3.4 Binder 调用超时

系统服务调用(比如 ActivityManager、PackageManager)本质上是通过 Binder 跨进程。如果服务端响应慢,主线程就会卡在 transact() 上。我曾经因为频繁调用 PackageManager.queryIntentActivities() 触发了 ANR — 它内部有锁,且可能遍历很大范围的包信息。

避免在 UI 线程直接调用系统 query*、getInstalled* 等方法。实在要调用,加缓存或用 ContentResolver 异步查询。

3.5 如何区分 Crash 和 ANR?

特征 Crash ANR
表现 应用闪退、无响应 屏幕卡住,弹出“应用无响应”对话框
日志位置 Logcat 中 FATAL EXCEPTION 堆栈 /data/anr/traces.txt 中 “ANR in” 行
是否抛异常 是(如 NullPointerException) 否,不抛 Java 异常
回调监听 Thread.setDefaultUncaughtExceptionHandler ActivityManager.getProcessInErrorState() 不可靠,一般用 ANRWatchDog 第三方库
用户感知 瞬间消失 先卡住,再弹框,用户可选“等待”或“关闭”
⚠️ 注意:不要用异常捕获来处理 ANR!ANR 根本不抛异常,你 try-catch 永远抓不到。一定要从 traces.txt 或者使用 FileObserver 监控该文件变化来上报。

3.6 避坑指南

  • 我曾经把 onReceive 里做了 8MB 的图片压缩,广播超时 10 秒根本不够,直接 ANR。后来改成 goAsync() 再加上工作线程,问题解决。
  • 我曾经看到线上 traces.txt 里主线程反复在 View.measure() 里循环 — 那是因为 RelativeLayout 嵌套深度超过 20 层,测量过程 O(n²) 级耗时。改用 ConstraintLayout 后立竿见影。
  • 一定不要忽视 低端机。同样的代码在旗舰机上没事,在 2G 内存的设备上可能 GC 就会卡几百毫秒。压上 StrictMode 跑一跑就能暴露。
  • 如果遇到“是不是 ANR”的争论,直接看 dumpsys activity processes | grep "anr",输出里如果有 waiting to be killed 那就是了。
💡 小经验:我习惯在每个 Activity 的 onCreate 里注入一段轻量级的 Looper.getMainLooper().setMessageLogging(),打印主线程每条消息的执行耗时。只要看到某条消息超过 200ms,就值得警惕。这个方法比等 ANR 上报要早得多。

总的来说,ANR 的根本原因就一条:主线程被耽误了。要么是它自己做了重活,要么被锁/GC/Binder 卡住了。下一层是系统 WatchDog 在规定时间内没收到“活着的信号”,于是触发了 ANR。你想想看,这个机制其实挺粗暴的 — 它不管你是因为什么在忙,超时就判死刑。所以咱们做数据保护时,不光要防 Crash,更要防这种“温水煮青蛙”式的 ANR。下一章我会具体讲如何在 traces.txt 里找到真凶,以及如何用代码监控 ANR 不遗漏。