ANR 产生原因分析

各位好,今天我们来聊聊 ANR 产生的根本原因。说白了就是主线程在 5 秒内没干完该干的活。为什么会这样?我把它归纳成四类:主线程阻塞、耗时操作、锁竞争、Binder 调用超时。下面一个个拆开看。

核心判断标准: 任何导致主线程 Looper 无法在设定时间内处理新消息的行为,都可能触发 ANR。
ANR 四大原因 主线程阻塞 耗时操作 锁竞争 Binder超时 Handler阻塞 View.draw()超时 大文件IO 网络请求 死锁 长时间持锁 服务端卡顿 Binder线程耗尽 💡 实际项目中往往多种原因叠加 例如:主线程持有锁 → 等待Binder服务端 → 服务端又在等另一个锁 → 形成链式阻塞 单一原因很少单独出现,日志分析时要同时关注CPU、锁等待、Binder transactions等。

1. 主线程阻塞

这是最常见的 ANR 原因。主线程(UI 线程)负责处理用户输入、绘制界面、分发事件。如果它被某个工作卡住超过 5 秒,系统就会弹 ANR 对话框。

常见阻塞场景:

  • Handler.post 大量任务: 比如某个监听器回调里做了很多计算,或者连续 post 了几百个延迟消息。
  • View 树递归遍历: 某些自定义 View 的 onDraw 或者 onLayout 里做了耗时操作,比如解析大图片、递归遍历所有子 View。
  • Activity 生命周期回调: onCreate、onResume 里直接做网络请求或数据库查询。
个人经验: 我曾经遇到一个 ANR,起因是列表的 Adapter 在 getView 里 new 了一个大 Bitmap。每次滑动都要解码,主线程直接被堵死。后来改用 Glide 加载图片,问题解决。

2. 耗时操作

直接在主线程里执行了长时间的任务。包括但不限于:

  • 网络请求(HttpURLConnection、OkHttp 同步调用)
  • 文件 I/O(SharedPreferences commit、文件读写、数据库查询)
  • 复杂计算(JSON 解析、加密解密、正则匹配)
  • Bitmap 解码(尤其是大分辨率图片)

每个操作可能只有几十毫秒,但叠加起来就容易超过 5 秒。你想想看,一个接口回调里先读 SharedPreferences,再解析 JSON,再写文件——不知不觉就几百毫秒了。如果用户连续操作,主线程消息队列越堆越长,最终 ANR。

注意: SharedPreferences 的 commit() 是同步写入磁盘,一定要用 apply() 或迁移到 DataStore。我曾经有个项目因为 commit() 太频繁导致 ANR,改成 apply() 后瞬间清净。

3. 锁竞争

这属于比较隐晦的原因。主线程试图获取一个对象锁,但该锁被其他线程长时间持有,主线程只能干等。常见的锁竞争模式:

  • 死锁: 两个线程互相等待对方释放锁。
  • 锁被长耗时任务持有: 比如工作线程在 synchronized 块里做网络请求,主线程刚好也需要这个锁。
  • ReentrantLock 未及时 unlock: 异常分支没有释放锁。

怎么排查?看 traces.txt 里主线程的堆栈,如果停在 Object.wait() 或 LockSupport.park() 上,基本就是锁竞争。再看看其他线程在干什么。

典型案例: 主线程调用某个单例的方法,该单例使用了 synchronized 关键字,而工作线程恰好在该方法内执行耗时操作(比如写文件)。主线程被迫等待工作线程完成,如果写入超过 5 秒,ANR 就来了。

4. Binder 调用超时

系统服务通信走的都是 Binder 机制。比如执行 startActivity、bindService、获取系统服务、跨进程数据传递等。如果服务端处理太慢,或者 Binder 线程池耗尽,主线程的 Binder 调用就会堵住。

具体场景:

  1. 系统服务繁忙(如 PackageManager 正在安装应用)
  2. 服务端死锁或运行慢(比如 ContentProvider 查询数据库时间过长)
  3. Binder 线程数耗尽(默认只有 16 个线程,如果都被占用,新请求只能排队)
  4. Binder 事务缓冲区不足(数据量过大超过 1MB)

日志里会有 waiting to lock 或者 Binder transaction failed 的信息。

避坑指南: 我曾经遇到一个 ANR,traces 里看到主线程在 BinderProxy.transact 上等了好几分钟。后来发现是第三方 SDK 在后台大量使用 Binder 调用来同步数据,把系统 Binder 线程池占满了。解决方案是限制 SDK 的并发数,或者把它的调用放到后台 HandlerThread 里。

原因对比如下表

原因类型内核表现典型 traces 堆栈解决思路
主线程阻塞主线程 Looper 无法处理新消息Handler.handleCallback, View.onDraw, Activity.onCreate耗时操作移出主线程
耗时操作主线程卡在某个方法里很长时间HttpURLConnection.connect, FileOutputStream.write使用线程池或协程
锁竞争主线程在等待 monitor/lock<@@id@ main> ... waiting for <0x...> (a java.lang.Object)减少锁粒度、避免持锁做耗时操作
Binder 超时Binder 调用阻塞在 BinderProxy.transactandroid.os.BinderProxy.transactNative, ServiceManager.getService优化服务端性能、增加 Binder 线程数

以上四种原因经常交织在一起。举个实际例子:主线程正在等待某个锁,而持有锁的线程又在等待一个 Binder 服务端返回——这个 Binder 服务端又因为锁竞争被卡住了。这时候只看主线程堆栈会误以为是锁竞争,其实根因在 Binder 那一端。

核心教训: 分析 ANR 时,别只盯着主线程。一定要看所有活跃线程的堆栈,特别是跟主线程有锁关系或者 Binder 关系的线程。我习惯把 traces.txt 里所有线程的状态先扫一遍,谁在等待谁,谁在运行,一目了然。