15、Sensor与Input系统:EventHub、InputReader、InputDispatcher,触摸事件分发
各位同学,今天我们来聊聊Android底层最核心的机制之一——输入系统。说白了,就是你的手指在屏幕上点了一下,这个动作是怎么一步步传到App里的。我当年刚接触这部分源码时,说实话,头大了好一阵子。但搞懂之后,你会发现它其实是一个很精巧的流水线。
Android的输入系统,核心有三个角色:EventHub、InputReader、InputDispatcher。它们跑在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 触摸事件分发的完整流程
我们来画一张图,把整个流程串起来。这张图我建议你保存下来,面试时经常会被问到。
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输入系统的核心。
好了,这一章就到这里。记住,搞底层没有捷径,多读源码,多动手调试,慢慢就通了。