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

📅 2026/6/27 3:43:41
Android 7系统输入(二):EventHub — 原始事件的采集者
系列目录第一篇从硬件到应用的事件旅程 |第二篇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结构体来维护状态structDevice{intfd;// 设备文件描述符String8 path;// 设备路径如 /dev/input/event2DeviceIdentifier identifier;// 设备唯一标识总线类型、厂商ID、产品ID等uint32_tclasses;// 设备类别掩码INPUT_DEVICE_CLASS_*KeyedVectorint,RawAbsoluteAxisInfoabsoluteAxes;// 绝对坐标轴信息String8 configurationFile;PropertyMap configuration;KeyMap keyMap;// 键盘映射boolenabled;boolignored;};其中classes是最关键的分类字段通过一系列标志位来标识设备类型enum{INPUT_DEVICE_CLASS_KEYBOARD0x00000001,// 键盘INPUT_DEVICE_CLASS_ALPHAKEY0x00000002,// 字母键盘INPUT_DEVICE_CLASS_TOUCH0x00000004,// 触摸屏/触摸板INPUT_DEVICE_CLASS_CURSOR0x00000008,// 鼠标/轨迹球INPUT_DEVICE_CLASS_TOUCH_MT0x00000010,// 多点触控屏INPUT_DEVICE_CLASS_DPAD0x00000020,// 方向键INPUT_DEVICE_CLASS_GAMEPAD0x00000040,// 游戏手柄INPUT_DEVICE_CLASS_SWITCH0x00000080,// 开关类INPUT_DEVICE_CLASS_JOYSTICK0x00000100,// 摇杆/游戏手柄INPUT_DEVICE_CLASS_VIBRATOR0x00000200,// 振动器INPUT_DEVICE_CLASS_MIC0x00000400,// 麦克风INPUT_DEVICE_CLASS_EXTERNAL_STYLUS0x00000800,// 外接手写笔INPUT_DEVICE_CLASS_ROTARY_ENCODER0x00001000,// 旋转编码器INPUT_DEVICE_CLASS_VIRTUAL0x40000000,// 虚拟设备INPUT_DEVICE_CLASS_EXTERNAL0x80000000,// 外部设备};2.2 RawEvent 结构体EventHub 从内核读取input_event后封装为RawEvent传递给 InputReaderstructRawEvent{nsecs_t when;// 事件时间戳纳秒int32_tdeviceId;// 设备IDEventHub内部分配int32_ttype;// 事件类型EV_KEY, EV_ABS, EV_SYN 等int32_tcode;// 事件编码int32_tvalue;// 事件值};和内核的input_event相比RawEvent多了deviceId字段——EventHub 内部为每个设备分配的唯一 ID后续 InputReader 用它识别事件来源。三、EventHub 的初始化流程EventHub在其构造函数中完成了大量初始化工作EventHub::EventHub(void){acquire_wake_lock(PARTIAL_WAKE_LOCK,WAKE_LOCK_ID);// 1. 创建 epoll 实例mEpollFdepoll_create(EPOLL_SIZE_HINT);// 2. 创建 inotify 实例监听 /dev/input/ 目录mINotifyFdinotify_init();inotify_add_watch(mINotifyFd,/dev/input,IN_DELETE|IN_CREATE);// 3. 将 inotify fd 加入 epoll 监控structepoll_eventeventItem;memset(eventItem,0,sizeof(eventItem));eventItem.eventsEPOLLIN;eventItem.data.u32EPOLL_ID_INOTIFY;epoll_ctl(mEpollFd,EPOLL_CTL_ADD,mINotifyFd,eventItem);// 4. 创建唤醒管道用于退出等待intwakeFds[2];pipe(wakeFds);mWakeReadPipeFdwakeFds[0];mWakeWritePipeFdwakeFds[1];fcntl(mWakeReadPipeFd,F_SETFL,O_NONBLOCK);fcntl(mWakeWritePipeFd,F_SETFL,O_NONBLOCK);eventItem.data.u32EPOLL_ID_WAKE;epoll_ctl(mEpollFd,EPOLL_CTL_ADD,mWakeReadPipeFd,eventItem);// 5. 检测内核是否支持 EPOLLWAKEUP3.5intmajor,minor;getLinuxRelease(major,minor);mUsingEpollWakeupmajor3||(major3minor5);// 6. 扫描已有设备scanDevicesLocked();}初始化时创建了三个关键文件描述符加入 epoll 监控被监控的 fd用途epoll data 标识mINotifyFd监听/dev/input/目录变更EPOLL_ID_INOTIFYmWakeReadPipeFd唤醒管道用于退出等待EPOLL_ID_WAKE各设备 fd读取各输入设备的原始事件EPOLL_ID_DEVICE deviceId3.1 扫描已有设备voidEventHub::scanDevicesLocked(){DIR*diropendir(/dev/input);while((direntreaddir(dir))!NULL){if(strncmp(dirent-d_name,event,5)0){String8path(/dev/input/);path.append(dirent-d_name);openDeviceLocked(path);}}closedir(dir);}3.2 打开并配置设备status_tEventHub::openDeviceLocked(constchar*devicePath){// 1. 以非阻塞方式打开设备intfdopen(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_tclasses0;if(ev_bits(1EV_KEY))classes|INPUT_DEVICE_CLASS_KEYBOARD;if(ev_bits(1EV_ABS)){if(test_bit(ABS_MT_POSITION_X,abs_bits))classes|INPUT_DEVICE_CLASS_TOUCH_MT;elseclasses|INPUT_DEVICE_CLASS_TOUCH;}if(ev_bits(1EV_REL))classes|INPUT_DEVICE_CLASS_CURSOR;// 4. 创建设备对象并加入 epollDevice*devicenewDevice(fd,devicePath,identifier,classes);structepoll_eventeventItem;eventItem.eventsEPOLLIN;eventItem.data.u32EPOLL_ID_DEVICEdeviceId;// 用 u32 携带 deviceIdepoll_ctl(mEpollFd,EPOLL_CTL_ADD,fd,eventItem);// 5. 加载键盘映射文件.kldevice-keyMap.load(identifier,configuration);mDevices.add(deviceId,device);returnOK;}关键设计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. 返回 eventCount4.2 源码逐步解读size_tEventHub::getEvents(inttimeoutMillis,RawEvent*buffer,size_t bufferSize){size_t eventCount0;structepoll_eventpendingEventItems[EPOLL_MAX_EVENTS];{AutoMutex_l(mLock);// 步骤1: 处理待处理的设备变更事件 while(mPendingINotifyeventCountbufferSize){// 生成 DEVICE_ADDED / DEVICE_REMOVED 类型的 RawEvent...}// 步骤2: epoll_wait 等待 if(eventCount0){mLock.unlock();// 关键释放锁再等待pollResultepoll_wait(mEpollFd,pendingEventItems,EPOLL_MAX_EVENTS,timeoutMillis);mLock.lock();// 重新获取锁}}// 步骤3: 处理 epoll 返回的事件 for(inti0;ipollResult;i){conststructepoll_eventitempendingEventItems[i];ssize_t deviceIndexitem.data.u32-EPOLL_ID_DEVICE;if(deviceIndex0){// --- 3a. 设备事件 ---Device*devicegetDeviceByIndexLocked(deviceIndex);while(eventCountbufferSize){structinput_eventiev;ssize_t readSizeread(device-fd,iev,sizeof(iev));if(readSizesizeof(iev)){RawEvent*eventbuffer[eventCount];event-whensystemTime(SYSTEM_TIME_MONOTONIC);event-deviceIddevice-id;event-typeiev.type;event-codeiev.code;event-valueiev.value;}elseif(readSize0errnoEAGAIN){break;// 没有更多数据}else{closeDeviceLocked(*device);// 设备故障break;}}}elseif(item.data.u32EPOLL_ID_INOTIFY){// --- 3b. 设备热插拔 ---readNotifyLocked();}elseif(item.data.u32EPOLL_ID_WAKE){// --- 3c. 唤醒事件 ---charbuffer[16];read(mWakeReadPipeFd,buffer,sizeof(buffer));}}returneventCount;}4.3 关键设计细节1锁的释放时机epoll_wait调用前释放锁这很关键。如果持锁等待InputReader 调用getEvents时会阻塞热插拔回调。释放锁后既能及时响应设备事件又不影响并发访问。2事件批量读取每次epoll_wait返回后EventHub 循环read()直到EAGAIN。一次epoll_wait唤醒可能对应多个内核事件多点触控多个手指同时移动批量读取减少系统调用。3EV_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 → InputReader5.2 设备移除用户拔出USB键盘 ↓ 内核删除 /dev/input/eventX ↓ inotify 上报 IN_DELETE ↓ closeDeviceLocked() ├── epoll_ctl(DEL) 移出监控 ├── close(fd) 关闭设备 └── 从 mDevices 移除 ↓ 生成 DEVICE_REMOVED RawEvent → InputReader六、唤醒管道Wake PipeEventHub 设计了一个巧妙的唤醒机制// 构造函数中创建管道pipe(mWakeReadPipeFd,mWakeWritePipeFd);epoll_ctl(mEpollFd,EPOLL_CTL_ADD,mWakeReadPipeFd,eventItem);// 被外部调用唤醒阻塞中的 getEventsvoidEventHub::wake(){write(mWakeWritePipeFd,W,1);}当系统需要关闭输入系统时如关机InputReader 正阻塞在epoll_wait中。wake()向管道写一个字节epoll_wait立刻返回getEvents即可正常退出。七、设备配置与键盘映射7.1 .idc 文件Input Device ConfigurationEventHub 打开设备时会查找对应的配置文件/system/usr/idc/Vendor_XXXX_Product_XXXX.idc /data/system/devices/idc/Vendor_XXXX_Product_XXXX.idc.idc可覆盖设备属性例如device.type touchScreen touch.orientationAware 17.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 WAKEKeyMap 的加载发生在openDeviceLocked()中InputReader 后续用它进行键码转换。八、线程模型EventHub 本身没有自己的线程所有方法都在调用方线程中执行InputReader 线程 └── loopOnce() └── mEventHub-getEvents(timeout, buffer, count) └── epoll_wait(...) ← 在 InputReader 线程中阻塞等待九、总结EventHub 用约 1000 行 C 代码优雅地实现了能力实现手段设备发现inotify监听/dev/input/多设备并发监听epoll同时监控所有设备 fd设备能力识别ioctl(EVIOCGBIT)获取事件类型位图阻塞退出pipewake()通知机制设备分类位掩码INPUT_DEVICE_CLASS_*键码映射加载.kl文件建立查找表批量读取循环read()直到EAGAIN理解 EventHub 后接下来我们看 InputReader 如何将原始 RawEvent 加工成 Android 能理解的 KeyEvent 和 MotionEvent。