2. EventHub源码分析:初始化、打开设备节点与epoll机制
好,咱们今天来啃一块硬骨头——EventHub。说实话,我在刚接触Android输入系统时,EventHub这层是最让我头疼的。它不像InputReader那样逻辑清晰,也不像InputDispatcher那样有明确的事件流。EventHub就像个「幕后管家」,默默处理着最底层的脏活累活。
但恰恰是这层,决定了整个输入系统的根基。你想想看,如果EventHub初始化失败,后面所有的事件分发都是空中楼阁。所以,咱们今天就把EventHub的初始化、设备节点打开、以及epoll监听机制,彻底讲透。
2.1 EventHub的初始化流程
EventHub的初始化,说白了就是三件事:打开输入设备、创建epoll句柄、注册监听。咱们直接看代码,我挑关键部分讲。
// EventHub.cpp 构造函数核心片段
EventHub::EventHub() :
mEpollFd(epoll_create(EPOLL_SIZE_HINT)) { // 创建epoll实例
// 打开系统输入设备目录
mInputDeviceScan = new InputDeviceScanner();
// 扫描 /dev/input/ 目录下的所有设备
scanDevicesLocked();
// 创建用于唤醒的pipe
int wakeFds[2];
pipe(wakeFds);
mWakeReadPipeFd = wakeFds[0];
mWakeWritePipeFd = wakeFds[1];
// 将wake pipe也加入epoll监听
epoll_ctl(mEpollFd, EPOLL_CTL_ADD, mWakeReadPipeFd, &eventItem);
}
嗯,这里要注意一个细节:epoll_create的参数EPOLL_SIZE_HINT,在Android中定义的是8。为什么是8?我记得当时看代码时也疑惑过,后来发现这其实只是个hint,内核会动态扩展。说白了,这个值设多大都行,内核不鸟你。
核心要点:EventHub的构造函数做了三件关键事:
- 创建epoll实例(mEpollFd)
- 扫描并打开所有输入设备节点
- 创建wake pipe并注册到epoll
2.2 打开设备节点:scanDevicesLocked()
接下来咱们看看设备节点是怎么打开的。这部分我建议你重点关注,因为我在项目中遇到过好几次设备节点打开失败导致触摸屏不响应的问题。
// 扫描设备的核心逻辑
status_t EventHub::scanDevicesLocked() {
// 遍历 /dev/input/ 目录
DIR *dir = opendir("/dev/input");
if (dir == NULL) return NO_INIT;
struct dirent *entry;
while ((entry = readdir(dir)) != NULL) {
// 只处理 event 开头的设备节点
if (strncmp(entry->d_name, "event", 5) == 0) {
char devPath[PATH_MAX];
snprintf(devPath, sizeof(devPath), "/dev/input/%s", entry->d_name);
// 打开设备节点
int fd = open(devPath, O_RDWR | O_CLOEXEC);
if (fd < 0) {
// 我曾经遇到过权限问题导致打开失败
// 特别是某些定制ROM会修改设备节点权限
continue;
}
// 创建设备信息并注册到epoll
Device* device = new Device(fd, entry->d_name);
registerDeviceForEpoll(device);
}
}
closedir(dir);
return OK;
}
这里有个坑,我必须要说。你看代码里用了O_CLOEXEC标志,这个标志的作用是:当进程执行exec()时自动关闭这个文件描述符。为什么要这么做?
我曾经在调试一个双屏异显项目时,发现子进程莫名其妙持有了输入设备的fd,导致主进程无法正常监听。查了两天才发现是忘记加O_CLOEXEC。嗯,从那以后我写任何打开设备节点的代码,都会习惯性加上这个标志。
避坑指南:
- 设备节点打开失败时,不要直接返回错误,应该跳过继续扫描其他设备
- 务必使用O_CLOEXEC标志,防止fd泄露到子进程
- 扫描完成后记得closedir(),否则会造成文件描述符泄漏
2.3 epoll机制:事件监听的基石
好,设备节点打开了,接下来就是怎么监听这些设备的事件。Android选择了epoll,而不是select或poll。为什么?
你想想看,一个Android设备可能有触摸屏、按键、鼠标、手柄等多个输入设备。如果用select,每次都要遍历所有fd,效率太低了。epoll的优势在于:它只返回有事件发生的fd,不需要遍历。
咱们看看EventHub是怎么用epoll的:
// 注册设备到epoll
void EventHub::registerDeviceForEpoll(Device* device) {
struct epoll_event eventItem;
memset(&eventItem, 0, sizeof(eventItem));
eventItem.events = EPOLLIN; // 只监听可读事件
eventItem.data.u32 = device->id;
// 添加到epoll实例
int result = epoll_ctl(mEpollFd, EPOLL_CTL_ADD, device->fd, &eventItem);
if (result == -1) {
// 注册失败的处理
close(device->fd);
delete device;
}
}
// 等待事件的核心循环
int EventHub::getEvents(int timeoutMillis) {
struct epoll_event eventItems[EPOLL_MAX_EVENTS];
// 阻塞等待事件,timeoutMillis为超时时间
int eventCount = epoll_wait(mEpollFd, eventItems, EPOLL_MAX_EVENTS, timeoutMillis);
if (eventCount < 0) {
// 被信号中断等情况
return -errno;
}
// 遍历所有有事件的fd
for (int i = 0; i < eventCount; i++) {
uint32_t epollEvents = eventItems[i].events;
uint32_t deviceId = eventItems[i].data.u32;
// 处理事件...
if (epollEvents & EPOLLIN) {
// 读取输入事件
readDeviceEvents(deviceId);
}
}
return eventCount;
}
个人经验:我在优化一个车载系统的触摸响应延迟时,发现epoll_wait的超时时间设置很关键。如果设得太短,CPU空转浪费电量;设得太长,触摸响应会有延迟感。Android默认的超时是-1(无限等待),但实际场景中,我建议根据具体设备调整到50-100ms。
2.4 epoll的边缘触发 vs 水平触发
这里有个技术细节,我觉得值得单独拿出来讲。epoll有两种触发模式:边缘触发(ET)和水平触发(LT)。
| 触发模式 | 特点 | EventHub使用情况 |
|---|---|---|
| 水平触发(LT) | 只要fd有数据可读,每次epoll_wait都会返回 | ✅ 默认使用 |
| 边缘触发(ET) | 只在状态变化时通知一次,需要一次性读完所有数据 | ❌ 未使用 |
为什么EventHub选择水平触发?我个人理解是:简单可靠。水平触发模式下,即使你某次没读完数据,下次epoll_wait还会通知你。而边缘触发要求你必须一次性读完,否则就会丢失事件。对于输入系统这种对可靠性要求极高的场景,水平触发是更稳妥的选择。
我记得有一次,团队里有个同事想优化性能,把EventHub改成了边缘触发。结果呢?触摸屏偶尔会丢事件,用户反馈说「点一下没反应,要点两下」。最后查出来就是边缘触发导致的事件丢失。嗯,从那以后,我再也不建议在输入系统里用边缘触发。
2.5 wake机制:如何优雅地中断epoll_wait
最后,咱们聊聊EventHub的wake机制。你可能会问:epoll_wait是阻塞的,如果我想主动唤醒它怎么办?
EventHub的做法是:创建一个pipe,把pipe的读端也注册到epoll中。当需要唤醒时,往pipe的写端写入一个字节,epoll_wait就会立即返回。
// 唤醒epoll_wait
void EventHub::wake() {
// 写入一个字节到wake pipe
const uint64_t ONE = 1;
ssize_t nWrite = write(mWakeWritePipeFd, &ONE, sizeof(ONE));
if (nWrite != sizeof(ONE)) {
// 写入失败的处理
}
}
// 消费wake事件
void EventHub::consumeWakeEvent() {
uint64_t u64;
ssize_t nRead = read(mWakeReadPipeFd, &u64, sizeof(u64));
// 读取并丢弃wake事件
}
这个设计很巧妙。你想想看,如果没有这个wake机制,当InputReader想要通知EventHub「有新设备插入了」,但EventHub正阻塞在epoll_wait上,那怎么办?有了wake pipe,InputReader只需要写一个字节,EventHub就能立即醒来处理新设备。
总结一下EventHub的核心设计思想:
- 用epoll统一管理所有输入设备的fd,实现高效的事件监听
- 通过wake pipe实现主动唤醒,打破epoll_wait的阻塞
- 水平触发模式保证事件不丢失,牺牲一点性能换取可靠性
- 设备节点扫描采用「跳过失败」策略,保证单个设备故障不影响整体
好了,EventHub的初始化、设备节点打开和epoll机制,咱们就讲到这里。下一章我会深入分析EventHub是如何读取原始输入事件,并转换成InputReader能理解的格式的。到时候咱们再聊。
公众号:蓝海资料掘金营,微信deep3321