第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 | 中 |
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秒内执行,以免关键线程状态被回收。
- logcat 全量抓取
命令执行后保存至traces.log,用于后续分析系统服务超时标记。adb logcat -b all -d > traces.log - dumpsys activity anr 获取ANR列表
该命令会输出最近5个ANR的记录,包含发生时间和最核心的trace摘要。adb shell dumpsys activity anr - 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 - 使用StrictMode提前捕获IO/锁违规
在release包未正式部署前,在debug包中启用StrictMode可提前暴露阻塞风险。
StrictMode的日志输出位置:android.os.StrictMode: policy violationStrictMode.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 修复动作:针对五类根因的定向修复
每种根因的修复策略差异极大。以下为已验证的工程级修复方向。
- 死锁修复:保证全局锁顺序一致;引入超时机制(
tryLock);或改用无锁数据结构。 - 耗时IO修复:主线程中所有磁盘/网络/数据库操作迁移至子线程。使用
AsyncTask、HandlerThread或Coroutines;数据库开启WAL模式并合并事务。 - 长时计算修复:拆分计算任务到
Worker;使用WorkManager;或优化算法,使用SparseArray代替HashMap等。 - Binder等待修复:使用
IHwBinder异步回调;在收到结果前先显示占位UI;对系统服务调用增加超时控制。 - 锁竞争修复:减小临界区;使用
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都要被质询是否可以移出主线程。