1. ANR基础概念与系统机制:触发条件、超时类型、主线程消息循环

1.1 现象识别

用户在使用 Android 应用时,如果触摸屏幕或按键后应用超过 5 秒仍未响应,系统会弹出提示框:“应用无响应”,并列出“等待”或“关闭”选项。这就是典型的 ANR(Application Not Responding)表象。

从技术视角看,ANR 的本质是 主线程(UI 线程)被阻塞,无法处理新到达的输入事件或系统广播。阻塞原因通常是主线程上执行了耗时操作:I/O 读写、网络请求、大数据量数据库操作、复杂 Bitmap 处理或死锁。

判断规则 R1:如果用户在操作应用时连续 5 秒以上界面无刷新、点击无响应,且系统弹出 ANR 对话框,即可判定为 ANR 事件。

1.2 数据采集

当 ANR 发生时,系统会在 /data/anr/traces.txt 中记录主线程及所有存活线程的堆栈快照。这是排查 ANR 的核心数据源。使用以下命令提取:

adb pull /data/anr/traces.txt ./traces_anr.log
adb logcat -b events | grep "anr"

同时收集 logcat 中 am_anr 和 cpu 标签的日志,用于判断 ANR 的类型及系统当时的 CPU 负载。

数据源 关键信息 获取命令
traces.txt 主线程堆栈、锁状态、线程状态 adb pull /data/anr/traces.txt
logcat (events) ANR 触发时间、包名、超时类型 adb logcat -b events | grep anr
logcat (system) CPU 负载、进程优先级 adb logcat -b system | grep CPU
采集要点:ANR 发生后应尽快提取 traces.txt,系统在后续 ANR 发生时可能覆盖该文件。优先使用 adb pull 保存现场数据。

1.3 原因定位

触发条件

ANR 由 Android 系统的 超时监控机制(Watchdog) 触发。当主线程在以下时间窗口内未完成指定操作时,系统判定 ANR:

  • 输入事件(InputEvent):5 秒内未完成分发或处理。
  • 广播接收器(BroadcastReceiver):前台广播 10 秒、后台广播 60 秒内未返回。
  • 服务(Service):Service 的 onCreate()、onStartCommand() 或 onDestroy() 在 20 秒内未执行完毕。
  • ContentProvider:onCreate() 超过 10 秒未完成。

三种核心超时类型对比

超时类型 超时阈值 触发场景 典型堆栈特征
InputDispatchingTimeout 5 秒 触摸、按键、轨迹球事件未响应 主线程卡在 View.onTouchEvent() 或 onClick()
BroadcastTimeout 前台 10 秒 / 后台 60 秒 BroadcastReceiver.onReceive() 未及时返回 主线程卡在 onReceive() 内,常有 I/O 或网络调用
ServiceTimeout 20 秒 Service 生命周期方法执行超时 主线程卡在 onStartCommand() 或 onBind()
常见误区:ANR 并不只在主线程“死掉”时才发生。如果主线程做了 4.9 秒的耗时操作后恢复,虽然未触发 ANR,但已经造成用户可感知的卡顿。阈值是系统判定 ANR 的底线,不是编码质量标准。

主线程消息循环机制

Android 主线程启动后进入 Looper.loop() 无限循环,从 MessageQueue 中依次取出消息,交给对应的 Handler 处理。这个循环是 UI 事件、绘制、生命周期回调、广播等全部主线程操作的唯一通道。一旦某个消息处理耗时过长,后续所有消息(包括输入事件、绘制请求)都会排队等待,直接导致 ANR。

// 主线程消息循环简化模型
public static void loop() {
    for (;;) {
        Message msg = queue.next(); // 可能阻塞等待新消息
        msg.target.dispatchMessage(msg); // 处理消息——若这里耗时 > 5s → ANR
        msg.recycle();
    }
}

ANR 故障树分析

从“ANR 发生”顶层事件出发,向下分解的直接原因和根因路径如下:

ANR 发生
├── 主线程消息处理阻塞
│   ├── 【I/O 阻塞】File / Network / SharedPreferences 大量写入
│   ├── 【数据库】SQLite 事务未优化,大数据量查询
│   ├── 【计算】Bitmap 缩放 / JSON 解析 / 正则匹配
│   ├── 【锁竞争】主线程等待子线程锁释放
│   └── 【Binder 调用】同步调用远程服务耗时
└── 超时监控触发
    ├── InputDispatchingTimeout (5s)  — 输入事件未消费
    ├── BroadcastTimeout (10s / 60s) — 广播未返回
    └── ServiceTimeout (20s)          — 服务生命周期未完成
定位捷径:查看 traces.txt 中主线程的堆栈,如果看到 android.os.MessageQueue.nativePollOnce() 表示线程正在空闲等待;如果看到 java.io.*、android.database.sqlite.*、android.os.BinderProxy.transact() 等调用,说明主线程正卡在具体的阻塞操作上。
ANR 触发机制与主线程消息循环架构 用户输入事件 触摸 / 按键 / 轨迹球 → InputManager → 主线程 MessageQueue 主线程消息循环 (Looper + MessageQueue + Handler) MessageQueue Looper.loop() Handler.dispatch() 耗时操作 (>5s) → 阻塞 消息入队 → 循环取消息 → 分发处理 → 若处理时间超过阈值则触发 ANR 系统超时监控 (Watchdog) InputDispatching 5s 超时 BroadcastTimeout 前台 10s / 后台 60s ServiceTimeout 20s 超时 ProviderTimeout 10s 超时 ANR 结果输出 生成 /data/anr/traces.txt 堆栈快照 · 弹出 “应用无响应” 对话框 · 系统可能杀死进程 超时判定 触发 入队 图 1-1 · ANR 触发机制与主线程消息循环关系图。用户输入事件进入 MessageQueue,Looper 循环取消息并由 Handler 处理; 若处理耗时超过 Watchdog 对应阈值,则系统判定 ANR 并输出 traces.txt。

1.4 修复动作

针对主线程阻塞的根因,采取以下 标准修复策略:

  • 耗时操作异步化:使用 HandlerThread、AsyncTask(旧)、Coroutine(Kotlin)或 RxJava 将 I/O、网络、数据库操作移至子线程。
  • 减少主线程计算量:Bitmap 采样加载、JSON 流式解析、避免在 onCreate()/onBind() 中做大量初始化。
  • 锁优化:避免主线程持有锁等待子线程;使用 synchronized 块尽量缩小范围,优先使用 ReentrantLock 带超时。
  • Binder 调用异步:对远程 Service 调用使用 oneway 或异步回调,避免主线程同步等待。
  • BroadcastReceiver 快速返回:在 onReceive() 中仅做状态转发,使用 goAsync() 或 JobService 处理耗时逻辑。
// 错误示例:主线程直接执行耗时 I/O
public void onClick(View v) {
    String data = fileReader.readLargeFile(); // 可能耗时 >5s → ANR
    textView.setText(data);
}

// 正确示例:使用 HandlerThread 异步处理
HandlerThread worker = new HandlerThread("worker");
worker.start();
Handler workerHandler = new Handler(worker.getLooper());
public void onClick(View v) {
    workerHandler.post(() -> {
        String data = fileReader.readLargeFile();
        runOnUiThread(() -> textView.setText(data));
    });
}

1.5 复测

修复后需验证 ANR 是否不再复现。推荐 三阶段验证:

  1. 场景复现测试:在低端机(2GB RAM 以下)上重复触发原 ANR 场景,确认不再弹出 ANR 对话框。
  2. StrictMode 检测:开启主线程磁盘/网络访问检测,确保修复后无违规调用。
  3. Monkey/压力测试:使用 adb monkey 发送 5000 次随机事件,检查是否触发 ANR。
// 启用 StrictMode 检测主线程违规
if (BuildConfig.DEBUG) {
    StrictMode.setThreadPolicy(new StrictMode.ThreadPolicy.Builder()
        .detectDiskReads()
        .detectDiskWrites()
        .detectNetwork()
        .penaltyLog()
        .build());
}
复测判断规则 R2:修复后连续 3 次重复原操作场景均未触发 ANR,且 StrictMode 未输出主线程违规日志,视为修复有效。

1.6 预防

从编码规范层面建立 主线程保护机制:

  • 代码审查清单:所有 Activity.onCreate()、Fragment.onViewCreated()、BroadcastReceiver.onReceive()、Service.onCreate() 中不得包含网络请求、文件 I/O、数据库查询、Bitmap 解码。
  • 线程模型标准化:项目统一使用 Kotlin Coroutine + Dispatchers.IO 或 RxJava Schedulers.io 处理耗时任务,主线程仅做 UI 更新。
  • 性能监控埋点:在关键入口和退出方法加入耗时打点,单次主线程消息处理超过 200ms 即输出警告日志。
  • 自动化检测:CI 流水线集成 blockcanary 或 matrix 框架,自动捕获主线程卡顿和 ANR 堆栈。
预防手段 实施时机 工具/方法
代码审查 MR 合并前 Checklist + 人工 Review
线程模型标准化 项目架构阶段 Coroutine / RxJava 规范文档
性能监控埋点 开发阶段 自定义 Looper 日志 / Choreographer
自动化检测 CI 流水线 BlockCanary / Matrix / StrictMode
核心原则:任何可能超过 16ms 的操作都不应放在主线程。ANR 是卡顿的极端表现,日常开发应以 16ms 一帧(60fps) 为性能基准,主线程单次消息处理不应超过 5ms(输入事件响应)或 16ms(绘制帧)。