4. 常见ANR场景分析归纳总结

聊到 ANR,你想想看,很多同学第一反应就是「主线程卡了」。其实没那么简单。

我这几年前前后后治理过上百个ANR案例,总结下来,常见场景就那几大类。今天咱们一个一个掰开了揉碎了说。

核心观点: ANR 本质是系统在规定时间内得不到响应,但原因往往藏在你看不到的地方。
常见ANR场景分类与关系图 ANR(应用无响应) 主线程耗时操作 锁竞争/死锁 Binder调用超时 广播/Service/ContentProvider超时 IO操作 网络请求 复杂计算 大文件解析 synchronized ReentrantLock 数据库锁 SharedPreferences锁 系统服务调用慢 跨进程大数据 广播onReceive超10s Service onCreate超20s ContentProvider query超时 提示:每种场景的根因往往相互交织,排查时需要结合 traces.txt + logcat 交叉分析

4.1 主线程耗时操作

这是最常见的一类,说白了就是主线程干了不该它干的活。

我个人习惯把「主线程触发 ANR」的原因总结成三句话:

  • 做了 IO:读文件、写 SharedPreferences、操作数据库
  • 算了不该算的:大量图片解码、JSON 解析、正则匹配
  • 等了不该等的:Thread.sleep()、LockSupport.park()、wait()
避坑案例: 我曾经在一个 IM 项目里发现,每次收到消息就调 SharedPreferences.edit().putString() 再 apply()。看似没问题,但 apply 会在主线程做锁等待——如果文件很大,写入队列满,主线程一样被卡住。后来全改成异步写入 + 内存缓存,ANR 下降 70%。

还有一个坑:Activity 的 onCreate 里做网络请求。很多同学图方便直接在主线程搞 Retrofit 同步调用。

// 错误示范 —— 主线程做网络同步请求
@Override
protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.activity_main);
    // 糟糕,这行代码会阻塞主线程!
    Response<User> response = apiService.getUser().execute();
}

嗯,只要网络慢点,或者后端挂了,ANR 几乎必现。应该用异步或协程。

4.2 锁竞争与死锁

这属于比较隐蔽的 ANR 场景。主线程没干重活,但它等锁——而且锁被别的线程一直拿着不放。

你想想看,常见的锁争夺点有哪些?

  • 数据库锁:SQLite 是单写多读,写线程慢,主线程读就等。
  • SharedPreferences 锁:多进程模式下,锁竞争更加严重。
  • Log 锁:Log.d 内部也有锁,大量打日志会死锁。
  • 自定义锁:HandlerThread 和主线程之间共享锁,顺序反了就死锁。
经典死锁案例: 主线程持有锁 A 等待锁 B,此时工作线程持有锁 B 等待锁 A。两边互不相让,死锁。但系统只看主线程有没有响应,所以只有主线程被判定 ANR。我曾经在排查时遇到一个 case:自定义 View 的 onDraw 里用了 synchronized (cacheLock),而另一个后台线程也在用同一个锁做耗时缓存清理。主线程等了 15 秒就 ANR 了。

排查锁问题,建议先看 traces.txt 里主线程的「waiting to lock」部分。找到锁对象的地址,再看哪个线程持有它。

4.3 Binder 调用超时

Binder 调用分为两类:

  • 应用调用系统服务(比如 getSystemService、PackageManager.getInstalledPackages)
  • 系统服务回调应用(比如 Input 事件、广播发送)

主线程调 Binder 时,系统会等待远程返回结果。如果远程服务处理慢(比如系统服务压力大、Binder 线程池满),主线程就一直等,直到超时。

我记得有个项目上线后,用户一打开「关于本机」就 ANR。查了半天,发现是 PackageManager.getInstalledPackages(0) 在低端机上要跑 3 秒。系统给的时间只有 5 秒,但如果系统本身也卡,叠加后就超了。

处理思路: 能异步就异步。如果是子线程调 Binder,就算慢也不会 ANR。但有些系统 API 只能在主线程调(比如 ActivityManager.getRecentTasks),无解的话就尽量少调,或者加缓存。

4.4 广播 / Service / ContentProvider 超时

这三种场景其实原理一样:系统给了一个时间窗口,你必须在窗口内完成,否则 ANR。

组件 超时阈值 典型触发场景 ANR 关键字段
BroadcastReceiver 前台广播 10s / 后台广播 60s onReceive 里做网络请求、写数据库、循环逻辑 Broadcast of Intent { act=... }
Service 前台 20s / 后台 200s onCreate / onStartCommand 里有大量初始化 ServiceRecord{...} timeout
ContentProvider 通常 10s query 里做复杂 SQL、跨进程大数据传输 ContentProvider timeout

看一下真实 traces 里的表现:

"main" prio=5 tid=1 Blocked
  at com.example.MyBroadcastReceiver$1.run(MyBroadcastReceiver.java:35)
  - waiting to lock <0x0a2b4c00> (a java.util.HashMap)
  at android.os.Handler.handleCallback(Handler.java:883)
  at android.os.Handler.dispatchMessage(Handler.java:100)
  at android.os.Looper.loop(Looper.java:214)
  at android.app.ActivityThread.main(ActivityThread.java:7356)
重点提示: 广播里的锁等待同样会触发 ANR。很多同学以为只要 onReceive 里代码量不大就没事,但如果它被某个锁卡住了,一样超时。所以广播里最好只做「发通知、启动 Service/JobScheduler」这类轻量操作,真正的处理交给子线程去做。

4.5 综合排查套路

说了这么多,你可能会问:拿到一个 ANR,我怎么快速定位是哪一类?

我个人的排查流程:

  1. 看 traces.txt 里的主线程堆栈 —— 是正在执行代码(CPU 忙),还是在等锁(Blocked),还是在调 Binder(BinderProxy.transactNative)?
  2. 看 logcat 中的 ANR 日志 —— 关键词:DALVIK THREADS、ANR in、Reason 字段(比如 Input dispatching timed out / Broadcast of Intent)。
  3. 结合 CPU 使用率 —— 如果主线程堆栈在一个循环里,CPU 占用 100%,多半是主线程自己在跑耗时计算;如果主线程等锁且 CPU 低,那就是锁竞争。
  4. 看系统服务状态 —— 如果 traces 里 Binder 线程池全满,那可能是系统服务压力大。
一个小技巧: 很多 ANR 其实不是单次触发,而是「慢累积」。比如每次 onResume 都做 200ms 的布局,反复进入退出 Activity,最后一次卡在某个点上。这种需要用 Systrace 或 perfetto 看更细的时间线。

嗯,总结一下。常见 ANR 场景就这几类,但实际项目里往往混合出现。你可以把本章的 SVG 图存下来,遇到 ANR 时对号入座,大概率不会偏。