主线程阻塞:常见原因与排查思路归纳总结

说白了,ANR 就是主线程被卡住太久——超过 5 秒(输入事件)或 10 秒(广播)没响应。但具体是谁卡住的?怎么快速揪出来?这才是我们日常要解决的问题。我个人习惯先把常见原因分成几类,然后逐个击破。

一、你猜谁最常干这事?

原因分类 典型场景 发生频率
I/O 阻塞 在主线程读文件、写数据库、网络请求 极高
锁竞争 主线程持锁等待子线程释放锁,或死锁 较高
长耗时计算 大图解码、JSON 解析、正则匹配 中
Binder 调用慢 跨进程调用系统服务(如 AMS、WMS)超时 中
对象泄漏导致 GC 频繁 持有大量 Activity 引用,GC 反复 STW 高(启动/页面切换时)

我在项目中遇到最多的,其实是 I/O 阻塞。很多同学觉得 SharedPreferences 很快,但它 commit() 是同步写磁盘的,直接在主线程调就炸了。

⚠️ 注意:千万别把网络请求、大文件读写、Bitmap 解码放在主线程。就算用上了协程,如果 Dispatchers.Main 里混入阻塞代码,一样 ANR。

二、锁竞争:一个经典的死锁案例

先看一段我实际遇到的代码。当时线上 ANR 集中在某个页面,堆栈卡死在这儿:

// Thread A (主线程)
synchronized (lockA) {
    // 做一些初始化...
    synchronized (lockB) {
        // 调用子线程的结果
    }
}

// Thread B (子线程)
synchronized (lockB) {
    // 异步任务处理完...
    synchronized (lockA) {
        // 回调主线程
    }
}

嗯,典型的交叉加锁。主线程拿了 lockA 在等 lockB,子线程拿了 lockB 在等 lockA,谁也等不到谁。主线程就这样被挂起超过 5 秒,ANR 就来了。

排查这类问题,我一般这么搞:

  1. 先看 traces.txt 里主线程的状态——如果一直是 BLOCKED 或 WAITING,大概率是锁。
  2. 再看它等待的锁对象是什么,找到持有锁的那个线程。
  3. 如果那个线程也是 BLOCKED,恭喜你,找到死锁了。
💡 个人建议:加锁时尽量固定顺序(比如总是先小锁后大锁),或者用 tryLock 加超时。我曾经在一个 SDK 里遇到类似问题,后来改成 ReentrantLock 配合 TimeUnit 解决了。

三、Binder 调用慢:系统服务堵车

你以为只有自己写的代码会卡主线程?错了。系统服务也常背锅。比如启动 Activity 时 AMS 的跨进程调用,或者获取包名、安装状态等。如果系统服务长时间不返回,主线程就卡在 Binder 线程上。

怎么判断?看 traces.txt 中主线程的堆栈——如果停在 BinderProxy.transact() 或 Parcel.readException(),那就是 Binder 调用超时。我在项目中遇到过某定制 ROM 的帐号管理服务响应慢,导致登录页 ANR,最后只能加个超时兜底。

核心思路:主线程阻塞的根因,要么是它自己在干重活儿,要么是它在等别人。我们要做的是——找到那个“别人”是谁。

四、排查路线图:一张图说清楚

下面这张图是我平时排查 ANR 主线程阻塞的思维路径。从拿到 traces 开始,到确认根因,一目了然。

主线程阻塞排查流程图 拿到 /data/anr/traces.txt 主线程 state = ? BLOCKED RUNNABLE 锁竞争 / 死锁 长耗时 / I/O 检查等待的锁对象 找到持有锁的线程 分析堆栈调用 定位耗时方法 确认根因:主线程到底卡在哪? 修复方案:异步 / 减少锁 / 加超时

五、避坑指南:我踩过的几个坑

  • 共享参数提交:Editor.commit() 是同步写磁盘的。我曾经在一个列表页的 Item 点击监听里调了 commit,结果用户快速点两下就 ANR。后来全换成 apply() 异步提交了。
  • WebView 的同步回调:比如 onPageFinished 里做大量字符串拼接。WebView 的回调其实是在主线程,别在里面磨叽。
  • ContentProvider 的 query:跨进程 Binder 调用,如果对手方耗时久,主线程只能干等。我建议加一个 Binder 线程池的 timeout 机制。
  • 大图直接显示:ImageView 设置 Bitmap 前,decode 就在主线程。超过 2 秒的 decode 就要用 BitmapFactory.Options 配合子线程。
一句话总结:主线程阻塞,说白了就是它没闲着——要么在等别人释放锁,要么自己在执行重量级操作。拿到 traces 文件后第一眼看线程状态,第二眼看堆栈,第三眼找持锁方。理清这三步,80% 的 ANR 都能定位。