15、Sensor与Input系统:EventHub、InputReader、InputDispatcher,触摸事件分发

各位同学,今天我们来聊聊Android底层最核心的机制之一——输入系统。说白了,就是你的手指在屏幕上点了一下,这个动作是怎么一步步传到App里的。我当年刚接触这部分源码时,说实话,头大了好一阵子。但搞懂之后,你会发现它其实是一个很精巧的流水线。

Android的输入系统,核心有三个角色:EventHubInputReaderInputDispatcher。它们跑在system_server进程里的InputManagerService中。嗯,这里要注意,它们不是Java层的东西,而是C++层的核心组件。

一句话总结:EventHub负责从驱动读原始事件,InputReader负责解析和分类,InputDispatcher负责找到目标窗口并分发。

15.1 EventHub——底层事件的“搬运工”

EventHub是输入系统的起点。它做的事情很简单:打开所有输入设备节点(/dev/input/eventX),然后用epoll监听。一旦有数据,就read出来。

我个人习惯把EventHub看作一个“事件采集器”。它不关心你按的是键盘还是触摸屏,它只负责把原始数据从内核空间搬到用户空间。

// EventHub的核心循环(简化版)
size_t EventHub::getEvents(int timeoutMillis, RawEvent* buffer, size_t bufferSize) {
    // 1. 用epoll_wait等待事件
    int eventCount = epoll_wait(mEpollFd, mPendingEventItems, EPOLL_MAX_EVENTS, timeoutMillis);
    
    // 2. 遍历事件,从设备节点读取
    for (int i = 0; i < eventCount; i++) {
        struct input_event iev;
        read(deviceFd, &iev, sizeof(iev));
        
        // 3. 封装成RawEvent
        buffer[bufferSize++] = RawEvent(deviceId, type, code, value);
    }
    return bufferSize;
}

这里有个坑,我曾经遇到过:某些低端手机的触摸屏驱动不太规范,上报的事件顺序偶尔会乱。EventHub本身不做校验,它只管读。所以问题会往后抛。

避坑指南:我曾经在调试一个项目时,发现触摸偶尔会“跳点”。查了两天才发现,是EventHub读取时没有处理SYN_REPORT同步事件。后来加了个校验,确保一组完整的事件读完再上报,问题就解决了。

15.2 InputReader——把原始事件“翻译”成标准事件

EventHub把原始数据扔给InputReader。InputReader的工作就是“翻译”。它根据设备的配置(比如是触摸屏还是键盘),把RawEvent解析成标准化的输入事件。

你想想看,触摸屏上报的坐标可能是0-4095,但屏幕分辨率是1080x1920。InputReader需要做坐标映射。还有,多点触控的协议(Protocol A vs Protocol B),也是在这里处理的。

// InputReader处理触摸事件的简化流程
void InputReader::loopOnce() {
    // 1. 从EventHub获取原始事件
    mEventHub->getEvents(timeout, mRawEvents, bufferSize);
    
    // 2. 遍历每个原始事件
    for (auto& rawEvent : mRawEvents) {
        // 3. 根据设备类型,调用对应的处理器
        if (device->isTouchscreen()) {
            processTouchEvent(rawEvent);  // 触摸事件处理
        } else if (device->isKeyboard()) {
            processKeyEvent(rawEvent);    // 按键事件处理
        }
    }
    
    // 4. 把处理好的事件交给InputDispatcher
    mQueuedListener->flush();
}

InputReader内部维护了一个状态机。比如触摸事件,它会跟踪当前按下的手指数量、坐标、压力值等。我建议你重点关注MultiTouchInputMapper这个类,它处理了90%的触摸逻辑。

注意:InputReader是跑在独立线程里的,它和InputDispatcher之间通过一个Queue(InputListenerInterface)通信。这个队列是线程安全的,但如果你在调试时发现事件丢失,可以优先检查这里是否有阻塞。

15.3 InputDispatcher——事件分发的“指挥官”

InputDispatcher是输入系统的最后一环,也是最复杂的一环。它的任务很简单:找到当前触摸点下方的窗口,然后把事件发过去。

但简单背后是复杂的逻辑。它需要处理窗口的Z-order、焦点、ANR、拦截器等等。说白了,它要确保事件能准确无误地送到正确的目标。

// InputDispatcher分发触摸事件的简化流程
void InputDispatcher::dispatchOnce() {
    // 1. 从队列取出一个事件
    EventEntry* entry = mInboundQueue->dequeue();
    
    // 2. 找到目标窗口(通过WindowManagerService查询)
    Vector<InputTarget> targets;
    findTouchedWindowTargets(entry->touchState, targets);
    
    // 3. 遍历所有目标窗口,发送事件
    for (auto& target : targets) {
        // 如果是前台应用,直接发送
        if (target->inputChannel) {
            target->inputChannel->sendMessage(entry);
        }
    }
    
    // 4. 设置ANR超时(5秒)
    startAnrTimer(entry);
}

这里有个关键点:InputDispatcher是通过InputChannel和App通信的。InputChannel本质上是一对SocketPair,一端在system_server,一端在App进程。事件通过Socket传输,所以会有序列化和反序列化的开销。

核心逻辑:触摸事件的分发路径是:
EventHub → InputReader → InputDispatcher → InputChannel → App主线程 → View.dispatchTouchEvent()

15.4 触摸事件分发的完整流程

我们来画一张图,把整个流程串起来。这张图我建议你保存下来,面试时经常会被问到。

Android触摸事件分发完整流程 硬件层 触摸屏驱动 /dev/input/eventX EventHub epoll_wait + read() InputReader 坐标映射 / 协议解析 InputDispatcher 查找目标窗口 + 分发 InputChannel SocketPair 跨进程通信 App主线程 进程说明 内核空间 system_server (C++层) system_server (C++层) system_server (C++层) system_server ↔ App进程 App进程 (Java层)

15.5 几个关键细节

搞清楚了主流程,我们再来看几个容易出问题的细节。

  • 事件合成:InputReader会把多个RawEvent合成一个完整的MotionEvent。比如一个DOWN事件,可能包含坐标、压力、尺寸等多个字段。这些字段可能分多次上报,InputReader需要等SYN_REPORT信号来了才合成。
  • 窗口查找:InputDispatcher通过WindowManagerService维护的窗口列表,找到触摸点下方的窗口。这里用的是hit-test算法,说白了就是遍历所有窗口,看触摸点落在哪个窗口的区域内。
  • ANR机制:如果事件发出去5秒内App没有处理完,InputDispatcher会触发ANR。我曾经遇到过一个问题:某个App的onTouchEvent里做了耗时操作,导致整个系统触摸卡顿。嗯,后来加了异步处理才解决。

调试技巧:如果你想看输入事件的原始日志,可以执行:adb shell dumpsys input。这个命令会打印出所有输入设备的状态、当前窗口焦点、以及最近的事件记录。我调试触摸问题时,第一个命令就是这个。

15.6 总结

Sensor和Input系统,说白了就是一条从硬件到App的“事件流水线”。EventHub负责采集,InputReader负责翻译,InputDispatcher负责投递。每个环节都有它的职责和坑。

我个人建议你花点时间把InputReader和InputDispatcher的源码走读一遍。重点看InputReader.cpp里的loopOnce()InputDispatcher.cpp里的dispatchOnce()。搞懂这两个函数,你就掌握了Android输入系统的核心。

好了,这一章就到这里。记住,搞底层没有捷径,多读源码,多动手调试,慢慢就通了。


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