引言
在 Android 开发的性能领域,如果说"丢帧"是让用户感到"不爽",那么 ANR (Application Not Responding) 则是让用户感到"绝望"------它直接宣告了交互的死刑 。治理 ANR 不能仅停留在"别在主线程做耗时操作"的表象,而需要深入到 Framework 的埋雷机制 、系统资源的争夺 以及精细化的现场还原分析中去。
本文将带你从系统底层视角,彻底拆解 ANR 的来龙去脉。
一、 ANR 的判定:系统的"埋雷"与"拆雷"
要深刻理解 ANR,必须跳出应用层。在系统进程 system_server 眼里,监控 ANR 就像是在引爆炸弹 。
-
埋下定时炸弹 :当应用进程发起一个 Service 启动或广播发送请求时,
system_server的ActivityManagerService(AMS) 会开启一个倒计时 。 -
正常拆雷 :应用进程在规定时间内干完活(如执行完
onCreate),并及时向system_server报告完成,倒计时取消,警报解除。 -
引爆炸弹:如果倒计时结束仍未收到反馈,AMS 就会判定 ANR,开始封装现场、抓取快照(traces),并根据进程状态决定是弹出对话框还是直接杀掉进程 。
二、 ANR 的四大核心战场与阈值
不同的组件和交互场景,其"炸弹"的引信长度(超时阈值)各不相同 :
| 触发场景 | 超时阈值 (前台/后台) | 核心机制与关键点 |
|---|---|---|
| Input Dispatching | 5s / -- | 唯一具有"扫雷"特性的场景。只有在处理后续事件时发现前一个事件还没干完,才会检测超时 。 |
| BroadcastQueue | 10s / 60s | 串行广播受此限制。只有 onReceive 处理过慢才会引爆 。 |
| Service Timeout | 20s / 200s | 涵盖 onCreate, onStartCommand, onBind 等生命周期 。 |
| ContentProvider | 10s / -- | 主要发生在 Provider 进程启动时的 publish 过程 。 |
面试高阶点:为什么后台进程的阈值长很多?因为后台进程 Adj(优先级)低,分配的 CPU 时间片少,且对用户不可见,系统容忍度更高 。
三、 深度解析:那些隐藏在暗处的"炸弹"
除了常见的 CPU 繁忙导致主线程卡顿外,还有几类极其隐蔽的 ANR 诱因:
1. SharedPreferences (SP) 写入陷阱
这是最坑的一点。很多同学知道 apply() 是异步的,但在 Activity 切换或 Service 停止时,系统为了数据安全会调用 QueuedWork.waitToFinish() 。
-
后果:主线程被迫等待所有异步 SP 任务写入磁盘,如果此时 IO 繁忙,直接引发 ANR 。
-
治理:迁移到 MMKV 或 DataStore。
2. 锁竞争 (Lock Contention)
主线程想要获取一把锁,而该锁正被一个正在进行耗时操作(如读大文件)的后台线程持有着。此时 traces 文件会显示 held by 某线程 。
3. Binder 通信阻塞
主线程调用了一个跨进程接口(如获取某个系统服务信息),而对端进程繁忙或死锁,导致主线程一直处于 NATIVE 状态等待返回 。
四、 案发现场破案:traces.txt 解读指南
当 ANR 发生后,/data/anr/ 下的 traces.txt 是最重要的罪证
1. 确认时间点与进程
检查文件头部的时间戳和进程名,确保找对了现场
- 查看主线程状态
-
RUNNABLE:正在执行代码,通常是复杂的计算、死循环或频繁的 IO 操作 。
-
BLOCKED / MONITOR :在等待锁,看
held by指向谁 。 -
NATIVE:正在进行跨进程 Binder 调用或系统层调用 。
-
WAIT / TIMED_WAIT :处于
Object.wait()或线程挂起状态 。
五、 防患于未然:监控与优化体系
1. 线下严防:StrictMode
在 Debug 阶段开启 StrictMode,一旦主线程检测到磁盘读写、网络请求等违规操作,直接给予警告或崩溃,将风险扼杀在开发阶段 。
2. 线上监控:WatchDog 方案
大厂常用的线上监控方案是开启一个后台线程,每隔一段时间向主线程发一个任务 。
- 原理:如果任务在规定时间内(如 5s)没被执行,说明主线程卡死了。此时后台线程主动 Dump 堆栈并上报大数据 。
3. 巧妙利用 IdleHandler 进行"错峰"初始化
针对冷启动过程中的初始化任务,我们不一定非要挤在 onCreate 中完成。
实战案例 :在 ViewModel 初始化时,通过 Looper.myQueue().addIdleHandler 将耗时的缓存数据加载动作推迟到主线程空闲时执行。
Java
// 利用 IdleHandler 优化,减小主线程启动负荷
Looper.myQueue().addIdleHandler(new MessageQueue.IdleHandler() {
@Override
public boolean queueIdle() {
// 主线程空闲了,执行耗时任务
model.getCachedDataAndLoad();
return false; // 执行一次即移除
}
});
这种做法能显著降低由于启动瞬时负载过高引发的 ANR 概率 .
六、 总结与建议
治理 ANR 是一场关于"空闲"的艺术:
-
减少负载:主线程只做 UI 操作,重活儿全部下放 。
-
警惕 IO :不仅是网络和数据库,SP 的
apply()也是潜伏的杀手 。 -
监控闭环 :通过 WatchDog 抓取线上真实案例,结合
traces文件深挖锁竞争和进程间通信瓶颈 。