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)读取原始事件。

它的工作流程大致是这样的:

  1. 等待事件:通过epoll机制阻塞等待,直到有输入事件发生
  2. 批量读取:一次读取多个原始事件,存入缓冲区
  3. 事件转换:把内核的 struct input_event 转换成框架层的 RawEvent
  4. 返回计数:告诉调用方这次拿到了多少个事件

这里有个关键点: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,看看这些原始事件是怎么被解析成具体的输入事件的。到时候你会看到,一个简单的触摸按下,背后经历了多少层转换。