18、输入系统:EventHub、InputReader、InputDispatcher

说到Android的输入系统,很多人第一反应就是「点一下屏幕,应用响应了」。但中间发生了什么?我当年刚接触这部分源码时,也是一头雾水。说白了,从你手指触摸屏幕到应用层的onTouchEvent被调用,中间经过了三个核心角色:EventHub、InputReader、InputDispatcher。

这三个家伙分工明确,各司其职。我习惯把它们比作一个快递系统:EventHub是收件员,InputReader是分拣员,InputDispatcher是派送员。今天我们就来拆解一下这个流程。

18.1 整体架构概览

先看一张图,帮你建立整体认知。这是Android输入系统的核心流程:

Android 输入系统核心流程 硬件设备 触摸屏 / 键盘 / 鼠标 EventHub 读取原始事件 InputReader 解析 / 分类事件 InputDispatcher 分发到目标窗口 关键说明 1. EventHub 从 /dev/input/ 读取原始事件 2. InputReader 将原始事件 转换为 Android 事件 3. InputDispatcher 根据 焦点窗口分发事件 4. 整个过程在 InputReader 线程中循环执行 5. 最终到达应用层

嗯,这张图把三个核心模块的上下游关系画清楚了。接下来我们逐个深入。

18.2 EventHub — 硬件事件的「收件员」

EventHub 是输入系统的最底层。它直接跟 Linux 内核的输入子系统打交道,从 /dev/input/ 目录下读取原始事件。

我个人习惯把 EventHub 看作一个「事件采集器」。它做的事情其实很简单:

  • 打开所有输入设备节点
  • 监听设备的热插拔(比如你插了个USB键盘)
  • 通过 epoll 机制等待事件到来
  • 读取原始 input_event 结构体

来看一段核心代码,这是 EventHub 中读取事件的逻辑:

// frameworks/native/services/inputflinger/EventHub.cpp
size_t EventHub::getEvents(int timeoutMillis, RawEvent* buffer, size_t bufferSize) {
    // 使用 epoll 等待事件
    int eventCount = epoll_wait(mEpollFd, mPendingEventItems, EPOLL_MAX_EVENTS, timeoutMillis);
    
    for (int i = 0; i < eventCount; i++) {
        // 读取原始 input_event
        struct input_event iev;
        ssize_t n = read(deviceFd, &iev, sizeof(iev));
        
        // 封装成 RawEvent
        buffer[bufferCount].deviceId = deviceId;
        buffer[bufferCount].type = iev.type;
        buffer[bufferCount].code = iev.code;
        buffer[bufferCount].value = iev.value;
        bufferCount++;
    }
    return bufferCount;
}
小技巧: 调试输入问题时,可以用 getevent 命令直接查看 EventHub 输出的原始事件。我在项目中排查触摸屏不灵敏问题时,就是靠这个命令定位到是硬件上报频率太低。

18.3 InputReader — 事件的「翻译官」

EventHub 读出来的原始事件,说白了就是一堆数字:type=3, code=53, value=720。这谁能看懂?InputReader 就是干这个翻译活的。

InputReader 运行在独立的 InputReader 线程中。它会:

  • 根据设备类型创建对应的 InputMapper(比如触摸屏用 TouchInputMapper,键盘用 KeyboardInputMapper)
  • 将原始事件映射成 Android 的 MotionEventKeyEvent
  • 进行坐标转换、多点触控协议解析等

举个例子,触摸屏上报的原始坐标可能是 0-4095 的 ADC 值,InputReader 需要把它转换成屏幕分辨率下的像素坐标。我曾经遇到过一个坑:某款平板触摸屏的坐标映射反了,点左上角却响应右下角。查了半天,发现是 InputReader 中的坐标转换矩阵配置错了。

// 伪代码:InputReader 处理流程
void InputReader::loopOnce() {
    // 1. 从 EventHub 获取原始事件
    RawEvent rawEvents[MAX_RAW_EVENTS];
    size_t count = mEventHub->getEvents(timeout, rawEvents, MAX_RAW_EVENTS);
    
    // 2. 交给对应的 Mapper 处理
    for (size_t i = 0; i < count; i++) {
        InputMapper* mapper = getMapperForDevice(rawEvents[i].deviceId);
        mapper->process(&rawEvents[i]);
    }
    
    // 3. 将处理好的事件放入队列
    mQueuedListener->flush();
}
重点: InputReader 处理完的事件会放入一个 InputListenerInterface 队列,然后通知 InputDispatcher 来取。这个队列是线程安全的,用到了管道(pipe)机制来做唤醒。

18.4 InputDispatcher — 事件的「派送员」

InputDispatcher 是输入系统的最后一环,也是最复杂的一环。它运行在 InputDispatcher 线程中,负责把事件分发给目标窗口。

你想想看,屏幕上可能同时有多个窗口:状态栏、桌面、悬浮窗、弹窗……手指点下去,事件该给谁?

InputDispatcher 通过 焦点窗口(Focused Window) 机制来决定。它会维护一个窗口列表,并根据窗口的 Z-order 和焦点状态进行分发。

分发流程大致如下:

  1. 从队列中取出事件
  2. 查找当前焦点窗口
  3. 检查窗口是否有 ANR 风险(如果窗口5秒内没有处理上一个事件,就会触发 ANR)
  4. 通过 InputChannel 将事件发送到目标窗口
  5. 等待窗口处理完成(同步事件需要等待)
// 伪代码:InputDispatcher 分发流程
void InputDispatcher::dispatchOnce() {
    // 取出待分发的事件
    EventEntry* entry = mInboundQueue.dequeue();
    
    // 找到目标窗口
    Vector<InputTarget> targets;
    findTargets(entry, &targets);
    
    // 分发到每个目标
    for (size_t i = 0; i < targets.size(); i++) {
        InputChannel* channel = targets[i].inputChannel;
        // 通过 socket 发送事件
        channel->sendMessage(entry);
    }
}
避坑指南: 我曾经遇到一个 ANR 问题,原因是某个自定义 View 的 onTouchEvent 中做了耗时操作(网络请求),导致 InputDispatcher 等待超时。记住:输入事件的处理必须在 5 秒内完成,否则系统会弹出 ANR 对话框。

18.5 三个模块的协作关系

这三个模块虽然分工不同,但协作非常紧密。我画了一张时序图,帮你理解它们之间的调用关系:

EventHub / InputReader / InputDispatcher 协作时序 EventHub InputReader InputDispatcher getEvents() 返回 RawEvent Mapper 处理 / 坐标转换 notify() 放入队列 查找焦点窗口 InputChannel 发送

18.6 实战中的常见问题

问题现象 可能原因 排查思路
触摸无响应 EventHub 未读到事件 getevent 检查硬件是否上报
触摸位置偏移 InputReader 坐标转换错误 检查 idc 配置文件中的坐标映射
点击后 ANR InputDispatcher 超时 检查应用层 onTouchEvent 是否有耗时操作
多点触控异常 协议解析错误 确认内核上报的是 Type A 还是 Type B 协议
调试利器: 在开发板上,可以用 dumpsys input 查看当前输入系统的状态,包括所有输入设备、焦点窗口、待分发事件队列等。我每次排查输入问题,第一件事就是跑这个命令。

好了,关于输入系统的三个核心组件就讲到这里。EventHub 负责收件,InputReader 负责翻译,InputDispatcher 负责派送。理解了这个链条,你就能在遇到输入问题时,快速定位是硬件层、系统层还是应用层的问题。


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