Android Input——输入子系统(三)
在之前的篇章中,我们已经对 Android 输入模块的组成有了大致的了解,包括从硬件事件的读取到应用层事件分发的全流程。本篇我们将深入探讨 Android 输入子系统如何通过 Linux 内核收集硬件设备产生的输入事件,以及这些事件是如何被转换和传递给用户空间的应用程序进行处理的。
一、输入子系统概述
Android 设备可以同时连接多个输入设备,如触摸屏、键盘、鼠标等。每当用户在任何一个设备上产生输入操作时,都会触发一个中断。这些中断经过 Linux 内核的中断处理机制以及相应的设备驱动程序转换为事件(Event),并最终传递给用户空间的应用程序进行处理。
同时,每个输入设备都有其特定的驱动程序,数据接口也各不相同。要在单个线程(例如 InputReader Thread)中捕获所有用户的输入,Linux 内核的输入子系统发挥了关键作用。该子系统在各种设备驱动程序之上提供了一个抽象层,只要底层设备驱动程序遵循这一抽象接口实现,上层应用就可以通过统一的接口访问所有的输入设备。这种设计极大地简化了多输入设备的支持和管理。
1、抽象层关键概念
在 Linux 内核的输入子系统中,为了实现对多种输入设备的支持,并提供统一的接口给用户空间的应用程序使用,定义了三个核心的概念:input_dev、input_handler 和 input handle。这些概念共同工作,确保从硬件中断到用户空间事件的高效转换和传递。它们的关系如下图所示:
上图中 4 个 evdev 的 handle 就对应了 /dev/input/ 下的四个文件"event0 - event3",通过 input handle 可以找到对应的 input_handler 和 input_dev。其中节点文件信息如下:
sa8511_xiaoxu:/dev/input $ ls
event0 event1 event2 event3 event4 event5 event6
input_dev
- 功能:表示底层驱动。是内核中用于表示物理输入设备的数据结构。每个实际存在的输入设备(如触摸屏、键盘、鼠标等)都对应一个 input_dev 实例。
- 角色:它负责描述设备的基本信息和状态,比如设备支持的事件类型(按键、移动、绝对坐标等)、设备名称、ID 等。此外,input_dev 还提供了与具体硬件交互的方法,例如读取设备寄存器或缓冲区来获取用户的输入数据。
- 特点:一个 input_dev 可以关联多个 input_handler,这意味着同一设备可以被不同类型的处理器处理,以适应不同的应用场景。
input_handler
- 功能:输入处理方法。表示某类输入设备的处理方法,可以视为一种“上层驱动”。它定义了如何处理来自特定类型输入设备的事件。
- 角色:当某个事件发生时,input_handler 负责解释这个事件的意义,并决定如何进一步处理或转发该事件。例如,它可以将事件发送到用户空间,或者根据事件内容执行特定的操作。
- 特点:一个 input_handler 可以应用于多种不同的 input_dev。这允许系统通过编写少量的通用处理逻辑来支持广泛的设备类型,提高了代码的复用性和灵活性。
input handle
- 功能:设备与处理器之间的桥梁。用来关联某个 input_dev 和某个 input_handler。它是两者之间的连接点,确保正确的事件能够到达适当的处理器进行处理。
- 角色:每当一个新的 input_dev 或 input_handler 注册到系统时,内核会尝试为它们寻找合适的匹配项,并创建相应的 input handle 实例。这种机制保证了即使有多个 input_handler 同时存在,也能准确地将事件路由到正确的处理器。
- 特点:每个 input handle 都会在文件系统中生成一个文件节点(通常是 /dev/input/eventX),使得用户空间的应用程序可以通过标准的文件操作接口访问这些事件。这样做的好处是简化了应用程序开发,因为开发者不需要直接与底层硬件打交道。
当用户与输入设备互动时,产生的中断首先由对应的 input_dev 捕获,并将其转换成内核可识别的事件格式。接着,这些事件通过已建立的 input handle 关系被传递给适当的 input_handler 进行处理。最后,处理后的事件会被写入到相应的 /dev/input/eventX 文件节点,供用户空间的应用程序读取和响应。
2、获取输入流程
- 设备注册:输入设备驱动程序使用 input_register_dev 函数将其对应的 input_dev 结构注册到输入子系统中。每个物理输入设备(如触摸屏、键盘等)都需要进行此步骤。
- 处理器注册:不同类型的输入处理器(例如,用于处理按键事件、鼠标移动事件等的处理器)通过 input_register_handler 函数将其对应的 input_handler 注册到输入子系统中。
- 建立连接并生成设备节点:每当一个新的 input_dev 或 input_handler 被注册时,系统会自动尝试为它们寻找合适的匹配项,并通过调用 input_connect() 方法创建相应的 input_handle 实例。每个 input_handle 都会在 /dev/input/ 目录下生成一个对应的设备节点文件(例如 event0, event1 等),这使得用户空间的应用程序可以通过这些文件节点与底层硬件交互。
- 应用程序监听输入事件:应用程序可以通过打开 /dev/input/ 下的设备节点文件来监听特定输入设备的事件。一旦某个输入设备产生事件(比如用户按下了一个键),底层驱动就会捕捉到该事件并通过与其关联的 input_handler 进行处理。处理后的事件可以被发送回用户空间,供应用程序进一步处理或响应。
因此,只要打开 /dev/input/ 下的所有 event* 设备文件,我们就可以有办法获取所有输入设备的输入事件,不管它是触摸屏,还是一个USB设备,还是一个个红外遥控器。Android 中完成这个工作的就是 EventHub。它在 InputManager 的初始化中被创建。
二、EventHub
说到输入子系统,不得不提 EventHub,它是底层 event 处理的枢纽,并向上层传递。
源码位置:/frameworks/native/services/inputflinger/reader/EventHub.cpp
1、构造函数
EventHub::EventHub(void) : mBuiltInKeyboardId(NO_BUILT_IN_KEYBOARD),
mNextDeviceId(1), mControllerNumbers(), mNeedToSendFinishedDeviceScan(false),
mNeedToReopenDevices(false), mNeedToScanDevices(true), mPendingEventCount(0),
mPendingEventIndex(0), mPendingINotify(false) {
ensureProcessCanBlockSuspend();
// 创建epoll对象用于监听是否有可读事件,EPOLL_SIZE_HINT为最大监听8个
mEpollFd = epoll_create1(EPOLL_CLOEXEC);
……
// 创建inotify对象用于监视/dev/input目录下的文件变化(如设备的添加或删除)
mINotifyFd = inotify_init();
// 指定目录(/dev/input)上设置监听,关注 IN_DELETE 和 IN_CREATE 事件
mInputWd = inotify_add_watch(mINotifyFd, DEVICE_PATH, IN_DELETE | IN_CREATE);
……
struct epoll_event eventItem = {};
eventItem.events = EPOLLIN | EPOLLWAKEUP;
eventItem.data.fd = mINotifyFd;
// 将inotify对象注册到epoll中监听是否有新的可读的设备增删事件
int result = epoll_ctl(mEpollFd, EPOLL_CTL_ADD, mINotifyFd, &eventItem);
……
int wakeFds[2];
// 创建一个管道,用于在必要时唤醒epoll_wait函数,避免其因无事件而长时间阻塞。
// 管道的一端(读端)由 epoll 监听,另一端(写端)由 InputReader 使用来发送唤醒信号。
result = pipe(wakeFds);
mWakeReadPipeFd = wakeFds[0];
mWakeWritePipeFd = wakeFds[1];
// 设置管道两端为非阻塞模式,并将管道的读端注册到 epoll
result = fcntl(mWakeReadPipeFd, F_SETFL, O_NONBLOCK);
result = fcntl(mWakeWritePipeFd, F_SETFL, O_NONBLOCK);
eventItem.data.fd = mWakeReadPipeFd;
result = epoll_ctl(mEpollFd, EPOLL_CTL_ADD, mWakeReadPipeFd, &eventItem);
}
EventHub 在初始化时,构造函数中创建了两个Fd,mEpollFd 和 mlNotifyFd。其中
mlNotifyFd 用于监听设备节点是否有设备文件的增删,并将 mINotifyFd 主册到 mEpollFd 中,当发生新的设备增删,设备节点下的设备文件也会随之增删,就会通知 mEpollFd 有新的可读的设备
增删事件,通知 EventHub 对设备进行处理。而 mEpollFd 监听是否有可读事件,创建管道,并将读端交给 epoll,写端交给 InputReader 来处理事件。
通过上述步骤,EventHub 完成了对输入设备事件的监听准备。它不仅能够监听现有输入设备的变化(通过 inotify),还能够高效地处理多个输入源的事件(通过 epoll)。此外,通过使用管道实现的唤醒机制,EventHub 可以灵活地控制何时处理事件,从而提高了整个输入系统的响应速度和效率。
InputReader 会在每次 loop 时, 调用 EventHub 的 getEvents 来获取input事件。
2、getEvents
size_t EventHub::getEvents(int timeoutMillis, RawEvent* buffer, size_t bufferSize) {
……
RawEvent* event = buffer;
size_t capacity = bufferSize;
bool awoken = false;
for (;;) {
nsecs_t now = systemTime(SYSTEM_TIME_MONOTONIC);
// 如果需要,重新打开输入设备
if (mNeedToReopenDevices) {
mNeedToReopenDevices = false;
closeAllDevicesLocked();
mNeedToScanDevices = true;
break; // return to the caller before we actually rescan
}
// 报告最近添加/删除的任何设备
for (auto it = mClosingDevices.begin(); it != mClosingDevices.end();) {
std::unique_ptr<Device> device = std::move(*it);
ALOGV("Reporting device closed: id=%d, name=%s\n", device->id, device->path.c_str());
event->when = now;
event->deviceId = (device->id == mBuiltInKeyboardId)
? ReservedInputDeviceId::BUILT_IN_KEYBOARD_ID : device->id;
event->type = DEVICE_REMOVED;
event += 1;
it = mClosingDevices.erase(it);
mNeedToSendFinishedDeviceScan = true;
if (--capacity == 0) {
break;
}
}
// 扫描新的输入设备,第一次为true
if (mNeedToScanDevices) {
mNeedToScanDevices = false;
// 打开 /dev/input/ 目录下的input设备后,将其注册到epoll的监控队列中。
scanDevicesLocked();
mNeedToSendFinishedDeviceScan = true;
}
// 报告设备添加事件
while (!mOpeningDevices.empty()) {
std::unique_ptr<Device> device = std::move(*mOpeningDevices.rbegin());
mOpeningDevices.pop_back();
ALOGV("Reporting device opened: id=%d, name=%s\n", device->id, device->path.c_str());
event->when = now;
event->deviceId = device->id == mBuiltInKeyboardId ? 0 : device->id;
// 对于新开的设备,生成一个 DEVICE_ADDED 类型的 RawEvent 并添加到输出缓冲区。
event->type = DEVICE_ADDED;
event += 1;
// 尝试为设备匹配相应的视频设备(如触摸屏),并将设备信息插入到 mDevices 映射中
for (auto it = mUnattachedVideoDevices.begin();
it != mUnattachedVideoDevices.end(); it++) {
std::unique_ptr<TouchVideoDevice>& videoDevice = *it;
if (tryAddVideoDeviceLocked(*device, videoDevice)) {
// videoDevice was transferred to 'device'
it = mUnattachedVideoDevices.erase(it);
break;
}
}
auto [dev_it, inserted] = mDevices.insert_or_assign(device->id, std::move(device));
if (!inserted) {
ALOGW("Device id %d exists, replaced.", device->id);
}
mNeedToSendFinishedDeviceScan = true;
if (--capacity == 0) {
break;
}
}
// 发送设备扫描完成通知
if (mNeedToSendFinishedDeviceScan) {
mNeedToSendFinishedDeviceScan = false;
event->when = now;
event->type = FINISHED_DEVICE_SCAN;
event += 1;
if (--capacity == 0) {
break;
}
}
// 处理待处理事件队列中的下一个输入事件
bool deviceChanged = false;
while (mPendingEventIndex < mPendingEventCount) {
const struct epoll_event& eventItem = mPendingEventItems[mPendingEventIndex++];
// 处理INotify事件
if (eventItem.data.fd == mINotifyFd) {
if (eventItem.events & EPOLLIN) {
mPendingINotify = true;
}
……
continue;
}
// 处理唤醒管道事件
if (eventItem.data.fd == mWakeReadPipeFd) {
if (eventItem.events & EPOLLIN) {
ALOGV("awoken after wake()");
awoken = true;
char wakeReadBuffer[16];
ssize_t nRead;
do {
nRead = read(mWakeReadPipeFd, wakeReadBuffer, sizeof(wakeReadBuffer));
} while ((nRead == -1 && errno == EINTR) || nRead == sizeof(wakeReadBuffer));
}
……
continue;
}
// 处理输入设备事件
Device* device = getDeviceByFdLocked(eventItem.data.fd);
……
// 处理触摸屏输入事件
if (device->videoDevice && eventItem.data.fd == device->videoDevice->getFd()) {
……
continue;
}
// 处理标准输入设备事件
// 对于标准输入设备(如键盘、鼠标等),检查是否有可读事件 (EPOLLIN)
if (eventItem.events & EPOLLIN) {
// 从device中得到fd后再去读取设备,获取input事件
int32_t readSize = read(device->fd, readBuffer, sizeof(struct input_event) * capacity);
if (readSize == 0 || (readSize < 0 && errno == ENODEV)) {
// 读取失败或设备已被移除,则关闭该设备
deviceChanged = true;
closeDeviceLocked(*device);
}
……
else {
// 读取成功,将每个input_event转换为RawEvent并添加到输出缓冲区中
int32_t deviceId = device->id == mBuiltInKeyboardId ? 0 : device->id;
size_t count = size_t(readSize) / sizeof(struct input_event);
for (size_t i = 0; i < count; i++) {
struct input_event& iev = readBuffer[i];
event->when = processEventTimestamp(iev);
event->readTime = systemTime(SYSTEM_TIME_MONOTONIC);
event->deviceId = deviceId;
event->type = iev.type;
event->code = iev.code;
event->value = iev.value;
event += 1;
capacity -= 1;
}
if (capacity == 0) {
// 缓冲区已满。重置挂起的事件索引,等待下一次再次尝试读取设备。
mPendingEventIndex -= 1;
break;
}
}
} else if (eventItem.events & EPOLLHUP) {
// 处理挂起事件 (EPOLLHUP),则关闭该设备
deviceChanged = true;
closeDeviceLocked(*device);
}
……
}
// 如果存在未处理的 INotify 事件并且所有待处理事件都已处理完毕
if (mPendingINotify && mPendingEventIndex >= mPendingEventCount) {
mPendingINotify = false;
// 处理设备节点的变化
readNotifyLocked();
deviceChanged = true;
}
// 报告添加或移除的设备
if (deviceChanged) {
continue;
}
……
// 等待更多事件的到来
int pollResult = epoll_wait(mEpollFd, mPendingEventItems, EPOLL_MAX_EVENTS, timeoutMillis);
……
// 所有操作完成后,返回我们读取的事件数
return event - buffer;
}
该函数是 Android 输入系统中用于从各种输入设备读取原始事件的核心方法。它负责处理设备的添加和移除、读取来自这些设备的实际输入事件,并将这些事件转换为内部使用的 RawEvent 格式以便后续处理。
第一次 getEvents 时,打开 /dev/input/ 目录下的 input 设备,并将其注册到 epoll 的监控队列中。这样一旦对应设备上有可读的 input 事件,则 epool_wait() 就会返回,并带回 deviceid,找到具体的 device。整个事件的获取中,除了 input 事件,设备的打开关闭等信息,也要包装成 event,上报给 InputReader。简单理解,EventHub 就是 InputReader 用于打开和关闭 input 设备,监听和读取 Input 事件的对象。