ANR 产生原因分析
各位好,今天我们来聊聊 ANR 产生的根本原因。说白了就是主线程在 5 秒内没干完该干的活。为什么会这样?我把它归纳成四类:主线程阻塞、耗时操作、锁竞争、Binder 调用超时。下面一个个拆开看。
1. 主线程阻塞
这是最常见的 ANR 原因。主线程(UI 线程)负责处理用户输入、绘制界面、分发事件。如果它被某个工作卡住超过 5 秒,系统就会弹 ANR 对话框。
常见阻塞场景:
- Handler.post 大量任务: 比如某个监听器回调里做了很多计算,或者连续 post 了几百个延迟消息。
- View 树递归遍历: 某些自定义 View 的 onDraw 或者 onLayout 里做了耗时操作,比如解析大图片、递归遍历所有子 View。
- Activity 生命周期回调: onCreate、onResume 里直接做网络请求或数据库查询。
2. 耗时操作
直接在主线程里执行了长时间的任务。包括但不限于:
- 网络请求(HttpURLConnection、OkHttp 同步调用)
- 文件 I/O(SharedPreferences commit、文件读写、数据库查询)
- 复杂计算(JSON 解析、加密解密、正则匹配)
- Bitmap 解码(尤其是大分辨率图片)
每个操作可能只有几十毫秒,但叠加起来就容易超过 5 秒。你想想看,一个接口回调里先读 SharedPreferences,再解析 JSON,再写文件——不知不觉就几百毫秒了。如果用户连续操作,主线程消息队列越堆越长,最终 ANR。
3. 锁竞争
这属于比较隐晦的原因。主线程试图获取一个对象锁,但该锁被其他线程长时间持有,主线程只能干等。常见的锁竞争模式:
- 死锁: 两个线程互相等待对方释放锁。
- 锁被长耗时任务持有: 比如工作线程在 synchronized 块里做网络请求,主线程刚好也需要这个锁。
- ReentrantLock 未及时 unlock: 异常分支没有释放锁。
怎么排查?看 traces.txt 里主线程的堆栈,如果停在 Object.wait() 或 LockSupport.park() 上,基本就是锁竞争。再看看其他线程在干什么。
4. Binder 调用超时
系统服务通信走的都是 Binder 机制。比如执行 startActivity、bindService、获取系统服务、跨进程数据传递等。如果服务端处理太慢,或者 Binder 线程池耗尽,主线程的 Binder 调用就会堵住。
具体场景:
- 系统服务繁忙(如 PackageManager 正在安装应用)
- 服务端死锁或运行慢(比如 ContentProvider 查询数据库时间过长)
- Binder 线程数耗尽(默认只有 16 个线程,如果都被占用,新请求只能排队)
- Binder 事务缓冲区不足(数据量过大超过 1MB)
日志里会有 waiting to lock 或者 Binder transaction failed 的信息。
原因对比如下表
| 原因类型 | 内核表现 | 典型 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.transact | android.os.BinderProxy.transactNative, ServiceManager.getService | 优化服务端性能、增加 Binder 线程数 |
以上四种原因经常交织在一起。举个实际例子:主线程正在等待某个锁,而持有锁的线程又在等待一个 Binder 服务端返回——这个 Binder 服务端又因为锁竞争被卡住了。这时候只看主线程堆栈会误以为是锁竞争,其实根因在 Binder 那一端。