ANR 产生原因分析
ANR 说白了就是系统觉得你的 App 卡住了,给了它几秒钟,它还没反应。为什么会这样?我这些年排查下来,基本就三个原因:主线程阻塞、死锁、Binder 调用超时。下面一个一个拆开聊。
主线程阻塞
这是最直接的原因。主线程本该干轻活——处理触摸事件、绘制UI、运行四大组件生命周期。结果你让它在里面做耗时操作,比如写数据库、网络请求、复杂运算,系统等不了那么久。
我见过一个典型的案子:一个列表页滑动卡顿持续很久,最后弹ANR。查log发现主线程正在做BitmapFactory.decodeStream,那图片文件大得离谱,解压一下就花了几秒。后来改成线程池加载,问题就没了。
诊断时直接看 /data/anr/traces.txt,搜索“main”线程的堆栈。如果你看到类似这种:
"main" prio=5 tid=1 Waiting
...
at java.io.FileInputStream.readBytes(Native Method)
at android.graphics.BitmapFactory.nativeDecodeStream(Native Method)
那么恭喜,找到了元凶。我个人的习惯是一发现主线程有IO操作、网络请求、复杂计算,就立刻拆到子线程。避坑指南:不要以为Annotation里写个@UiThread就以为安全了,不阻塞才算。
死锁
死锁就更有意思了。两个线程互相持有对方需要的锁,谁也不放手。主线程一旦卷进去,就像被困在死胡同里,外边的输入事件只能干等。
我遇到过一次:一个后台线程拿到了锁A,然后去拿锁B。主线程恰好先拿到了锁B,等着锁A。两边一卡,ANR就来了。当时的trace里两个线程都在Object.wait(),看锁ID就能对上。
检测死锁,我喜欢用 Thread.dumpStack() 或者直接取 traces.txt。Android 系统其实会在ANR report里标注“waiting to lock <0x...>”这种信息。举个简单的死锁例子:
// Thread 1
synchronized (lockA) {
Thread.sleep(100);
synchronized (lockB) { }
}
// Thread 2 (main)
synchronized (lockB) {
synchronized (lockA) { }
}
你想想看,主线程和后台线程交换锁的顺序不一致,典型的死锁诱因。我的建议是统一锁顺序,或者用 tryLock 加超时,别让它一直等。
Binder 调用超时
这也是个坑。Binder 是 Android 跨进程通信的核心,主线程经常通过它调用系统服务或者其他进程的服务。如果服务端处理得慢,或者Binder线程池满了,主线程就会卡在 transact() 上。
ANR日志里会出现类似 blocked in Binder: onTransact。我记得之前有个版本,用户频繁拍照,MediaProvider的Binder服务处理不过来,导致相机ANR。后来在服务端做了限流和异步,才兜住。
Binder调用的超时时间分几种:普通调用默认 1秒,但如果主线程在等待系统关键服务(比如AMS、WMS)时,系统的ANR机制会叠加判断。严格来说,一旦主线程被Binder调用阻塞超过5秒,就会引发ANR。
我一般这样排查:看ANR trace里主线程堆栈是否停在 android.os.BinderProxy.transactNative。如果是,再找对端的Binder线程在干嘛。常见的优化手段:
- 把频繁的Binder调用放到子线程(比如用
HandlerThread) - 服务端用异步接口,别让
onTransact里做耗时操作 - 监控Binder线程池是否被打满(默认16个线程)
三种原因速查对比
| 原因 | 典型场景 | 超时时间 | 主要诊断手段 |
|---|---|---|---|
| 主线程阻塞 | UI渲染、IO、大计算 | 输入事件5秒 | traces.txt 主线程堆栈 |
| 死锁 | 锁顺序不一致 | 无固定时间,直到超时 | traces.txt 多个线程等待锁 |
| Binder超时 | 系统服务响应慢、Binder线程满 | 1秒~5秒(取决于服务) | 主线程堆栈 blocked in Binder |
以上就是三种根本原因。记住:只要主线程在规定时间内不能处理完手头任务,系统就判定ANR。至于什么堵住了它,不外乎这三大类。你掌握它们,监控就能有的放矢。