摘要: ANR 从来不是突然出现的意外,而是主线程长期带病运行的结果。本文从"幽门螺杆菌"的病程演化切入,深度拆解 ANR 的四阶段恶化路径,并给出一套架构级的根治方案——Data 层四条红线。不靠优化续命,而是靠隔离断根。
一、写在前面:一个被忽视的“危险信号”
这两年,身边很多中年同事,体检报告里开始出现“幽门螺杆菌(HP)阳性。
一开始大家的反应几乎一模一样:
- “没事吧,好像很多人都有?”
- “医生也没让我立刻治。”
- “先放着吧,最近太忙了。”
听起来是不是很熟?
但真正危险的,从来不是“有没有 HP”,而是这种心态:
“反正现在没症状,先不管。”
HP 的问题就在这里:
- 不会立刻让你疼
- 不会影响你今天的工作
- 甚至几年内都“看起来没事”
但它会在你看不见的地方:
- 持续破坏胃黏膜
- 改变胃部环境
- 一点一点降低你的“健康冗余”
直到某一天:
👉 胃炎 👉 胃溃疡 👉 更严重的问题
你才意识到:
问题从来不是“突然出现”,而是“长期放任”。
然后我意识到一件事:
我们对 ANR 的态度,和对 HP 的态度,一模一样。
在代码里,我们每天都在做类似的选择:
- “这个 IO 放主线程问题不大吧?”
- “这个接口先用同步写,后面再优化。”
- “这个卡顿用户应该感觉不到。”
这些代码,就像 HP:
- 不会立刻出问题
- 不影响你今天上线
- 甚至还能稳定运行一段时间
但它们会:
- 悄悄污染主线程
- 慢慢堆积消息队列
- 降低系统调度能力
直到某一天:
ANR
突然出现在用户面前。
ANR 从来不是一次事故,而是一种“长期带病运行”的结果。
二、病程全解析:ANR 的四阶段演化
1️⃣ 潜伏期:无感阻塞
特征:
- 高端机正常
- Debug 无感
- 测试看不出来
👉 主线程开始被污染
2️⃣ 诱发期:环境放大
| 场景 | 后果 |
|---|---|
| 低端机 | IO 放大 |
| CPU 忙 | 排队 |
| GC | 抖动 |
| 磁盘忙 | 阻塞 |
👉 10ms → 500ms
3️⃣ 萎缩期:掉帧(Jank)
👉 用户开始“感觉不对劲”
4️⃣ 爆发期:ANR
👉 系统强制弹窗 👉 用户唯一操作:关闭
三、真正的转折点:HP 为什么能“根治”?
很多人知道 HP 可以治。
但关键不是吃药。
而是:
必须“分餐制”,否则会反复感染。
👉 药物解决“当前问题” 👉 分餐解决“系统性传播”
这件事非常关键,因为它对应到工程里就是:
你不做架构隔离,ANR 永远会复发。
四、ANR 的“工程化根治”:不是优化,而是“隔离”
很多文章会讲:
- StrictMode
- 协程
- Flow
这些都没错。
但本质上它们只是:
对症治疗
真正的“分餐制”,是这一层:
五、核心升级:Data 层四条红线(真正的根治)
⚠️ 这不是建议,是底线。
🚫 红线一:接口必须统一为 Flow / suspend
🚫 红线二:禁止 BlockingQueue
本质问题:线程阻塞污染协程调度
🚫 红线三:禁止 Java 重锁
本质问题:阻塞线程,破坏调度
🚫 红线四:禁止伪 suspend
本质问题:协程壳 + 阻塞内核
总结一句话:
Data 层必须彻底协程化,否则主线程迟早出问题。
六、更深一层:为什么要“禁止 AQS”?
在协程成为主流之后,我越来越坚定一个观点:
业务层不要显式使用 AQS 同步器。
包括:
ReentrantLockCountDownLatchSemaphoreFutureTasksynchronized
1️⃣ 问题不在 API,而在“层级错位”
AQS 解决的是:
- 线程竞争
- 线程阻塞
而协程解决的是:
- 任务调度
- 挂起恢复
当你在协程里用 ReentrantLock:
本质是在用线程模型解决任务问题
2️⃣ 阻塞 vs 挂起
| 机制 | 行为 |
|---|---|
| ReentrantLock | 阻塞线程 |
| Mutex | 挂起协程 |
👉 最大区别:
是否占用线程
错误后果:
- 线程池耗尽
- 调度抖动
- 假死
- 难排查
3️⃣ 正确并发模型
业务层应该用:
MutexChannelFlowStateFlowasync/await
更进一步:
- actor 模型
- 单线程 dispatcher
- 状态建模
4️⃣ AQS 只属于底层
只允许两种场景:
- 写基础库
- 兼容旧 Java 系统
AQS 是地基,业务代码不应该住在地基里。
总结
HP 可以靠吃药缓解,但不分餐一定复发。ANR 可以靠优化缓解,但不做架构隔离,一定回归。不做隔离的优化,本质上都是“延迟复发”。
参考
并发编程的新篇章:以Kotlin协程告别JUC的重锁与死锁风险