第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 类型。以下故障树展示了从根节点到具体触发条件的排查路径。

ANR 分类故障树 ANR 触发条件故障树 ANR 发生 输入事件超时 广播超时 服务超时 ContentProvider 超时 主线程 Input 事件 处理超过 5s onReceive() 前台 >10s 后台 >60s 未返回 关键生命周期前台 >20s 后台 >200s CRUD 操作 超过 10s 布局加载 / 列表刷新 复杂计算 / 同步网络 磁盘 IO / 数据库操作 远程请求 / 文件下载 资源等待 / 锁竞争 死锁 / Binder 阻塞 大数据量查询 文件 IO 无缓冲

3.1 触发条件判断规则

使用以下规则表配合 trace 日志,可以快速锁定 ANR 类别:

条件编号判断规则证据链
R1logcat 包含 InputDispatcher: Timeout确认输入事件超时
R2logcat 包含 BroadcastQueue: Timeout + trace 主线程在 onReceive()确认广播超时
R3logcat 包含 ServiceRecord: Timeout + trace 主线程在 onCreate()/onStartCommand()确认服务超时
R4logcat 包含 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 / 网络 / 慢操作
异步架构强制广播 / 服务 / ProviderCode Review 禁止在主线程执行 IO 和网络请求
ANR 监控 SDK线上全量接入 Matrix、BlockCanary 或自研 ANR 监控,采集主线程堆栈
超时熔断ContentProvider为 CRUD 操作设置超时阈值,超时后降级返回缓存或默认值
压力测试与 Monkey输入事件CI 中加入 Monkey 测试 + 自定义 ANR 检测脚本
核心预防理念: ANR 的本质是“主线程被阻塞超过阈值”。只要保证主线程的 Looper 循环不被耗时操作打断,四类超时均可避免。将异步编程、线程隔离、监控告警刻入团队规范,比事后修复成本低 10 倍。