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_anr | ANR 触发时间、包名、进程 PID、reason 字段 |
采集时机建议:ANR 对话框出现后 不要立即点“等待”,应先在 3 秒内执行
dumpsys input 和 dumpsys window windows,否则焦点窗口可能已被系统重置。
3.3 原因定位——InputDispatcher 调度流程与 5 秒超时策略
输入事件 ANR 的根因存在于 InputDispatcher 的调度链路中。系统从触摸事件产生到 ANR 触发,共经历 6 个关键阶段:
- 事件采集:InputReader 从驱动读取原始触摸事件,封装为
MotionEvent。 - 事件排队:事件进入 InputDispatcher 的
mInboundQueue,等待分发。 - 窗口查找:InputDispatcher 通过
mWindowHandle找到当前焦点窗口,检查窗口是否注册了InputChannel。 - 事件分发:通过
InputChannel将事件写入 socket,通知目标窗口处理。 - 启动超时:分发同时设置一个 5 秒的 Alarm(
INPUT_TIMEOUT),若超时前未收到finished信号则触发 ANR。 - 等待完成:App 主线程通过
View.dispatchTouchEvent处理事件,处理完毕通过InputChannel.sendFinishedSignal回执。
核心规则:5 秒超时是 壁钟时间,不是 CPU 时间。即使 App 在后台或被其他进程抢占,只要事件处理超过 5 秒(wall clock)就触发 ANR。这与 Service 超时(20 秒/200 秒)有本质区别。
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 复测——验证修复是否阻断超时
修复后必须执行三轮复测,确保修复有效且无退化:
- 压力复测:使用
adb shell monkey --pct-touch 100 -v 5000向目标 App 发送 5000 次触摸事件,同时采集dumpsys input检查DispatchStatistics中 timeout 计数是否为 0。 - 边界复测:模拟弱网 + 低内存场景(
adb shell am set-inactive --user 0 package+procrank压内存),验证触摸事件仍能在 5 秒内完成。 - 回归复测:运行
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 同步即可根治。