一、窗口输入事件分发: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 整体流程图

下面这张图是我画的,展示了事件从内核到窗口的完整路径:

事件从内核到窗口的传递路径 Linux 内核层 /dev/input/eventX → 输入子系统驱动 → 事件队列 Native 层 (InputReader + InputDispatcher) EventHub 读取 → InputReader 解析 → InputDispatcher 分发 System Server 层 (WMS + IMS) WMS 提供窗口信息 → IMS 做 hit-test → 确定目标窗口 应用层 (目标窗口) InputChannel 接收 → ViewRootImpl 分发 → View 树处理 硬件中断 Socket 对 Binder 调用 InputChannel

3.2 逐层详解

第一步:内核层

触摸屏硬件产生中断,内核输入子系统将原始数据打包成 struct input_event,写入 /dev/input/eventX 设备节点。IMS 的 EventHub 通过 epoll 监听这些节点。

第二步:Native 层

EventHub 读取到事件后,交给 InputReader 进行解析。InputReader 会把原始坐标、压力值等转换成 Android 的 MotionEventKeyEvent。然后交给 InputDispatcher。

这里有个关键点:InputDispatcher 运行在一个独立的线程中,它维护着一个事件队列 mInboundQueue。它会逐个处理队列中的事件,做 hit-test 找到目标窗口。

hit-test 的逻辑:

  1. 遍历所有窗口的 InputWindowHandle
  2. 检查事件坐标是否在窗口的 touchable 区域内
  3. 如果有多个窗口重叠,按 Z-order 从高到低匹配
  4. 如果窗口设置了 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::dispatchOnceViewRootImpl::deliverInputEvent 这两个函数,它们是整个流程的枢纽。理解了它们,你就掌握了输入事件分发的命脉。


公众号:蓝海资料掘金营,微信deep3321