第2章:主线程阻塞根因分类:死锁、耗时IO、长时计算、Binder等待、锁竞争

主线程阻塞并非单一问题,而是多个根因的聚合表现。将根因归纳为五类——死锁、耗时IO、长时计算、Binder等待、锁竞争——是ANR治理的第一步。每一种阻塞模式都有其独特的信号特征、采集方法和修复路径。本章以“现象识别→数据采集→原因定位→修复动作→复测→预防”为主线,为每个根因建立排查规则。

根因分类总览

根因类型 本质 典型现象 排查难度
死锁 锁A等锁B,锁B等锁A,线程循环等待 ANR堆栈出现“waiting to lock”闭环 高
耗时IO 主线程直接执行文件读写、数据库事务、网络请求 Disk I/O或SQLite锁等待日志 中
长时计算 O(n²)或O(2ⁿ)复杂操作,大型Bitmap处理 CPU占用高,主线程长时间run 中
Binder等待 主线程跨进程调用后对方挂起或慢响应 “Binder: waiting for reply” 高
锁竞争 高并发下主线程争抢非公平锁导致超时 多条线程阻塞于同一Monitor 中
主线程阻塞根因故障树 主线程阻塞 死锁 耗时IO 长时计算 Binder等待 锁竞争 现象: 无输入无UI更新 定位: waiting to lock 闭环 现象: UI卡顿, 磁盘写入慢 定位: Disk I/O logs / SQLite 现象: CPU100%, 主线程run 定位: Trace堆栈单线程长run 现象: 系统服务超时 定位: binder waiting reply 现象: 多条线程阻塞Monitor 定位: 大量线程阻塞同Monitor 修复:锁顺序调整 + 超时 修复:异步IO / 缩减事务 修复:优化算法 / 异步计算 修复:缓存Binder结果 修复:减小临界区 预防: StrictMode + 代码审查 + 测试

2.1 现象识别:五类阻塞的外在表现

不同的根因会留下不同的“现场痕迹”。以下判断规则帮助你从ANR对话框出现前的用户行为、系统日志关键词快速定位方向。

根因 用户感知 系统日志关键词
死锁 界面完全无响应,触摸无反馈,约5秒后弹出ANR “waiting to lock <...> held by thread X”
耗时IO UI卡顿,点击后延迟响应,或出现白屏 “Slow operation: Disk I/O”“SQLiteException”
长时计算 动画卡顿,点击无反馈,但CPU风扇高转 “cpu times: x.xs” + 主线程被调度器标记为run
Binder等待 启动Service/获取内容提供者时无响应 “Binder: waiting for reply from pid X”
锁竞争 多线程操作共享资源时出现间歇性卡顿 大量线程状态为“blocked / waiting for”

2.2 数据采集:工具与命令

当ANR发生时,立即采集以下数据。采集动作必须在确认ANR后的30秒内执行,以免关键线程状态被回收。

  1. logcat 全量抓取
    命令执行后保存至traces.log,用于后续分析系统服务超时标记。
    adb logcat -b all -d > traces.log
  2. dumpsys activity anr 获取ANR列表
    该命令会输出最近5个ANR的记录,包含发生时间和最核心的trace摘要。
    adb shell dumpsys activity anr
  3. adb shell 获取完整trace文件
    Android 11及以上需要先通过root或adb权限读取/data/anr目录。
    adb shell ls -l /data/anr/    # 查看最近的trace文件
    adb shell cat /data/anr/<traces_file> > anr_trace.txt
  4. 使用StrictMode提前捕获IO/锁违规
    在release包未正式部署前,在debug包中启用StrictMode可提前暴露阻塞风险。
    StrictMode的日志输出位置:android.os.StrictMode: policy violation
    StrictMode.setThreadPolicy(new StrictMode.ThreadPolicy.Builder()
            .detectDiskReads()   // 检测主线程读磁盘
            .detectDiskWrites()  // 检测主线程写磁盘
            .detectResourceMismatches()
            .penaltyLog()        // 仅log不抛异常
            .build());

2.3 原因定位:症状→根因的映射规则

以下“排查表”用于从trace字段快速确定根因类别。判断时优先搜索字符串,无需通读完整堆栈。

trace字段/模式 匹配根因 确认规则
“waiting to lock <...> held by thread X” 且出现第二个线程同样的等待形成循环 死锁 若存在2个及以上线程互相持有对方需要的锁,则为死锁
“at java.io.FileInputStream.read” 或 “at java.io.RandomAccessFile.read” 或 “SQLiteConnection” 耗时IO 主线程堆栈正在执行文件或数据库操作
主线程状态为“RUNNABLE”且连续执行超过10秒,堆栈在循环(如 for、while、Bitmap操作) 长时计算 CPU时间占比高 (>80%),无IO或锁等待
“Binder: waiting for reply from” 或 “BinderProxy.transact” Binder等待 主线程正在等待IPC结果,且目标Binder服务处于挂起状态
多个线程状态为“blocked on monitor”“waiting for object”且锁为同一个地址 锁竞争 锁地址相同,线程数>3,且主线程参与竞争

2.4 修复动作:针对五类根因的定向修复

每种根因的修复策略差异极大。以下为已验证的工程级修复方向。

  1. 死锁修复:保证全局锁顺序一致;引入超时机制(tryLock);或改用无锁数据结构。
  2. 耗时IO修复:主线程中所有磁盘/网络/数据库操作迁移至子线程。使用AsyncTask、HandlerThread或Coroutines;数据库开启WAL模式并合并事务。
  3. 长时计算修复:拆分计算任务到Worker;使用WorkManager;或优化算法,使用SparseArray代替HashMap等。
  4. Binder等待修复:使用IHwBinder异步回调;在收到结果前先显示占位UI;对系统服务调用增加超时控制。
  5. 锁竞争修复:减小临界区;使用ReadWriteLock;使用ConcurrentHashMap或CopyOnWriteArrayList;减少主线程持锁时间。

2.5 复测:验证修复有效性

修复后必须复测。核心方法:恢复ANR触发条件(如快速点击、大文件加载、频繁锁请求),确认以下三点:

  • UI保持60fps,无卡顿感
  • logcat中不再出现上述定位关键词
  • adb shell dumpsys activity anr 列表不再增长
推荐使用Android Vitals或自定义FrameMetricsAggregator来量化卡顿率,作为复测的硬指标。

2.6 预防:将根因扼杀在编码阶段

在项目早期引入系统化的预防机制,能让ANR治理事半功倍。

根因 预防措施 工具/流程
死锁 每次加锁前审查顺序;使用静态代码分析 Detekt / spotbugs规则
耗时IO 编码规范规定:主线程禁止读写超过1KB的数据 StrictMode + 代码门禁(CI)
长时计算 复杂计算自动分配到WorkManager Performance监控 + CI检查
Binder等待 对远程调用的所有路径增加超时保护 Instrumented测试
锁竞争 在CR中使用synchronized和ReentrantLock的静态代码分析 Reviewers训练 + 性能测试
注意:预防最重要的是建立“主线程不能等待”的首位原则。任何主线程上的等待(哪怕是1ms)都是可疑的。在代码Review中,每一个Thread.sleep()、inputStream.read()、bitmap.compress()、synchronized都要被质询是否可以移出主线程。