ANR 产生原因分析

ANR 说白了就是系统觉得你的 App 卡住了,给了它几秒钟,它还没反应。为什么会这样?我这些年排查下来,基本就三个原因:主线程阻塞、死锁、Binder 调用超时。下面一个一个拆开聊。

快速印象:三个原因都会导致主线程无法及时处理输入事件或执行四大组件的生命周期。区别在于阻塞的源头不同——有的是自己堵住了,有的是被别的线程锁住了,有的是等着远端服务回复。
ANR 三大原因 主线程阻塞 死锁 Binder 超时 UI 渲染耗时 IO 阻塞 锁等待 循环依赖 服务端慢 线程池满 核心逻辑 这些原因最终都会使主线程在5秒内无法完成输入事件的处理

主线程阻塞

这是最直接的原因。主线程本该干轻活——处理触摸事件、绘制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 加超时,别让它一直等。

口诀:锁顺序一致,超时别忘记。如果项目里用了很多synchronized,建议加个线程dump的监控工具定期检查。

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 不一定每次都是同一原因。有时候是组合拳——主线程做了耗时操作,同时还启动了Binder调用,两个因素叠加导致超时。我曾遇到一个例子,主线程先做了数据库查询(阻塞了2秒),然后发起一个Binder调用(又等了3秒),合计超过5秒触发ANR。所以排查时别只盯一个点。

以上就是三种根本原因。记住:只要主线程在规定时间内不能处理完手头任务,系统就判定ANR。至于什么堵住了它,不外乎这三大类。你掌握它们,监控就能有的放矢。