第2章 ANR分类与触发条件:输入事件超时、广播超时、服务超时、ContentProvider超时
阶段一:现象识别
ANR 在用户端的表现并不完全相同。排查的第一步是通过 现象特征 快速缩小范围,判断属于哪一类超时。下表列出了四类 ANR 的用户感知差异与系统弹窗特征。
| ANR 类型 | 用户感知 | 系统弹窗标题 | 日志关键字 |
|---|---|---|---|
| 输入事件超时 | 触摸/按键无响应,屏幕卡死 | "应用无响应" | InputDispatcher: Timeout |
| 广播超时 | 后台操作卡顿,广播未生效 | "广播未在指定时间内完成" | BroadcastQueue: Timeout |
| 服务超时 | 服务启动/绑定卡住,界面跳转失败 | "服务执行超时" | ServiceRecord: Timeout |
| ContentProvider 超时 | 数据加载缓慢,ContentResolver 调用挂起 | "ContentProvider 操作超时" | ProviderRecord: Timeout |
排查切入点: 当用户反馈“点击没反应”或“界面卡死”时,优先怀疑输入事件超时;若反馈“后台任务没执行”或“广播没收到”,优先检查广播与服务超时。
阶段二:数据采集
确认 ANR 现象后,需要立刻采集三类证据:系统 trace、logcat、dropbox 记录。不同 ANR 类型在日志中的标记位置不同。
2.1 采集命令与路径
# 获取 ANR trace 文件(需 root 或 adb 权限)
adb pull /data/anr/traces.txt
adb pull /data/anr/anr_*.txt
# 过滤 ANR 相关 logcat 日志
adb logcat -b all | grep -E "ANR|Timeout|InputDispatcher|BroadcastQueue|ServiceRecord|ProviderRecord" > anr_log.txt
# 查看 dropbox 系统级 ANR 记录
adb shell dumpsys dropbox --print ANR
2.2 各类 ANR 的日志特征
| ANR 类型 | logcat 特征行 | traces.txt 主线程堆栈关键点 |
|---|---|---|
| 输入事件超时 | InputDispatcher: Timeout waiting for focus window | 主线程卡在 View.onDraw()、Layout.inflate() 或 Thread.sleep() |
| 广播超时 | BroadcastQueue: Timeout of broadcast | 主线程卡在 BroadcastReceiver.onReceive() 内的 IO/数据库/网络调用 |
| 服务超时 | ServiceRecord: Timeout executing service | 主线程卡在 Service.onCreate() / onStartCommand() 的耗时操作 |
| ContentProvider 超时 | ProviderRecord: Timeout publishing provider | 主线程卡在 query() / insert() / update() / delete() 的大数据量操作 |
采集守则: 发生 ANR 后 不要立即杀掉应用,保持进程存活 30 秒以上,确保系统完成 trace 堆栈 dump。否则 traces.txt 可能不完整。
阶段三:原因定位
拿到日志与 trace 后,使用 故障树分析法 逐层判断 ANR 类型。以下故障树展示了从根节点到具体触发条件的排查路径。
3.1 触发条件判断规则
使用以下规则表配合 trace 日志,可以快速锁定 ANR 类别:
| 条件编号 | 判断规则 | 证据链 |
|---|---|---|
| R1 | logcat 包含 InputDispatcher: Timeout | 确认输入事件超时 |
| R2 | logcat 包含 BroadcastQueue: Timeout + trace 主线程在 onReceive() | 确认广播超时 |
| R3 | logcat 包含 ServiceRecord: Timeout + trace 主线程在 onCreate()/onStartCommand() | 确认服务超时 |
| R4 | logcat 包含 ProviderRecord: Timeout + trace 主线程在 query()/insert() 等方法 | 确认 ContentProvider 超时 |
| R5 | 同时出现多条超时日志 | 以最先触发的为准,通常是 输入事件超时 覆盖其他类型 |
避坑提示: 输入事件超时往往是“结果”而非“原因”。如果主线程因广播或服务超时卡死,后续的触摸事件必然会触发输入事件超时。排查时务必查看 第一个 Timeout 日志的时间戳,而非最后一个。
3.2 trace 日志实例分析
以下是一段真实的输入事件超时 trace 摘录展示主线程卡在布局加载上:
----- pid 12345 at 2025-01-20 14:23:45 -----
Cmd line: com.example.myapp
"main" prio=5 tid=1 Native
at android.os.MessageQueue.nativePollOnce(Native Method)
at android.os.MessageQueue.next(MessageQueue.java:325)
at android.os.Looper.loop(Looper.java:142)
at android.app.ActivityThread.main(ActivityThread.java:6934)
at java.lang.reflect.Method.invoke(Native Method)
at com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run(RuntimeInit.java:493)
at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:858)
// 主线程正在执行 inflate 操作 —— 典型耗时场景
at android.view.LayoutInflater.inflate(LayoutInflater.java:540)
at com.example.myapp.MainActivity.onCreate(MainActivity.java:45)
...
若日志中主线程堆栈停在 BroadcastReceiver.onReceive() 或 ContentResolver.query(),则可据此直接定位到对应的 ANR 类型。
阶段四:修复动作
根据定位到的 ANR 类型,采取对应的修复策略。下表汇总了四类超时的典型修复方案与代码级操作:
| ANR 类型 | 修复方向 | 具体动作(代码/配置) |
|---|---|---|
| 输入事件超时 | 主线程减负 | 异步 inflate、使用 AsyncLayoutInflater、列表分页加载、复杂计算移至子线程 |
| 广播超时 | 异步化 + 限时 | 在 onReceive() 内调用 goAsync() 并另起线程,或改用 WorkManager/JobScheduler |
| 服务超时 | 生命周期轻量化 | onCreate()/onStartCommand() 中只做初始化,实际任务交给 IntentService 或 WorkManager |
| ContentProvider 超时 | 异步 CRUD | 使用 AsyncQueryHandler 或子线程执行 ContentResolver 操作,避免在主线程直接 query |
4.1 广播超时修复示例(goAsync)
public class SafeReceiver extends BroadcastReceiver {
@Override
public void onReceive(Context context, Intent intent) {
final PendingResult pendingResult = goAsync(); // 关键:转为异步
new Thread(() -> {
try {
// 耗时操作:数据库写入 / 网络请求
handleBackgroundWork(context, intent);
} finally {
pendingResult.finish(); // 必须在 10s/60s 内调用
}
}).start();
}
}
4.2 服务超时修复示例(IntentService)
public class LightweightService extends IntentService {
public LightweightService() {
super("LightweightService");
}
@Override
protected void onHandleIntent(Intent intent) {
// 此方法运行在子线程,不会导致 ANR
processTask(intent);
}
}
修复原则: 任何与 UI 无关的耗时操作(IO、网络、数据库、Bitmap 解码)都必须移到子线程。主线程只负责 测量、布局、绘制 和 事件响应。
阶段五:复测
修复后需要按照 原始触发路径 重新验证,并辅以压力测试确保问题闭环。
- 输入事件超时: 快速连续点击按钮、滑动列表、多次旋转屏幕,使用
adb shell dumpsys input检查事件响应延迟。 - 广播超时: 使用
adb shell am broadcast -a com.example.TEST_ANR发送测试广播,观察 logcat 是否还有 Timeout。 - 服务超时: 使用
adb shell am startservice或绑定服务重复启动,检查 ServiceRecord 超时日志。 - ContentProvider 超时: 使用
adb shell content query --uri content://...模拟大数据量查询,验证是否超时。
每轮测试后检查 /data/anr/traces.txt 是否生成新的 ANR 记录,同时监控 logcat 中 Timeout 关键字出现次数。
阶段六:预防
将以下预防机制纳入开发流程与 CI 门禁,从源头减少 ANR 风险:
| 预防措施 | 适用范围 | 落地方式 |
|---|---|---|
| StrictMode 主线程检测 | 所有 ANR 类型 | 在 Application.onCreate() 中开启,检测磁盘 IO / 网络 / 慢操作 |
| 异步架构强制 | 广播 / 服务 / Provider | Code Review 禁止在主线程执行 IO 和网络请求 |
| ANR 监控 SDK | 线上全量 | 接入 Matrix、BlockCanary 或自研 ANR 监控,采集主线程堆栈 |
| 超时熔断 | ContentProvider | 为 CRUD 操作设置超时阈值,超时后降级返回缓存或默认值 |
| 压力测试与 Monkey | 输入事件 | CI 中加入 Monkey 测试 + 自定义 ANR 检测脚本 |
核心预防理念: ANR 的本质是“主线程被阻塞超过阈值”。只要保证主线程的 Looper 循环不被耗时操作打断,四类超时均可避免。将异步编程、线程隔离、监控告警刻入团队规范,比事后修复成本低 10 倍。