3. 输入事件 ANR:触摸事件分发超时与 InputDispatcher 调度流程

3.1 现象识别

触摸事件 ANR 是用户感知最强烈的 ANR 类型——用户手指触碰屏幕后 App 没有给出任何视觉或触觉反馈。常见表现:

  • 点按无响应:触摸屏幕后按钮无高亮、界面无跳转、无震动反馈。
  • 滑动卡死:手指滑动后列表不滚动,画面持续静止超过 5 秒。
  • ANR 对话框弹出:系统弹出“应用无响应”提示,标题明确标注 Input dispatching timed out。
  • 日志关键字:ANR in ... + InputDispatcher: Timeout + Waiting because no window has focus 等。
典型日志摘录(/data/anr/traces.txt):
ANR in com.example.app (com.example.app/.MainActivity)
  Reason: Input dispatching timed out (Waiting because no window has focus)
  ...
  CPU usage: 8% total, 5% user, 3% kernel, 0% I/O
  "main" prio=5 tid=1 Runnable
    at com.example.app.MainActivity.onTouchEvent(MainActivity.java:120)
    - locked <0x0a1b2c3d> (a java.lang.Object)
    at android.view.View.dispatchTouchEvent(View.java:14200)

关键信号:Input dispatching timed out + 主线程在 onTouchEvent 中持锁阻塞。

3.2 数据采集——捕获输入调度状态

当触摸 ANR 复现后,第一时间采集以下三类数据:

数据源命令/路径采集的关键信息
Input 调度快照adb shell dumpsys input当前窗口焦点、InputDispatcher 队列深度、等待窗口、超时计数
窗口层次adb shell dumpsys window windows焦点窗口包名、是否有窗口遮挡、窗口可见性
ANR trace/data/anr/traces.txt主线程堆栈、锁等待、Binder 调用、CPU 使用率
事件流水日志adb logcat -b events | grep am_anrANR 触发时间、包名、进程 PID、reason 字段
采集时机建议:ANR 对话框出现后 不要立即点“等待”,应先在 3 秒内执行 dumpsys input 和 dumpsys window windows,否则焦点窗口可能已被系统重置。

3.3 原因定位——InputDispatcher 调度流程与 5 秒超时策略

输入事件 ANR 的根因存在于 InputDispatcher 的调度链路中。系统从触摸事件产生到 ANR 触发,共经历 6 个关键阶段:

  1. 事件采集:InputReader 从驱动读取原始触摸事件,封装为 MotionEvent。
  2. 事件排队:事件进入 InputDispatcher 的 mInboundQueue,等待分发。
  3. 窗口查找:InputDispatcher 通过 mWindowHandle 找到当前焦点窗口,检查窗口是否注册了 InputChannel。
  4. 事件分发:通过 InputChannel 将事件写入 socket,通知目标窗口处理。
  5. 启动超时:分发同时设置一个 5 秒的 Alarm(INPUT_TIMEOUT),若超时前未收到 finished 信号则触发 ANR。
  6. 等待完成:App 主线程通过 View.dispatchTouchEvent 处理事件,处理完毕通过 InputChannel.sendFinishedSignal 回执。
核心规则:5 秒超时是 壁钟时间,不是 CPU 时间。即使 App 在后台或被其他进程抢占,只要事件处理超过 5 秒(wall clock)就触发 ANR。这与 Service 超时(20 秒/200 秒)有本质区别。
InputDispatcher 调度流程与 5 秒超时触发故障树 InputDispatcher 调度流程与 5 秒超时触发故障树 ① InputReader 采集触摸事件 ② mInboundQueue 排队等待分发 ③ 窗口查找 mWindowHandle 匹配 ④ 分发 + 5s 闹钟 InputChannel.send + Alarm ⑤ 主线程处理 View.dispatchTouchEvent ⑥ finished 信号 取消 Alarm → 正常 5秒到? 🚫 ANR 触发 InputDispatcher ANR 是 ✅ 正常 事件完成 否 ⚡ 根因故障树: 主线程阻塞 → 未及时发送 finished 信号 → 5s Alarm 触发 → InputDispatcher 判定 ANR 常见阻塞点:Sync barrier · Binder 长调用 · 锁竞争 · 布局递归

3.4 修复动作——从阻塞点到解决

根据故障树排查结果,针对性修复:

阻塞类型根因定位依据修复动作
Sync barrier 未释放trace 中 ViewRootImpl.doTraversal 长时间等待 mTraversalBarrier检查 View.post(Runnable) 是否在主线程死循环发送;改用 HandlerThread 执行
Binder 同步调用trace 中 BinderProxy.transact 处于 BLOCKED 状态将 Binder 调用改为异步(oneway)或迁移到工作线程
锁竞争trace 显示主线程持有锁并在等待另一把锁(waiting to lock)减少主线程加锁范围;使用 ReadWriteLock 或 StampedLock 降低竞争
布局/绘制递归trace 主线程在 View.measure 或 View.layout 中循环超过 5 秒优化嵌套层级(扁平化 ConstraintLayout);对复杂列表使用 RecyclerView 预加载
文件 I/O 或网络同步trace 显示 FileInputStream.read 或 OkHttp.execute 在主线程强制所有 I/O 和网络请求走 AsyncTask / Coroutine(Dispatchers.IO) / RxJava
快速修复对照脚本(Kotlin):
// 错误:主线程直接做网络请求
fun onTouchEvent(ev: MotionEvent): Boolean {
    val response = OkHttpClient().newCall(request).execute()  // 阻塞 5s 以上 → ANR
    return true
}

// 修复:切换 IO 协程
fun onTouchEvent(ev: MotionEvent): Boolean {
    CoroutineScope(Dispatchers.IO).launch {
        val response = OkHttpClient().newCall(request).execute()
        withContext(Dispatchers.Main) { updateUI(response) }
    }
    return true
}

3.5 复测——验证修复是否阻断超时

修复后必须执行三轮复测,确保修复有效且无退化:

  1. 压力复测:使用 adb shell monkey --pct-touch 100 -v 5000 向目标 App 发送 5000 次触摸事件,同时采集 dumpsys input 检查 DispatchStatistics 中 timeout 计数是否为 0。
  2. 边界复测:模拟弱网 + 低内存场景(adb shell am set-inactive --user 0 package + procrank 压内存),验证触摸事件仍能在 5 秒内完成。
  3. 回归复测:运行 dumpsys input 检查 mGlobalKeyState 和 mKeyRepeatState 无异常,且 ANR 关键字在 logcat 中不再出现。
复测通过标准: adb shell dumpsys input | grep "DispatchStatistics" 输出中 timeoutCount=0,且 ANR in 日志不再弹出。

3.6 预防——建立输入 ANR 监控防线

在开发与测试阶段植入三道防线:

防线工具/方法拦截时机
第一道:静态分析Lint 规则 + Android Studio 主线程检查插件,禁止在主线程调用 execute()、read()、write()编译阶段
第二道:动态监控接入 StrictMode,捕获主线程磁盘 I/O 和网络访问;BlockCanary 监控 UI 帧耗时开发自测 / CI
第三道:线上告警集成 Firebase Performance 或自研 ANR 采集,设置 InputDispatcher 超时事件的实时告警线上灰度/全量
避坑提醒:不要仅依赖 ANR 对话框出现才采集数据。线上环境建议在 InputDispatcher 超时前 4.5 秒(即分发后立即启动一个 4.5 秒的 watchdog)预采集主线程堆栈,比系统 ANR 更早拿到阻塞点。

3.7 排查速查表——触摸 ANR 一键判断

现场复现后,按以下规则表快速定位:

现场现象dumpsys input 关键字段指向根因优先级
触摸后无反馈,CPU 低mPendingEvent != null + mWindowHandle == null窗口焦点丢失(窗口不可见或被遮挡)高
触摸后无反馈,CPU 高mInboundQueue.size() > 5 + 主线程在 measure布局递归 / 过度绘制高
触摸后无反馈,CPU 中mActiveInputEventList 中事件状态为 PENDING主线程被锁阻塞 / Binder 同步中
触摸后反馈延迟但最终完成DispatchStatistics 中 timeoutCount>0 但 ANR 未触发接近超时边缘(4.2~4.9 秒)中
多指触摸/手势冲突mCurrentMotionEvent 有异常指针数或 action 序列乱序自定义 View 中 onTouchEvent 未正确消费事件低
一句话总结:触摸 ANR 的根因永远落在“主线程在 5 秒内未能完成事件处理并发送 finished 信号”,排查时从 trace 中找出主线程在 onTouchEvent / onClick / onScroll 中的阻塞点,一刀砍掉 I/O、锁、Binder 同步即可根治。