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 的 MotionEvent 或 KeyEvent
  • 进行坐标转换、多点触控协议解析等

举个例子,触摸屏上报的原始坐标可能是 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