3、InputReader线程循环:loopOnce()函数详解,从EventHub读取原始事件
好,我们继续深入Input系统的核心。
上一章我们聊了InputReader的启动流程。线程跑起来了,那它到底在循环里干了什么?
答案就在 loopOnce() 这个函数里。我个人习惯把它叫做「一次事件循环的骨架」。你想想看,整个Android的触摸、按键、轨迹球事件,源头都在这里。
3.1 loopOnce() 的整体流程
先看代码结构。我简化了一下,把核心逻辑拎出来:
void InputReader::loopOnce() {
// 1. 从EventHub读取原始事件
size_t count = mEventHub->getEvents(mEventBuffer);
// 2. 处理读取到的事件
if (count > 0) {
processEventsLocked(mEventBuffer, count);
}
// 3. 刷新设备状态,把处理好的事件发出去
mQueuedListener->flush();
}
嗯,就这么三行核心逻辑。但每一行背后都藏着不少细节。
我曾经在调试一个触控延迟问题时,就是卡在第一步 getEvents 的返回时机上。当时百思不得其解,为什么明明手指已经按下了,事件却迟迟不来?后来才发现是EventHub的epoll超时设置有问题。
3.2 EventHub::getEvents() 的读取机制
这个函数是InputReader和底层硬件的桥梁。说白了,它负责从Linux内核的输入设备节点(比如 /dev/input/eventX)读取原始事件。
它的工作流程大致是这样的:
- 等待事件:通过epoll机制阻塞等待,直到有输入事件发生
- 批量读取:一次读取多个原始事件,存入缓冲区
- 事件转换:把内核的
struct input_event转换成框架层的RawEvent - 返回计数:告诉调用方这次拿到了多少个事件
这里有个关键点:getEvents 是阻塞的。没有事件时,InputReader线程就乖乖等着,不浪费CPU。有事件来了,epoll唤醒它,一口气读一批。
核心数据结构:mEventBuffer
这是一个 RawEvent 数组,默认大小是256。每次getEvents最多读256个原始事件。如果一次读不完,下次循环继续读。
我建议你在分析事件丢失问题时,先看看这个缓冲区有没有溢出。我曾经遇到过因为缓冲区太小导致事件被丢弃的案例,排查了很久才发现是这里的问题。
3.3 原始事件的结构
从内核读上来的 struct input_event 长这样:
struct input_event {
struct timeval time; // 事件发生的时间戳
__u16 type; // 事件类型:EV_KEY, EV_ABS, EV_REL 等
__u16 code; // 事件代码:比如按键的键值,触摸的X/Y轴
__s32 value; // 事件值:按键按下/抬起,触摸坐标值等
};
你想想看,一个触摸事件从手指按下到抬起,会产生多少条这样的记录?
举个例子,一个简单的触摸按下:
- 先来一个
EV_ABS / ABS_MT_POSITION_X / x坐标 - 再来一个
EV_ABS / ABS_MT_POSITION_Y / y坐标 - 最后来一个
EV_SYN / SYN_REPORT / 0表示这一帧数据结束
嗯,这里要注意:SYN_REPORT 是事件分帧的边界。没有它,上层就不知道这一组数据什么时候算完整。
3.4 事件读取的避坑指南
我在项目中遇到过几个典型问题,分享给你:
坑1:事件读取不全
我曾经遇到一个情况:快速滑动时,触摸轨迹出现断点。排查发现是 getEvents 返回的事件数少于实际产生的。原因?EventHub在读取时,如果缓冲区满了,会丢弃旧事件保留新事件。这个策略在某些场景下会导致轨迹不连续。
解决方案:增大 mEventBuffer 的大小,或者调整丢弃策略。
技巧:利用时间戳排查问题
每个原始事件都带有内核时间戳。我习惯在调试时打印相邻事件的时间差。如果发现某个事件的时间戳比前一个晚了100ms以上,那基本可以断定有卡顿或者事件被延迟处理了。
这个技巧帮我定位过好几个跟输入延迟相关的bug。
3.5 从EventHub到InputReader的数据流转
我们梳理一下整个链条:
| 层级 | 组件 | 数据形式 | 说明 |
|---|---|---|---|
| 内核 | 输入设备驱动 | struct input_event | 原始硬件事件 |
| Native层 | EventHub | RawEvent | 封装后的框架事件 |
| Native层 | InputReader | NotifyArgs | 处理后的通知事件 |
| Native层 | InputDispatcher | DispatchEntry | 分发队列中的事件 |
你看,loopOnce() 就是整个链条的起点。它从EventHub拿到 RawEvent,然后交给 processEventsLocked 去解析、转换、封装。
我个人觉得,理解这个起点特别重要。因为很多输入问题的根因,其实就出在「事件怎么从内核读上来的」这一步。比如:
- 为什么某些外设插上后没反应?——EventHub没检测到设备节点
- 为什么触摸有时灵有时不灵?——getEvents读取策略有问题
- 为什么按键响应慢?——epoll等待超时设置不合理
嗯,这些问题我都踩过坑。后面章节我们会逐一深入。
3.6 小结
这一章我们重点看了 loopOnce() 的第一行代码:mEventHub->getEvents()。
说白了,它就是InputReader线程的「发动机」。每次循环,它从内核拉取一批原始事件,然后交给后续流程处理。
下一章,我们会深入 processEventsLocked,看看这些原始事件是怎么被解析成具体的输入事件的。到时候你会看到,一个简单的触摸按下,背后经历了多少层转换。