4、Input事件超时:输入事件分发路径、系统如何判定超时、KeyEvent/触摸事件ANR案例
4.1 输入事件分发路径
Input 事件从硬件到应用,到底走了多远?很多人以为就是屏幕点一下,直接回调 onTouchEvent。其实背后是一条很长的链路。我把它拆成三个阶段:内核捕获 → Native 分发 → 应用处理。每个阶段都可能卡住,最终引发 ANR。
4.1.1 内核层:硬件事件怎么变成输入流?
触屏或按键按下时,硬件中断触发,内核输入子系统将事件写入 /dev/input/ 设备节点。InputReader 线程通过 epoll 监控这些节点,读取出原始事件数据。
// 内核上报的原始事件格式(struct input_event)
struct input_event {
struct timeval time; // 时间戳
__u16 type; // 类型:EV_KEY / EV_ABS / EV_REL
__u16 code; // 键值或轴编号
__s32 value; // 按下/抬起/坐标值
};
4.1.2 Native 层:InputReader 与 InputDispatcher 的分工
InputReader 负责读取原始数据,将其封装成 KeyEvent 或 MotionEvent,然后扔给 InputDispatcher。InputDispatcher 负责找到当前焦点窗口,把事件派发过去。这两兄弟配合不好,ANR 就来了。
| 组件 | 职责 | 线程 | 关键方法 |
|---|---|---|---|
| InputReader | 读取、解码、分类事件 | InputReaderThread | loopOnce() → processEventsLocked() |
| InputDispatcher | 窗口查找、事件派发、超时监控 | InputDispatcherThread | dispatchEventLocked() → startDispatchCycleLocked() |
| InputChannel | 跨进程传输事件(Socket 对) | — | sendMessage() / receiveMessage() |
| WindowInputEventReceiver | 应用端接收事件入口 | 主线程 Looper | onInputEvent() |
4.1.3 应用层:事件如何到达具体的 View?
ViewRootImpl 中有一个 InputEventReceiver,它把事件从 Native 层拉到 Java 层。然后通过 DecorView 分发给当前 Focused View 或 TouchTarget。
// 应用层接收事件的核心路径
ViewRootImpl$WindowInputEventReceiver.onInputEvent()
→ ViewRootImpl.enqueueInputEvent()
→ ViewRootImpl.processInputEvent()
→ ViewRootImpl.deliverInputEvent()
→ View.dispatchTouchEvent() / View.dispatchKeyEvent()
InputDispatcher 的日志,它记录了每次派发的目标窗口和超时状态,比应用层日志更可靠。
4.2 系统如何判定超时
判定逻辑其实不复杂。InputDispatcher 给每个派发的事件挂一个 5 秒的计时器。5 秒内收不到应用的「处理完成」信号,就判定超时。
4.2.1 关键数据结构:Connection 与 InputChannel
每个窗口对应一个 Connection 对象。InputDispatcher 通过 InputChannel 与应用的 InputEventReceiver 通信。
// InputDispatcher.cpp 中的核心判断
status_t InputDispatcher::startDispatchCycleLocked(
nsecs_t currentTime, const sp<Connection>& commandConnection) {
// 记录派发时间
commandConnection->inputPublisherTouched = true;
commandConnection->inputPublisherWait = currentTime;
// 启动超时监控(5 秒后回调)
setAnrTimeoutsLocked(commandConnection);
return OK;
}
void InputDispatcher::setAnrTimeoutsLocked(
const sp<Connection>& connection) {
nsecs_t timeout = 5 * 1000 * 1000000LL; // 5 秒
connection->responseTime = systemTime() + timeout;
// 向 Looper 注册一个超时回调
mLooper->removeMessages(connection->inputChannel->getFd());
mLooper->addFd(connection->inputChannel->getFd(), ...);
}
4.2.2 超时判定的两条路径
- 分发超时:事件已经通过 InputChannel 发给应用,但应用没有在规定时间内返回
finished信号。这是最常见的 Input ANR 类型。 - 队列积压超时:应用主线程卡住,上一个事件还没处理完,新的 Input 事件在队列里排着,导致后续事件整体超时。说白了,就是「前面堵车,后面的车过不去」。
4.2.3 超时后的处理流程
InputDispatcher 发现超时后,会向 SystemServer 的 InputManagerService 发送 ANR 回调。然后 AMS 开始收集 CPU 信息、各线程堆栈,最终弹出 ANR 对话框。
// InputDispatcher 超时后调用
void InputDispatcher::onAnrLocked(
const sp<Connection>& connection) {
InputManagerService* ims = getInputManagerService();
// 构造 ANR 信息
int32_t pid = connection->inputChannel->getPid();
std::string reason = "Input dispatching timed out";
// 发给 AMS 处理
ims->notifyANR(pid, reason);
}
4.3 KeyEvent / 触摸事件 ANR 案例
4.3.1 案例一:KeyEvent ANR —— 某个按键在 onKeyDown 中做了 I/O
当时我接手一个智能硬件项目,遥控器按返回键经常卡住 5 秒弹出 ANR。看堆栈发现 onKeyDown 里有一段数据库查询,偶尔因为锁竞争执行超过 5 秒。
// 问题代码(简化版)
@Override
public boolean onKeyDown(int keyCode, KeyEvent event) {
if (keyCode == KeyEvent.KEYCODE_BACK) {
// ⚠️ 在主线程做数据库查询,导致 Input 超时
List<String> data = database.queryLargeData();
updateUI(data);
return true;
}
return super.onKeyDown(keyCode, event);
}
解决方式:把数据库查询丢到子线程,用 LiveData 或 Handler 回调更新 UI。同时加了一个「正在退出」的 Toast 让用户有感知。
4.3.2 案例二:触摸事件 ANR —— onTouchEvent 里做网络请求
这其实是一个很经典的错误。我在一个图片编辑 App 里遇到过,用户在画布上拖动时触发 ANR。堆栈指向 MotionEvent.ACTION_MOVE 的分发,主线程卡在 OkHttp 的同步调用上。
// 问题代码(简化版)
@Override
public boolean onTouchEvent(MotionEvent event) {
if (event.getAction() == MotionEvent.ACTION_MOVE) {
// ⚠️ 在主线程做网络请求,致命错误
Response resp = okHttpClient.newCall(request).execute();
processResult(resp.body().string());
return true;
}
return super.onTouchEvent(event);
}
解决方式:网络请求必须走异步。另外我建议在 onTouchEvent 里只做 UI 相关的轻量操作,任何超过 20ms 的工作都应该移到子线程或使用 View.post 延迟处理。
traces.txt 里主线程在干什么。InputDispatcher 的日志会告诉你「等的是哪个窗口的哪个事件」。然后顺着窗口找 View,基本 10 分钟定位。
4.3.3 KeyEvent vs TouchEvent ANR 特征对比
| 对比维度 | KeyEvent ANR | TouchEvent ANR |
|---|---|---|
| 触发场景 | 物理按键(返回、菜单、音量等) | 触摸屏滑动、点击、长按 |
| 典型堆栈 | View.onKeyDown() / Activity.onKeyDown() | View.onTouchEvent() / ViewGroup.dispatchTouchEvent() |
| 超时敏感度 | 单次按键超 5s 即触发 | 连续 Move 事件可能累计超时 |
| 排查方向 | 焦点窗口的 onKeyDown / onKeyUp | TouchTarget 的 onTouchEvent / ScrollView 嵌套 |
| 我遇到的频率 | 偏低(约占 Input ANR 的 20%) | 偏高(约占 Input ANR 的 80%) |
4.4 本章小结
Input 事件超时的根因,说白了就是主线程卡住了,没能及时处理派发过来的事件。事件分发路径虽然长,但每个环节都有日志可查。
- 分发路径:内核 → InputReader → InputDispatcher → 应用主线程 → View
- 超时判定:InputDispatcher 为每个事件设置 5 秒倒计时,超时即 ANR
- 排查要点:先看 InputDispatcher 日志,再看主线程堆栈,最后定位到具体的 View 回调
嗯,这一章就到这里。记住一条:主线程别碰 I/O,别碰锁,别碰网络。做到这三点,Input ANR 基本与你无缘。