常见 ANR 原因:主线程耗时操作、锁竞争、Binder 调用阻塞

干 Android 这么久,我排查过的 ANR 少说也有上百个了。总结下来,90% 的 ANR 都跑不出这三个坑:主线程干了不该干的活、锁抢得死去活来、Binder 通信卡在半路。今天咱们就掰开揉碎聊一聊,每一类我会带上我踩过的坑。

主线程耗时操作

这是最基础,也是出现频率最高的 ANR 原因。说白了,主线程就是 UI 线程,它只能专心做一件事:处理界面刷新和用户交互。你要是往上面塞文件读写、网络请求、大数据解析,它哪还有空管你的触摸事件?

我印象最深的是某次直播项目,用户反馈进直播间经常 ANR。我一行行查代码,发现业务同学直接在 onCreate 里调用了一个图片压缩工具类——那个工具类内部用 Bitmap.compress 同步写文件,一次压缩耗时 300~500ms。你想想看,一个页面还加载其他东西,几帧没响应,系统直接就报警了。

主线程上的典型耗时操作
  • 网络请求(HttpURLConnection、OkHttp 同步调用)
  • 文件 I/O(SharedPreferences 的 apply/commit、数据库读写、文件复制)
  • Bitmap 解码 & 大图处理
  • JSON/XML 解析(尤其多层嵌套大对象)
  • 循环计算或排序大量数据
排查技巧
用 StrictMode 可以秒杀这类问题。打开 StrictMode.ThreadPolicy.Builder().detectAll().penaltyLog(),所有主线程上的磁盘读写、网络访问都会打日志。我个人习惯在 Debug 包直接 penaltyDeath(),不给我留任何侥幸。

锁竞争

多线程这把双刃剑,用不好就捅自己。锁竞争导致的 ANR 往往比主线程耗时更隐蔽——你代码里没有明显的耗时操作,但线程就是卡住了。原因很简单:主线程在等一把锁,而拿锁的那个子线程在磨洋工。

我曾经遇到过这样一个 case:一个日志 SDK 用了 synchronized 保证写日志线程安全,但它的写入方法里包含了文件 I/O 和上传逻辑。主线程在某个地方调了这个 SDK 的 getLogCount() 方法,恰好那个方法加了同一把锁。结果主线程被卡了十几秒……嗯,后来我把写日志的逻辑全部挪到子线程,再把锁的粒度拆细,才算解决。

避坑指南
我后来给自己定了一条铁律:主线程上不获取任何可能被子线程长期占用的锁。如果你的锁需要同时保护 UI 和后台任务,就用 ReadWriteLock 或者 ReentrantLock 的 tryLock(timeout) 加超时——别让主线程死等。

Binder 调用阻塞

Binder 是 Android 进程间通信的命脉,但也经常成为 ANR 的元凶。系统服务(ActivityManagerService、WindowManagerService)都是运行在独立进程里的,主线程调用它们的方法本质上是同步 Binder 事务。如果服务端响应慢,或者你传的数据太大,主线程就卡了。

我遇到过最夸张的一次:某应用在 onResume 里调了 ActivityManager.getProcessMemoryInfo(),传了一个很大的进程列表。这个方法底层会走 Binder 到 AMS,AMS 又要遍历所有进程的内存信息——整个调用耗时接近 2 秒。系统一看主线程没响应,直接弹 ANR 对话框。

常见的 Binder 阻塞场景
  1. 频繁调用系统服务(如 getPackageManager().getInstalledPackages())
  2. 跨进程传递超大数据(超过 1MB 的 Bitmap 或 Bundle)
  3. Service 的 onBind 或 onStartCommand 中执行耗时操作
  4. ContentProvider 的 query / insert 在主线程被调用
个人建议
对于系统服务的调用,能用异步就用异步(比如 PackageManager.queryIntentActivities 改为 ContentResolver 的异步查询)。如果必须同步,加上 handler.postDelayed 兜底,或者预先缓存结果。我自己习惯在 onCreate 中把需要的数据提前拉好放到子线程,而不是等用户点击时才去触发 Binder 调用。

三类 ANR 原因对比

类别 典型信号 定位方法 常见突破口
主线程耗时 Trace 中主线程长时间执行一个方法 StrictMode / Profile GPU 把耗时操作扔到子线程或用协程
锁竞争 主线程 stuck 在 Object.wait() 或 LockSupport.park() 抓取线程快照看锁持有方 拆锁、换读写锁、加超时
Binder 阻塞 主线程处于 Binder:br_ 状态 adb shell dumpsys activity 或 dumpsys cpuinfo 异步化、缓存、减少跨进程调用
ANR 三大诱因分类 主线程耗时操作 I/O & 网络 运算 & 解码 锁竞争 死锁 长时间等待 Binder 调用阻塞 服务端耗时 大数据传输 排查思路 抓取 traces.txt → 定位主线程状态 → 逐类排除

说来说去,ANR 的本质就是「主线程被堵住了」。你只要记住这一点,排查起来就有方向。我个人觉得,与其等到线上反馈再慌张,不如在开发阶段就养成好习惯:所有非 UI 操作一律交给子线程,所有跨进程调用评估成本,所有锁加上超时。嗯,做到这三点,大部分 ANR 根本不会出现。

一句话总结
主线程别碰 IO 和网络,锁别让主线程等,Binder 调用能异步就别同步。这三条红线,踩了就 ANR。