Android 7系统输入(二):EventHub — 原始事件的采集者

系列目录 :第一篇:从硬件到应用的事件旅程 | 第二篇:EventHub --- 原始事件的采集者 | 第三篇:InputReader --- 原始事件到Android事件的转换引擎 | 第四篇:InputDispatcher --- 事件分发与ANR超时机制 | 第五篇:应用侧 --- InputChannel、ViewRootImpl与事件消费


一、EventHub 在整个输入系统中的位置

回顾第一篇的总览,EventHub 处于整个事件流水线的最上游:

复制代码
/dev/input/eventX  →  EventHub  →  InputReader  →  InputDispatcher  →  APP
                        ▲
                    本篇聚焦

EventHub 的职责一句话概括:监听所有输入设备,读取内核上报的原始事件,并把它们交给 InputReader 。看似简单,但实现中涉及 Linux 的 inotifyepollioctl 等多个系统调用,是理解 Android 输入系统底层机制的绝佳切入点。

源码位置:

复制代码
frameworks/native/services/inputflinger/EventHub.cpp
frameworks/native/services/inputflinger/EventHub.h

二、EventHub 的核心数据结构

2.1 Device 结构体

每个输入设备在 EventHub 中都有一个对应的 Device 结构体来维护状态:

cpp 复制代码
struct Device {
    int fd;                          // 设备文件描述符
    String8 path;                    // 设备路径,如 /dev/input/event2
    DeviceIdentifier identifier;     // 设备唯一标识(总线类型、厂商ID、产品ID等)
    uint32_t classes;                // 设备类别掩码(INPUT_DEVICE_CLASS_*)
    KeyedVector<int, RawAbsoluteAxisInfo> absoluteAxes; // 绝对坐标轴信息
    String8 configurationFile;
    PropertyMap configuration;
    KeyMap keyMap;                   // 键盘映射
    bool enabled;
    bool ignored;
};

其中 classes 是最关键的分类字段,通过一系列标志位来标识设备类型:

cpp 复制代码
enum {
    INPUT_DEVICE_CLASS_KEYBOARD      = 0x00000001,  // 键盘
    INPUT_DEVICE_CLASS_ALPHAKEY      = 0x00000002,  // 字母键盘
    INPUT_DEVICE_CLASS_TOUCH         = 0x00000004,  // 触摸屏/触摸板
    INPUT_DEVICE_CLASS_TOUCH_MT      = 0x00000008,  // 多点触控屏
    INPUT_DEVICE_CLASS_CURSOR        = 0x00000010,  // 鼠标/轨迹球
    INPUT_DEVICE_CLASS_SWITCH        = 0x00000020,  // 开关类
    INPUT_DEVICE_CLASS_JOYSTICK      = 0x00000040,  // 摇杆/游戏手柄
    INPUT_DEVICE_CLASS_EXTERNAL      = 0x80000000,  // 外部设备
};

2.2 RawEvent 结构体

EventHub 从内核读取 input_event 后,封装为 RawEvent 传递给 InputReader:

cpp 复制代码
struct RawEvent {
    nsecs_t when;        // 事件时间戳(纳秒)
    int32_t deviceId;    // 设备ID(EventHub内部分配)
    int32_t type;        // 事件类型(EV_KEY, EV_ABS, EV_SYN 等)
    int32_t code;        // 事件编码
    int32_t value;       // 事件值
};

和内核的 input_event 相比,RawEvent 多了 deviceId 字段------EventHub 内部为每个设备分配的唯一 ID,后续 InputReader 用它识别事件来源。


三、EventHub 的初始化流程

EventHub 在其构造函数中完成了大量初始化工作:

cpp 复制代码
EventHub::EventHub(void) {
    // 1. 创建 epoll 实例
    mEpollFd = epoll_create1(EPOLL_CLOEXEC);

    // 2. 创建 inotify 实例,监听 /dev/input/ 目录
    mINotifyFd = inotify_init();
    inotify_add_watch(mINotifyFd, "/dev/input", IN_DELETE | IN_CREATE);

    // 3. 将 inotify fd 加入 epoll 监控
    struct epoll_event eventItem;
    eventItem.events = EPOLLIN;
    eventItem.data.u32 = EPOLL_ID_INOTIFY;
    epoll_ctl(mEpollFd, EPOLL_CTL_ADD, mINotifyFd, &eventItem);

    // 4. 创建唤醒管道(用于退出等待)
    pipe(mWakeReadPipeFd, mWakeWritePipeFd);
    eventItem.data.u32 = EPOLL_ID_WAKE;
    epoll_ctl(mEpollFd, EPOLL_CTL_ADD, mWakeReadPipeFd, &eventItem);

    // 5. 扫描已有设备
    scanDevicesLocked();
}

初始化时创建了三个关键文件描述符加入 epoll 监控:

被监控的 fd 用途 epoll data 标识
mINotifyFd 监听 /dev/input/ 目录变更 EPOLL_ID_INOTIFY
mWakeReadPipeFd 唤醒管道,用于退出等待 EPOLL_ID_WAKE
各设备 fd 读取各输入设备的原始事件 EPOLL_ID_DEVICE + deviceId

3.1 扫描已有设备

cpp 复制代码
void EventHub::scanDevicesLocked() {
    DIR* dir = opendir("/dev/input");
    while ((dirent = readdir(dir)) != NULL) {
        if (strncmp(dirent->d_name, "event", 5) == 0) {
            String8 path("/dev/input/");
            path.append(dirent->d_name);
            openDeviceLocked(path);
        }
    }
    closedir(dir);
}

3.2 打开并配置设备

cpp 复制代码
status_t EventHub::openDeviceLocked(const char* devicePath) {
    // 1. 以非阻塞方式打开设备
    int fd = open(devicePath, O_RDWR | O_CLOEXEC | O_NONBLOCK);

    // 2. 获取设备信息
    ioctl(fd, EVIOCGNAME(sizeof(name)), name);        // 设备名称
    ioctl(fd, EVIOCGPHYS(sizeof(phys)), phys);        // 物理路径
    ioctl(fd, EVIOCGBIT(0, sizeof(ev_bits)), ev_bits); // 支持的事件类型

    // 3. 判断设备类别
    uint32_t classes = 0;
    if (ev_bits & (1 << EV_KEY)) classes |= INPUT_DEVICE_CLASS_KEYBOARD;
    if (ev_bits & (1 << EV_ABS)) {
        if (test_bit(ABS_MT_POSITION_X, abs_bits))
            classes |= INPUT_DEVICE_CLASS_TOUCH_MT;
        else
            classes |= INPUT_DEVICE_CLASS_TOUCH;
    }
    if (ev_bits & (1 << EV_REL)) classes |= INPUT_DEVICE_CLASS_CURSOR;

    // 4. 创建设备对象并加入 epoll
    Device* device = new Device(fd, devicePath, identifier, classes);
    struct epoll_event eventItem;
    eventItem.events = EPOLLIN;
    eventItem.data.u32 = EPOLL_ID_DEVICE + deviceId; // 用 u32 携带 deviceId
    epoll_ctl(mEpollFd, EPOLL_CTL_ADD, fd, &eventItem);

    // 5. 加载键盘映射文件(.kl)
    device->keyMap.load(identifier, configuration);

    mDevices.add(deviceId, device);
    return OK;
}

关键设计epoll_event.data.u32 被用来携带 deviceId。当 epoll_wait 返回时,通过 u32 字段快速定位具体设备,避免遍历 mDevices


四、核心函数:getEvents() 详解

getEvents() 是 EventHub 对外暴露的核心接口,InputReader 在无限循环中不断调用它。

4.1 整体流程

复制代码
getEvents(timeoutMillis, buffer, bufferSize)
    │
    ├── 1. 处理设备变更(热插拔)
    │      mPendingINotify → 打开/关闭设备 → 生成 DEVICE_ADDED/REMOVED
    │
    ├── 2. epoll_wait() 等待事件(释放锁后阻塞等待)
    │      │
    │      ├── inotify 事件 → 加入 mPendingINotify
    │      ├── wake 事件 → 退出等待
    │      ├── 设备事件 → read() 读取 input_event → 封装 RawEvent
    │      └── 超时 → 返回 0
    │
    └── 3. 返回 eventCount

4.2 源码逐步解读

cpp 复制代码
size_t EventHub::getEvents(int timeoutMillis, RawEvent* buffer, size_t bufferSize) {
    size_t eventCount = 0;
    struct epoll_event pendingEventItems[EPOLL_MAX_EVENTS];

    {
        AutoMutex _l(mLock);

        // ===== 步骤1: 处理待处理的设备变更事件 =====
        while (mPendingINotify && eventCount < bufferSize) {
            // 生成 DEVICE_ADDED / DEVICE_REMOVED 类型的 RawEvent
            ...
        }

        // ===== 步骤2: epoll_wait 等待 =====
        if (eventCount == 0) {
            mLock.unlock();   // 关键!释放锁再等待
            pollResult = epoll_wait(mEpollFd, pendingEventItems,
                                    EPOLL_MAX_EVENTS, timeoutMillis);
            mLock.lock();     // 重新获取锁
        }
    }

    // ===== 步骤3: 处理 epoll 返回的事件 =====
    for (int i = 0; i < pollResult; i++) {
        const struct epoll_event& item = pendingEventItems[i];
        ssize_t deviceIndex = item.data.u32 - EPOLL_ID_DEVICE;

        if (deviceIndex >= 0) {
            // --- 3a. 设备事件 ---
            Device* device = getDeviceByIndexLocked(deviceIndex);
            while (eventCount < bufferSize) {
                struct input_event iev;
                ssize_t readSize = read(device->fd, &iev, sizeof(iev));
                if (readSize == sizeof(iev)) {
                    RawEvent* event = &buffer[eventCount++];
                    event->when     = systemTime(SYSTEM_TIME_MONOTONIC);
                    event->deviceId = device->id;
                    event->type     = iev.type;
                    event->code     = iev.code;
                    event->value    = iev.value;
                } else if (readSize < 0 && errno == EAGAIN) {
                    break; // 没有更多数据
                } else {
                    closeDeviceLocked(*device); // 设备故障
                    break;
                }
            }
        } else if (item.data.u32 == EPOLL_ID_INOTIFY) {
            // --- 3b. 设备热插拔 ---
            readNotifyLocked();
        } else if (item.data.u32 == EPOLL_ID_WAKE) {
            // --- 3c. 唤醒事件 ---
            char buffer[16];
            read(mWakeReadPipeFd, buffer, sizeof(buffer));
        }
    }
    return eventCount;
}

4.3 关键设计细节

(1)锁的释放时机epoll_wait 调用前释放锁 ,这很关键。如果持锁等待,InputReader 调用 getEvents 时会阻塞热插拔回调。释放锁后既能及时响应设备事件,又不影响并发访问。

(2)事件批量读取 :每次 epoll_wait 返回后,EventHub 循环 read() 直到 EAGAIN。一次 epoll_wait 唤醒可能对应多个内核事件(多点触控多个手指同时移动),批量读取减少系统调用。

(3)EV_SYN 的同步语义 :内核以 EV_SYN / SYN_REPORT 分隔一次完整触摸操作。EventHub 不解析这个语义,原样传递给 InputReader。


五、设备热插拔机制

EventHub 通过 inotify 实现设备即插即用。

5.1 设备添加

复制代码
用户插入USB键盘
    ↓ 内核创建 /dev/input/eventX
    ↓ inotify 上报 IN_CREATE
    ↓ epoll_wait 返回 → readNotifyLocked()
    ↓ openDeviceLocked(devicePath)
      ├── open() 设备节点
      ├── ioctl() 获取设备能力
      ├── 判断设备类别 (classes)
      ├── epoll_ctl(ADD) 加入监控
      └── 加入 mDevices 列表
    ↓ 生成 DEVICE_ADDED RawEvent → InputReader

5.2 设备移除

复制代码
用户拔出USB键盘
    ↓ 内核删除 /dev/input/eventX
    ↓ inotify 上报 IN_DELETE
    ↓ closeDeviceLocked()
      ├── epoll_ctl(DEL) 移出监控
      ├── close(fd) 关闭设备
      └── 从 mDevices 移除
    ↓ 生成 DEVICE_REMOVED RawEvent → InputReader

六、唤醒管道(Wake Pipe)

EventHub 设计了一个巧妙的唤醒机制:

cpp 复制代码
// 构造函数中创建管道
pipe(mWakeReadPipeFd, mWakeWritePipeFd);
epoll_ctl(mEpollFd, EPOLL_CTL_ADD, mWakeReadPipeFd, &eventItem);

// 被外部调用,唤醒阻塞中的 getEvents
void EventHub::wake() {
    write(mWakeWritePipeFd, "W", 1);
}

当系统需要关闭输入系统时(如关机),InputReader 正阻塞在 epoll_wait 中。wake() 向管道写一个字节,epoll_wait 立刻返回,getEvents 即可正常退出。


七、设备配置与键盘映射

7.1 .idc 文件(Input Device Configuration)

EventHub 打开设备时会查找对应的配置文件:

复制代码
/system/usr/idc/Vendor_XXXX_Product_XXXX.idc
/data/system/devices/idc/Vendor_XXXX_Product_XXXX.idc

.idc 可覆盖设备属性,例如:

复制代码
device.type = touchScreen
touch.orientationAware = 1

7.2 .kl 文件(Key Layout)

对于键盘设备,EventHub 加载 .kl 文件定义 Linux 键码到 Android 键码的映射:

复制代码
/system/usr/keylayout/Generic.kl
/system/usr/keylayout/Vendor_XXXX_Product_XXXX.kl

典型的 .kl 文件格式:

复制代码
key 1     ESCAPE
key 2     1
key 14    DEL
key 29    A
key 30    B
key 114   VOLUME_DOWN
key 115   VOLUME_UP
key 116   POWER             WAKE

KeyMap 的加载发生在 openDeviceLocked() 中,InputReader 后续用它进行键码转换。


八、线程模型

EventHub 本身没有自己的线程,所有方法都在调用方线程中执行:

复制代码
InputReader 线程
    └── loopOnce()
          └── mEventHub->getEvents(timeout, buffer, count)
               └── epoll_wait(...)  ← 在 InputReader 线程中阻塞等待

九、总结

EventHub 用约 1000 行 C++ 代码,优雅地实现了:

能力 实现手段
设备发现 inotify 监听 /dev/input/
多设备并发监听 epoll 同时监控所有设备 fd
设备能力识别 ioctl(EVIOCGBIT) 获取事件类型位图
阻塞退出 pipe + wake() 通知机制
设备分类 位掩码 INPUT_DEVICE_CLASS_*
键码映射 加载 .kl 文件建立查找表
批量读取 循环 read() 直到 EAGAIN

理解 EventHub 后,接下来我们看 InputReader 如何将原始 RawEvent 加工成 Android 能理解的 KeyEvent 和 MotionEvent。