系列目录 :第一篇:从硬件到应用的事件旅程 | 第二篇:EventHub --- 原始事件的采集者 | 第三篇:InputReader --- 原始事件到Android事件的转换引擎 | 第四篇:InputDispatcher --- 事件分发与ANR超时机制 | 第五篇:应用侧 --- InputChannel、ViewRootImpl与事件消费
一、EventHub 在整个输入系统中的位置
回顾第一篇的总览,EventHub 处于整个事件流水线的最上游:
/dev/input/eventX → EventHub → InputReader → InputDispatcher → APP
▲
本篇聚焦
EventHub 的职责一句话概括:监听所有输入设备,读取内核上报的原始事件,并把它们交给 InputReader 。看似简单,但实现中涉及 Linux 的 inotify、epoll、ioctl 等多个系统调用,是理解 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。