一、窗口输入事件分发:InputManagerService 与 WMS 的协作
各位同学,今天我们来聊聊 Android 系统中一个非常核心的环节——窗口输入事件分发。说白了,就是你的手指在屏幕上点了一下,这个触摸事件是怎么找到它应该去的那个窗口的?
我个人习惯把这件事分成三个层面来看:谁在管输入设备、谁在管窗口、以及它们怎么配合。前两个角色分别是 InputManagerService(IMS)和 WindowManagerService(WMS)。
1.1 IMS 与 WMS 的分工
IMS 负责从内核读取原始输入事件。它不关心屏幕上有什么窗口,只关心「哪个设备产生了什么事件」。而 WMS 正好相反,它维护着窗口的层级、位置、大小,知道「哪个窗口在哪个区域」。
你想想看,这两个服务如果不配合,事件就不知道该发给谁。所以 Android 设计了一套协作机制:IMS 问 WMS 要窗口信息,WMS 告诉 IMS 事件该发给谁。
核心流程:
- IMS 从 /dev/input 读取原始事件
- IMS 通过 InputDispatcher 查找目标窗口
- WMS 提供窗口的 hit-test 结果(点击测试)
- IMS 将事件写入目标窗口的 InputChannel
我在项目中遇到过一个问题:某个定制设备上触摸不灵敏,排查了半天发现是 IMS 和 WMS 之间的通信延迟太高,导致事件分发超时。嗯,这里要注意,IMS 和 WMS 运行在同一个进程(system_server),但它们的通信走的是 Binder 调用,如果 Binder 线程池被占满,就会出问题。
1.2 协作的关键:WindowHandle
WMS 会为每个可见窗口创建一个 InputWindowHandle 对象。这个对象包含了窗口的 token、位置、大小、是否可触摸等信息。IMS 的 InputDispatcher 会维护一个 mWindowHandles 列表,每次事件到来时,它遍历这个列表做 hit-test。
// frameworks/native/services/inputflinger/InputDispatcher.cpp
// 伪代码示意
void InputDispatcher::dispatchOnce() {
// 1. 从 WMS 获取最新的窗口列表
Vector<InputWindowHandle*> windowHandles;
mWindowManagerService->getWindowHandles(&windowHandles);
// 2. 对每个事件做 hit-test
for (const auto& entry : mInboundQueue) {
findTargetWindow(entry, windowHandles);
}
}
这里有个细节:WMS 更新窗口列表是异步的。也就是说,当你打开一个新窗口时,WMS 会立刻更新自己的数据结构,但 IMS 可能还在用旧的窗口列表做 hit-test。这会导致短暂的事件丢失或错发。我曾经调试过一个 bug,就是快速切换应用时触摸事件被发给了上一个窗口,原因就在这里。
二、InputChannel 的建立
好,现在 IMS 知道了事件该发给哪个窗口,但怎么发过去呢?这就引出了 InputChannel。
InputChannel 本质上是一个 Socket 对(在 Android 中用的是 Unix Domain Socket)。一端在 IMS 的 InputDispatcher 手里,另一端在目标窗口所在的进程(通常是应用进程)手里。
2.1 建立过程
这个过程发生在窗口添加时。WMS 的 addWindow 方法会调用 WindowState.openInputChannel:
// frameworks/base/services/core/java/com/android/server/wm/WindowState.java
void openInputChannel(InputChannel outInputChannel) {
// 1. 创建一对 InputChannel
InputChannel[] channels = InputChannel.openInputChannelPair(name);
// 2. 将服务端通道注册到 IMS
mService.mInputManager.registerInputChannel(channels[0], mInputWindowHandle);
// 3. 将客户端通道返回给应用
channels[1].transferTo(outInputChannel);
// 4. 保存服务端通道引用
mInputChannel = channels[0];
}
你可能会问:为什么要用 Socket 而不是 Binder?我个人觉得原因有两个:
- 性能:Socket 的读写比 Binder 调用更轻量,适合高频事件流
- 同步性:IMS 需要直接写入事件,不需要等待应用响应
避坑指南:我曾经遇到过 InputChannel 泄漏的问题。如果应用进程异常退出,WMS 没有及时清理对应的 InputChannel,会导致 IMS 持续向一个已关闭的 Socket 写入数据,最终触发 ANR。解决方案是在应用死亡回调中显式调用 unregisterInputChannel。
三、事件从内核到窗口的传递路径
这是本章最核心的部分。我们从头到尾走一遍完整路径。
3.1 整体流程图
下面这张图是我画的,展示了事件从内核到窗口的完整路径:
3.2 逐层详解
第一步:内核层
触摸屏硬件产生中断,内核输入子系统将原始数据打包成 struct input_event,写入 /dev/input/eventX 设备节点。IMS 的 EventHub 通过 epoll 监听这些节点。
第二步:Native 层
EventHub 读取到事件后,交给 InputReader 进行解析。InputReader 会把原始坐标、压力值等转换成 Android 的 MotionEvent 或 KeyEvent。然后交给 InputDispatcher。
这里有个关键点:InputDispatcher 运行在一个独立的线程中,它维护着一个事件队列 mInboundQueue。它会逐个处理队列中的事件,做 hit-test 找到目标窗口。
hit-test 的逻辑:
- 遍历所有窗口的 InputWindowHandle
- 检查事件坐标是否在窗口的 touchable 区域内
- 如果有多个窗口重叠,按 Z-order 从高到低匹配
- 如果窗口设置了 FLAG_NOT_TOUCHABLE,跳过
第三步:System Server 层
InputDispatcher 找到目标窗口后,通过该窗口的 InputChannel(服务端)将事件写入 Socket。注意,这里写入的是序列化后的 InputMessage,包含事件类型、坐标、时间戳等。
第四步:应用层
应用进程的 InputEventReceiver 在 Looper 线程中监听客户端的 InputChannel。当 Socket 有数据可读时,它会被唤醒,读取 InputMessage,反序列化成 MotionEvent,然后交给 ViewRootImpl 的 deliverInputEvent 方法,最终沿着 View 树分发下去。
// frameworks/base/core/java/android/view/InputEventReceiver.java
// 核心代码片段
public final class InputEventReceiver {
// 这是一个 native 方法,在 Looper 中注册 fd 监听
private native void nativeInit(InputChannel channel, Looper looper);
// 当有事件到来时,框架层会回调这个方法
public void onInputEvent(InputEvent event, int displayId) {
// 默认实现是交给 ViewRootImpl 处理
// 子类可以重写这个方法
}
}
注意事项:应用进程的 InputEventReceiver 是在主线程 Looper 中工作的。如果主线程被耗时操作阻塞,事件就无法及时处理,最终会导致 ANR。我曾经在一个项目中看到,某个页面在 onTouchEvent 里做了网络请求,结果触摸事件队列越积越多,最后系统弹出了 ANR 对话框。
3.3 关键数据结构总结
| 层级 | 关键类/结构 | 作用 |
|---|---|---|
| 内核 | input_event | 原始输入事件 |
| Native | EventHub | 监听 /dev/input 设备 |
| Native | InputReader | 解析原始事件为 Android 事件 |
| Native | InputDispatcher | 分发事件到目标窗口 |
| System Server | InputWindowHandle | 窗口信息(位置、大小、token) |
| System Server | InputChannel | Socket 对,用于跨进程传输事件 |
| 应用 | InputEventReceiver | 接收事件并分发给 View 树 |
嗯,到这里,事件从内核到窗口的完整路径就讲完了。你可能会觉得步骤很多,但说白了就是:硬件产生事件 → 内核上报 → IMS 解析分发 → WMS 协助定位 → InputChannel 传输 → 应用处理。每一步都有它存在的理由,少一个环节都不行。
我个人建议你在阅读源码时,重点关注 InputDispatcher::dispatchOnce 和 ViewRootImpl::deliverInputEvent 这两个函数,它们是整个流程的枢纽。理解了它们,你就掌握了输入事件分发的命脉。