ANR 基础:什么是 ANR,类型与触发场景归纳

1. 到底什么是 ANR?

ANR 全称 Application Not Responding,说白了就是应用“卡死”了。系统发现你的 App 太久没响应,就会弹出一个对话框——“XXX 应用无响应”。用户看到这个,大概率直接点“关闭”或者“等待”。

我记得刚入行时,每次遇到 ANR 都慌得一批,后来慢慢摸清了它的脾气。其实 ANR 的本质是主线程被阻塞,超出了一定的时间阈值。系统通过 MessageQueue、Service、BroadcastReceiver 等组件内部埋了计时器,一旦超时,就判定为 ANR。

关键点:ANR 不是崩溃,它不一定会闪退,但会给用户极差的体验。你想想看,如果用户在输入框里打字,卡了 5 秒没反应,是不是会暴走?

2. ANR 的四种常见类型

我接触过的项目里,ANR 主要分为以下四类。每种类型的超时时间和触发场景都不一样,需要分别对待。

类型 超时时间 典型触发场景 关键观察指标
输入事件超时
(InputDispatching)
5 秒 主线程处理触摸、按键等事件过长,比如列表滑动时执行大图解码 主线程消息耗时 > 5s,Choreographer 跳帧
广播超时
(BroadcastReceiver)
前台 10 秒 / 后台 60 秒 在 onReceive 里做网络请求或数据库写入 BroadcastQueue 中 pending 的 receiver 超时
服务超时
(Service)
20 秒 在 onStartCommand 或 onCreate 中执行耗时的初始化操作 ActiveServices 中 service 超时
ContentProvider 超时 默认 10 秒 (不同厂商可调) Provider 的查询、插入操作在主线程阻塞,或者死锁导致超时 ProviderMap 中 provider 发布超时

避坑指南:我曾经以为只有 5 秒输入超时才叫 ANR,结果有一次广播超时导致线上大量用户点击无响应,查了半天才发现是 onReceive 里调用了阻塞式网络库。所以,**不要小看任何一种类型**。

3. 触发场景归纳:谁在拖主线程的后腿?

说白了,ANR 的罪魁祸首只有两个:主线程卡住和锁竞争(后续课程会深入对象泄漏如何引发锁竞争)。用一个简单的归纳图来展示它们的关系:

ANR 触发场景层次图 ANR (应用无响应) 主线程消息执行超时 同步锁 / 死锁 耗时 IO / 网络请求 内存泄漏 → GC 频繁 对象锁竞争 / wait-notify 超时 大文件读取 / 同步 HTTP Activity 泄漏 → 长生命周期持有 常见误区 • 显式 new Thread 不会直接导致 ANR,但如果在主线程调用了 Thread.join() 或 Future.get(),就会堵死主线程。 • 内存泄漏本身不会立即 ANR,但泄漏导致频繁 GC → GC 线程占 CPU → 主线程得不到时间片 → 超时

4. 实战中如何快速定位是哪类场景?

当线上 ANR 上报后,我们通常会拿到 traces.txt。我教你一个小技巧:直接搜索 main 线程的堆栈,看它卡在哪个方法上。

----- pid 12345 at 2024-01-15 10:22:33 -----
Cmd line: com.example.yourapp
Build fingerprint: 'google/redfin/redfin:12/SP1A.210812.016/7986624:user/release-keys'
ABI: 'arm64'

"main" prio=5 tid=1 Blocked
  | group="main" sCount=1 dsCount=0 flags=1 obj=0x72c2f488 self=0x7b8a04e400
  | sysTid=12345 nice=-10 cgrp=default sched=0/0 handle=0x7b8e0b2f98
  | state=S schedstat=( 23456789 1234567 123 ) utm=123 stm=45 core=0 HZ=100
  | stack=0x7ffc800000-0x7ffc802000 stackSize=8192KB
  | held mutexes=
  at com.example.yourapp.MainActivity$1.run(MainActivity.java:25)
  - waiting to lock <0x0a0b0c0d> (a java.lang.Object) held by thread 5 (AsyncTask #4)
  at android.os.Handler.handleCallback(Handler.java:938)
  at android.os.Handler.dispatchMessage(Handler.java:99)
  at android.os.Looper.loop(Looper.java:236)
  at android.app.ActivityThread.main(ActivityThread.java:8125)
  at java.lang.reflect.Method.invoke(Native Method)
  at com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run(RuntimeInit.java:656)
  at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:967)

你看,主线程明确写着 Blocked,并且 waiting to lock 某个对象。这就是典型的锁竞争导致的 ANR。如果你看到主线程在 BitmapFactory.decodeStream 里,那就是耗时 IO。如果看到 GC_Alloc 频繁出现,多半是内存泄漏。

注意:不要只看主线程的堆栈,还要关注其他线程的状态。比如上面例子中,线程 5(AsyncTask #4)持有了锁,它在做什么?如果它也在等待另一把锁,就形成了死锁。我曾经排查过一个线上 ANR,主线程等锁,子线程等另一个锁,结果两个线程互相等待,锁的等待图就像一张网。

5. 总结一下

  • ANR 本质是主线程超时,超时类型有输入、广播、服务、Provider 四种。
  • 常见的触发元凶:锁竞争(包括对象泄漏导致的间接死锁)、耗时 IO、GC 风暴。
  • 定位第一步:看 traces.txt 中主线程栈,卡在 waiting to lock 还是某个耗时方法。
  • 避坑:不要在 onReceive 和 onClick 里干重活,保持主线程清爽。

记住一句话:主线程不要做任何可能超过 5 毫秒的事。如果你觉得自己做不到,那就用异步 + 回调,或者协程。这个理念会贯穿我们后续的课程。