13. WMS 中的输入事件分发:Window 与 InputDispatcher 的关联
好,我们继续往下走。上一章我们把 WMS 的窗口管理讲得差不多了,这一章我们来聊聊一个非常关键的问题——用户点了一下屏幕,事件是怎么找到对应窗口的?
说白了,就是 InputDispatcher 和 Window 之间怎么建立关联的。我当年刚接触这部分时,总觉得事件分发是 View 层的事,后来才发现,真正的“路由决策”其实发生在 WMS 里。
13.1 整体架构:谁在负责分发?
先看一张图,帮你快速建立全局认知。
你看,整个链路其实很清晰:InputReader 从驱动拿事件,交给 InputDispatcher,Dispatcher 再根据 WMS 提供的窗口信息,把事件投递到对应的 InputChannel 上。
嗯,这里有个关键点:InputDispatcher 本身并不知道窗口长什么样,它只认 InputChannel。而 WMS 就是那个“牵线搭桥”的角色。
13.2 Window 如何注册到 InputDispatcher?
我们来看一个具体的流程。当一个窗口被添加到 WMS 时,会发生什么?
我直接带你走一遍核心代码路径。在 WindowManagerService.addWindow() 方法中,有一段关键逻辑:
// 伪代码,但逻辑和真实源码一致
public int addWindow(Session session, IWindow client, ...) {
// 1. 创建 WindowState
WindowState win = new WindowState(this, session, client, ...);
// 2. 创建 InputChannel 对
InputChannel[] inputChannels = InputChannel.openInputChannelPair(win.getName());
// 3. 把服务端 InputChannel 注册到 InputDispatcher
mInputManager.registerInputChannel(
win.mInputChannel, // 服务端通道
win.mInputWindowHandle // 窗口句柄,包含位置、焦点等信息
);
// 4. 把客户端 InputChannel 返回给应用进程
// 应用通过 ViewRootImpl 接收事件
win.mClientInputChannel = inputChannels[1];
// 5. 保存到 WindowState 中
win.mInputChannel = inputChannels[0];
}
这段代码我简化了,但核心步骤一个没少。你想想看,这里最关键的是第 2 步——openInputChannelPair。它创建了一对 SocketPair,一个给 WMS/Dispatcher 用,一个给应用进程用。
核心要点:
- 每个 Window 对应一个 InputChannel 对
- 服务端通道注册到 InputDispatcher
- 客户端通道通过 Binder 传给应用
- 事件通过 Socket 跨进程传递,不走 Binder
为什么不用 Binder?我个人习惯上觉得,Binder 虽然方便,但高频事件场景下性能不够。Socket 对,尤其是 Linux 的 SocketPair,延迟极低,适合触摸这种高频事件流。
13.3 InputDispatcher 如何找到目标 Window?
好,现在事件来了。InputDispatcher 手里有一堆注册好的 InputChannel,它怎么知道该发给谁?
这里涉及一个重要的数据结构——mWindowHandles。它是一个列表,里面存着所有窗口的句柄信息:
// InputDispatcher.cpp 中的核心结构
struct InputWindowHandle {
int32_t displayId; // 屏幕 ID
Rect touchableRegion; // 可触摸区域
bool visible; // 是否可见
bool focusable; // 是否可获取焦点
int32_t layer; // Z-order 层级
sp<InputChannel> inputChannel; // 对应的通道
};
分发流程大致是这样的:
- 找到焦点窗口:如果是按键事件,直接找当前焦点窗口
- 命中测试:如果是触摸事件,遍历所有窗口,检查触摸点是否落在
touchableRegion内 - Z-order 排序:如果有多个窗口都命中,选层级最高的那个
- 投递事件:通过 InputChannel 的 Socket 写入事件
我曾经在项目中遇到过一个问题:一个全屏的 Dialog 弹出来之后,底下的 Activity 还能收到触摸事件。排查了半天,发现是 Dialog 的 touchableRegion 没有正确设置,导致命中测试时漏掉了 Dialog 的区域。
避坑指南:
我曾经在自定义 PopupWindow 时,忘记设置 setTouchModal(true),结果导致 PopupWindow 后面的 View 也能收到点击事件。后来我养成了一个习惯:任何弹出窗口,一定要检查它的 touchableRegion 是否覆盖了预期区域。
13.4 焦点管理:谁说了算?
说到焦点,这是另一个容易踩坑的地方。WMS 中维护了一个 mFocusedWindow 和 mFocusedApp,分别表示当前焦点窗口和焦点应用。
焦点切换的时机:
| 场景 | 焦点变化 | 触发方式 |
|---|---|---|
| 用户点击新窗口 | 旧窗口失去焦点,新窗口获得焦点 | InputDispatcher 通知 WMS |
| 窗口被覆盖 | 被覆盖窗口失去焦点 | WMS 在 relayout 时检测 |
| 窗口被移除 | 焦点转移到下一个可见窗口 | WMS 在 removeWindow 时处理 |
| 应用主动请求 | 调用 requestFocus | WMS 直接设置 |
这里有个细节:InputDispatcher 并不直接管理焦点,它只负责查询。每次分发事件前,Dispatcher 会调用 WMS 的接口获取当前焦点窗口。说白了,焦点决策权在 WMS 手里。
13.5 事件投递的完整链路
我们来走一遍完整的流程,从手指按下到应用收到事件:
// 伪代码,展示事件投递链路
// 1. InputReader 读取事件
InputEvent event = readFromDevice();
// 2. 交给 InputDispatcher
mDispatcher.dispatch(event);
// 3. Dispatcher 查找目标窗口
InputWindowHandle target = findTargetWindow(event);
// 4. 通过 InputChannel 发送
target.inputChannel.sendMessage(event);
// 5. 应用端 ViewRootImpl 接收
// (在应用进程)
InputChannel.receiveMessage(event);
// 然后交给 View 树分发
View.dispatchTouchEvent(event);
你看,整个链路其实很简洁。但简洁背后,是 WMS 和 InputDispatcher 之间大量的状态同步工作。
个人经验:
我建议你在调试触摸问题时,重点关注两个地方:
- Window 的 touchableRegion:是不是被其他窗口挡住了?
- InputChannel 是否还活着:应用进程崩溃后,通道会断开,Dispatcher 会收到通知
用 adb shell dumpsys input 可以查看当前所有注册的 InputChannel 和窗口信息,非常实用。
13.6 窗口动画对事件分发的影响
嗯,这里还要提一个容易被忽略的点——窗口动画期间的事件处理。
当一个窗口正在执行进入动画时,它其实已经可见了,但可能还没完全就位。这时候触摸事件怎么处理?
WMS 的做法是:动画期间的窗口,仍然参与命中测试。但有一个例外——如果窗口正在执行退出动画,它会被标记为 REMOVING 状态,此时不再接收事件。
我记得有一次,一个同事发现窗口关闭动画还没结束,用户快速点击,结果事件被分发到了已经“半关闭”的窗口上,导致奇怪的闪退。后来我们在 WindowState 中加了一个判断:如果窗口正在执行退出动画,直接跳过事件分发。
13.7 总结一下
这一章我们聊了 WMS 和 InputDispatcher 之间的协作关系。核心就一句话:WMS 负责注册和管理窗口的 InputChannel,InputDispatcher 负责根据窗口信息分发事件。
你想想看,整个 Android 的触摸系统,本质上就是一个“生产者-消费者”模型。InputReader 是生产者,应用 View 是消费者,而 InputDispatcher 和 WMS 就是中间的“路由层”。
下一章我们会深入 InputDispatcher 的内部调度策略,看看它是如何处理多指触控、事件序列、以及 ANR 的。嗯,这些内容在实际开发中非常有用,尤其是当你遇到“点击无响应”或者“触摸卡顿”的问题时。
公众号:蓝海资料掘金营,微信deep3321