3. ANR 的产生原因与触发机制
上一章我们聊了 Crash,说白了就是应用“挂了”。那 ANR 呢?它更隐蔽 — 应用没死,但卡住不动了。我在项目里见过太多人把 ANR 当成超时异常来处理,其实背后完全是两套逻辑。
咱们先搞清楚 ANR 到底怎么来的。系统有个 WatchDog 在盯着你,一旦主线程在规定时间内没响应某些“关键事件”,它就判定你 ANR 了。触发时间各有不同:
- Input 事件超时:5 秒。触摸、按键没处理完。
- BroadcastReceiver 超时:前台 10 秒,后台 60 秒。onReceive 耗时太长。
- Service 超时:前台 20 秒,后台 200 秒。onCreate/onStartCommand 没返回。
- ContentProvider 超时:10 秒。publish 或 query 太久。
为什么会超时?原因其实就那几类。我整理了一张脉络图,先看整体,再细聊。
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 第三方库 |
| 用户感知 | 瞬间消失 | 先卡住,再弹框,用户可选“等待”或“关闭” |
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那就是了。
onCreate 里注入一段轻量级的 Looper.getMainLooper().setMessageLogging(),打印主线程每条消息的执行耗时。只要看到某条消息超过 200ms,就值得警惕。这个方法比等 ANR 上报要早得多。
总的来说,ANR 的根本原因就一条:主线程被耽误了。要么是它自己做了重活,要么被锁/GC/Binder 卡住了。下一层是系统 WatchDog 在规定时间内没收到“活着的信号”,于是触发了 ANR。你想想看,这个机制其实挺粗暴的 — 它不管你是因为什么在忙,超时就判死刑。所以咱们做数据保护时,不光要防 Crash,更要防这种“温水煮青蛙”式的 ANR。下一章我会具体讲如何在 traces.txt 里找到真凶,以及如何用代码监控 ANR 不遗漏。