Android 输入系统【3】输入事件的起点


Android 输入系统系列文章:

【1】通过 IMS 的创建理解 Android 的输入流程

【2】Native 层输入事件的处理机制

在前面的文章中我们分析过 IMS 在 native 层的 InputManager 中,事件的传递流程:

InputReader -> UnwantedInteractionBlocker -> InputClassifier -> InputDispatcher

最开始接触事件的角色是 InputReader ,它启动了一个 InputThread 来执行 loopOnce 函数:

void InputReader::loopOnce() {
    // ...
    size_t count = mEventHub->getEvents(timeoutMillis, mEventBuffer, EVENT_BUFFER_SIZE); // 【1】获取事件数量
    { // acquire lock
        std::scoped_lock _l(mLock);
        mReaderIsAliveCondition.notify_all(); 
      
        if (count) {
            processEventsLocked(mEventBuffer, count); // 【2】处理事件
        }
        // timeout and input device changed process ...
    } // release lock

    // Send out a message that the describes the changed input devices.
    if (inputDevicesChanged) {
        mPolicy->notifyInputDevicesChanged(inputDevices); // 输入设备发生变化,发出消息
    }

    // 将排队的事件刷新到侦听器。
    mQueuedListener.flush();
}

在这个函数中描述的事件的获取以及将其进行处理的流程:

  • 【1】通过 mEventHub->getEvents 函数读取事件数量,实际上也是在这里将事件序列保存到了 mEventBuffer 指针中。
  • 【2】调用自己的 processEventsLocked 函数去加锁处理事件。

可以看出这里有一些重点信息:

  1. mEventHub
  2. mEventBuffer
  3. mEventHub 的 getEvents 函数
  4. InputReader 的 processEventsLocked 函数

最后一个 processEventsLocked 函数在上一篇文章中分析过,剩余的三个接下来逐个进行分析。

mEventHub

mEventHub 的定义如下:

// in InputReader.h
    // 这可能是 unique_ptr,但由于 InputReader 测试的编写方式,它在这里成为 shared_ptr。 在测试中,测试保留 EventHub 引用,同时将其传递给 InputReader。
    std::shared_ptr<EventHubInterface> mEventHub;

mEventHub 本意是一个指针,它的赋值是在 InputReader 的构造函数中:

InputReader::InputReader(std::shared_ptr<EventHubInterface> eventHub,
                         const sp<InputReaderPolicyInterface>& policy,
                         InputListenerInterface& listener)
      : mContext(this),
        mEventHub(eventHub), // 【here】
        mPolicy(policy),
        mQueuedListener(listener),
        mGlobalMetaState(AMETA_NONE),
        mLedMetaState(AMETA_NONE),
        mGeneration(1),
        mNextInputDeviceId(END_RESERVED_ID),
        mDisableVirtualKeysTimeout(LLONG_MIN),
        mNextTimeout(LLONG_MAX),
        mConfigurationChangesToRefresh(0) {
    refreshConfigurationLocked(0);
    updateGlobalMetaStateLocked();
}

在 InputReader 的创建流程中,InputManager 的构造函数中通过 createInputReader 函数创建了 mReader 指针:

InputManager::InputManager(
        const sp<InputReaderPolicyInterface>& readerPolicy,
        const sp<InputDispatcherPolicyInterface>& dispatcherPolicy) {
    // ... 
    mReader = createInputReader(readerPolicy, *mBlocker);
}

createInputReader 方法中,创建了 eventHub 实例:

std::unique_ptr<InputReaderInterface> createInputReader(
        const sp<InputReaderPolicyInterface>& policy, InputListenerInterface& listener) {
    return std::make_unique<InputReader>(std::make_unique<EventHub>(), policy, listener);
}

但准备工作此刻还没有结束,我们顺便看看 EventHub 的构造函数中做了些什么:

EventHub::EventHub(void)
      : mBuiltInKeyboardId(NO_BUILT_IN_KEYBOARD),
        mNextDeviceId(1),
        mControllerNumbers(),
        mNeedToSendFinishedDeviceScan(false),
        mNeedToReopenDevices(false),
        mNeedToScanDevices(true),
        mPendingEventCount(0),
        mPendingEventIndex(0),
        mPendingINotify(false) {
    ensureProcessCanBlockSuspend(); // 确保处理可以阻塞中断 

    mEpollFd = epoll_create1(EPOLL_CLOEXEC); // 创建一个系统的轮询,mEpollFd (文件描述符)代表结果
 
    mINotifyFd = inotify_init1(IN_CLOEXEC);

    std::error_code errorCode;
    bool isDeviceInotifyAdded = false;
    if (std::filesystem::exists(DEVICE_INPUT_PATH, errorCode)) {
        addDeviceInputInotify(); // 添加设备输入通知
    } else {
        addDeviceInotify(); // 添加设备通知
        isDeviceInotifyAdded = true;
        if (errorCode) {
            ALOGW("Could not run filesystem::exists() due to error %d : %s.", errorCode.value(), errorCode.message().c_str());
        }
    }
    // ...
}

在 EventHub 的构造方法中,重点的部分是根据 DEVICE_INPUT_PATH 输入设备路径是否存在,添加了不同的 Inotify 监听, addDeviceInputInotify 和 addDeviceInotify ,它们都是监控某一目录下的通知:

/**
 * 在没有任何输入设备的设备上(如某些开发板), /dev/input 目录将不存在。 然而,用户稍后仍可插入输入设备。
 * 仅当 /dev/input 出现时添加监视 /dev/input 的内容。
 */
void EventHub::addDeviceInputInotify() {
    mDeviceInputWd = inotify_add_watch(mINotifyFd, DEVICE_INPUT_PATH, IN_DELETE | IN_CREATE);
    LOG_ALWAYS_FATAL_IF(mDeviceInputWd < 0, "Could not register INotify for %s: %s",
                        DEVICE_INPUT_PATH, strerror(errno));
}

void EventHub::addDeviceInotify() {
    mDeviceWd = inotify_add_watch(mINotifyFd, DEVICE_PATH, IN_DELETE | IN_CREATE);
    LOG_ALWAYS_FATAL_IF(mDeviceWd < 0, "Could not register INotify for %s: %s", DEVICE_PATH,
                        strerror(errno));
}

这两个函数的区别是路径参数不同:

static const char* DEVICE_INPUT_PATH = "/dev/input";
// v4l2 devices go directly into /dev
static const char* DEVICE_PATH = "/dev";

这里是通过 Linux 的 inotify 机制(notify 是一种文件系统的变化通知机制如文件增加、删除等事件可以立刻让用户态得知) 告知系统输入设备的变化。这里只监听了添加和删除。

mEventBuffer

mEventBuffer 十分重要,实际上它就是保存读取到的事件的队列。它的定义是:

    // The event queue.
    static const int EVENT_BUFFER_SIZE = 256;
    RawEvent mEventBuffer[EVENT_BUFFER_SIZE] GUARDED_BY(mLock);

这是一个 RawEvent 类型的数组,容量是 256 。RawEvent 是从 EventHub 获取到的真实事件的结构体:

struct RawEvent {
    // Time when the event happened
    nsecs_t when;
    // EventHub 读取事件的时间。 仅为输入事件填充。
    // 对于其他事件(设备添加/删除/等),此值未定义且不应读取。
    nsecs_t readTime;
    int32_t deviceId;
    int32_t type;
    int32_t code;
    int32_t value;
};

mEventBuffer 的引用只有两个位置:

Android 输入系统【3】输入事件的起点
image-20221213184304846.png

前者接下来的 getEvents 函数中会进行分析;后者在 processEventsLocked 函数可以参考 【2】Native 层输入事件的处理机制 中的相关讲解。

整体逻辑是:在 getEvents 函数中,将读取到的事件保存在 mEventBuffer 数组中,然后交给 processEventsLocked 进行处理。

EventHub 的 getEvents 函数

InputReader 在 InputThread 线程中循环执行 loopOnce 方法直到 InputThread 被销毁,而在 loopOnce 中最重要的在于 EventHub 的 getEvents 函数。

size_t EventHub::getEvents(int timeoutMillis, RawEvent* buffer, size_t bufferSize) {

    std::scoped_lock _l(mLock);
    struct input_event readBuffer[bufferSize];
    RawEvent* event = buffer;
    size_t capacity = bufferSize;
    bool awoken = false;

    // 【核心逻辑】
    for (;;) { 
        // ...
    }
    
    // All donereturn the number of events we read.
    return event - buffer;
}

上面的代码是 getEvents 函数的大致逻辑,核心部分是中间的死循环中的代码。

在启动死循环之前,创建了一个锁、一个保存输入事件的 readBuffer ,这里的 bufferSize 是 EVENT_BUFFER_SIZE (256);将参数 buffer 用一个新的指针 event 标记,这里的 buffer 就是 mEventBuffer

最后的返回逻辑,是 event 指针在读取消息进行保存后的内存地址减去最开始的 mEventBuffer 指针指向的地址,也就是两者的差值即是此次 getEvents 方法调用读取到的事件数量。

核心循环部分

在这一部分逻辑比较多,下面描述一下整体的逻辑:

  1. 检查是否需要重新打开输入设备;
  2. 待移除的设备生成对应的事件,加入事件队列;
  3. 如有需要重新扫描设备;
  4. 待添加的设备生成对应的事件,加入事件队列;
  5. 设备扫描完成后生成对应的事件,加入事件队列;
  6. 处理待执行的事件,这里是核心的一步;
  7. 更新设备信息并生成设备变化事件,加入事件队列;
  8. 如果收集到事件或被唤醒,跳出循环;
  9. 若没有收集到事件或被唤醒,通过 epoll 机制轮询待执行的事件,保存到待执行的数组中。

1. 检查是否需要重新打开设备

        nsecs_t now = systemTime(SYSTEM_TIME_MONOTONIC);
        // Reopen input devices if needed. 
        if (mNeedToReopenDevices) {
            mNeedToReopenDevices = false;
            ALOGI("Reopening all input devices due to a configuration change.");
            // 关闭所有设备
            closeAllDevicesLocked();
            mNeedToScanDevices = true;
            break; // 在实际重新扫描设备前返回给调用者 
        }

如果需要重新打开设备,在这一步中先将所有的设备关闭,然后将 mNeedToScanDevices 置为 true,在下次循环的第三步中去重新扫描设备。

2. 生成设备移除事件

        for (auto it = mClosingDevices.begin(); it != mClosingDevices.end();) {
            std::unique_ptr<Device> device = std::move(*it);
            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;
            }
        }

启动了一个对 mClosingDevices 的遍历,也就是说,如果 mClosingDevices 中存在内容,将会对每个元素产生一个类型是 DEVICE_REMOVED 的 event,event += 1; 代表 mEventBuffer 中的元素在增加,mNeedToSendFinishedDeviceScan 变更为 true ,此标志位用于执行后续对于设备扫描完成后的处理工作。直到 mEventBuffer 数组中容纳不下新数据,跳出循环。

3. 如有需要重新扫描设备

        if (mNeedToScanDevices) {
            mNeedToScanDevices = false;
            scanDevicesLocked();
            mNeedToSendFinishedDeviceScan = true;
        }

在步骤 1 中变更的 mNeedToScanDevices ,在这里起到了判断的作用,执行 scanDevicesLocked 进行扫描,然后更新 mNeedToSendFinishedDeviceScan ,在扫描完成后执行下一步操作。

4. 生成设备添加事件

        while (!mOpeningDevices.empty()) {
            std::unique_ptr<Device> device = std::move(*mOpeningDevices.rbegin());
            mOpeningDevices.pop_back();
            event->when = now;
            event->deviceId = device->id == mBuiltInKeyboardId ? 0 : device->id;
            event->type = DEVICE_ADDED;
            event += 1;

            // 尝试通过比较设备名称来找到匹配的 videoDevice
            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;
            }
        }

这里和第 2 步一样,对所有正在开启的设备生成事件保存到事件队列。

5. 生成设备扫描完成后的事件

        // 在报告了最近一次扫描中添加/删除的所有设备时发送消息。该事件总是至少发送一次。
        if (mNeedToSendFinishedDeviceScan) {
            mNeedToSendFinishedDeviceScan = false;
            event->when = now;
            event->type = FINISHED_DEVICE_SCAN;
            event += 1;
            if (--capacity == 0) {
                break;
            }
        }

6. 处理待执行的事件

在这一个步骤中开始读取事件进行处理,主要是结构如下:

Android 输入系统【3】输入事件的起点根据 mPendingEventIndex 与 mPendingEventCount 遍历 mPendingEventItems。

mPendingEventItems 是待处理的 epoll 事件数组;mPendingEventIndex 是下一个要处理的事件的索引。

    struct epoll_event mPendingEventItems[EPOLL_MAX_EVENTS];
    size_t mPendingEventCount;
    size_t mPendingEventIndex;

这里用到了 linux 的 Inotify 特性,它监控文件系统操作,比如读取、写入和创建。Inotify 反应灵敏,用法非常简单。

来自 epoll 的事件

首先是两个针对事件来源的判断,当待处理事件的 eventItem.data.fd 等于 mINotifyFd 时:

            // 来自 inotify
            if (eventItem.data.fd == mINotifyFd) {
                if (eventItem.events & EPOLLIN) { // 事件可读
                    mPendingINotify = true;
                } else {
                    ALOGW("Received unexpected epoll event 0x%08x for INotify.", eventItem.events);
                }
                continue;
            }

  1. EPOLLIN :表示对应的文件描述符可以读;
  2. EPOLLOUT:表示对应的文件描述符可以写;
  3. EPOLLPRI:表示对应的文件描述符有紧急的数据可读;
  4. EPOLLERR:表示对应的文件描述符发生错误;
  5. EPOLLHUP:表示对应的文件描述符被挂断;
  6. EPOLLET:表示对应的文件描述符有事件发生;

在确认事件可读后,将 mPendingINotify 置为 true ,否则表示事件无法处理, 并在此进行下一次循环。

唤醒事件
            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)); // EINTR指Interrupted system call, 表示由于中断而错误返回.
                } else {
                    ALOGW("Received unexpected epoll event 0x%08x for wake read pipe.", eventItem.events);
                }
                continue;
            }
Device 检查
            // 设备处理
            Device* device = getDeviceByFdLocked(eventItem.data.fd);
            if (device == nullptr) {
                ALOGE("Received unexpected epoll event 0x%08x for unknown fd %d.", eventItem.events, eventItem.data.fd);
                ALOG_ASSERT(!DEBUG);
                continue;
            }
            // 对于 videoDevice 相关处理,这里忽略

在这一步检查 Device ,确保事件来自设备的输入。

输入事件

在完成上述步骤后,就开始处理输入事件了,下面的逻辑描述了整体的处理规则:

Android 输入系统【3】输入事件的起点
image-20221213201644089.png

事件可读的情况下,根据 read 函数的返回值进行判断,检查事件是否能够正常处理:

  • 结果等于 0,或不等于 0 ,errno 代表尚未分配到具体设备的情况下,关闭设备重启;
  • 结果小于 0 ,表示出现错误,不做任何处理;
  • 对结果数量进行检查,不对不做任何处理;
  • 执行处理输入逻辑。

在最后一步处理事件的逻辑:

                    // todo 处理事件的位置
                    int32_t deviceId = device->id == mBuiltInKeyboardId ? 0 : device->id;
                    // 挨个事件塞进 mEventBuffer
                    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;
                    }

这里 event 指针是参数 buffer ,也就是 mEventBuffer ,也就是事件保存到了 mEventBuffer 中,后续逻辑在 InputReader.loopOnce 方法中,通过 processEventsLocked(mEventBuffer, count); 去处理事件。

7. 更新设备信息并进行生成设备变化事件

将修改设备列表,因此这必须在处理所有其他事件之后完成,以确保我们在关闭设备之前读取所有剩余事件。

        // readNotify() will modify the list of devices so this must be done after
        // processing all other events to ensure that we read all remaining events
        // before closing the devices.
        // readNotify() 将修改设备列表,因此这必须在处理所有其他事件之后完成,以确保我们在关闭设备之前读取所有剩余事件。
        if (mPendingINotify && mPendingEventIndex >= mPendingEventCount) {
            mPendingINotify = false;
            readNotifyLocked(); 
            deviceChanged = true;
        }

        // Report added or removed devices immediately.
        if (deviceChanged) {
            continue;
        }

8. 如果收集到了事件或被唤醒,结束循环

        // 如果我们收集了任何事件或者我们被明确唤醒,请立即返回。
        if (event != buffer || awoken) {
            break;
        }

9. 若没有收集到事件或唤醒,通过 epoll 机制轮询事件

        mPendingEventIndex = 0;
        mLock.unlock(); // 轮询前释放锁
        int pollResult = epoll_wait(mEpollFd, mPendingEventItems, EPOLL_MAX_EVENTS, timeoutMillis);
        mLock.lock(); // 轮询后重新加锁
        // 轮询结果等于 0 
        if (pollResult == 0) {
            // Timed out.
            mPendingEventCount = 0;
            break;
        }
        //  小于 0 出现错误
        if (pollResult < 0) {
            // An error occurred.
            mPendingEventCount = 0;
            // 出错后休眠以避免锁定系统。
            // 希望错误是暂时的。
            if (errno != EINTR) {
                ALOGW("poll failed (errno=%d)n", errno);
                usleep(100000);
            }
        } else {
            // Some events occurred. 事件出现
            mPendingEventCount = size_t(pollResult);
        }

这里的 epoll_wait 函数是 Linux 的 epoll 机制的方法之一:

int epoll_wait (int epfd , struct epoll_event * events,
                int maxevents,int timeout);

events 指向了事件缓冲区,每当 interest list 中fd(文件描述符)触发事件加入到ready list 后,会将其返回到事件缓冲区中。最大返回 maxevents 个,因此 maxevents ‍至少大于 0,返回的 event 顺序写入缓冲区。 timeout 参数指定 epoll_wait 将阻塞的毫秒数。

epoll_wait(…) 将会一直阻塞直到:fd产生事件 / 被信号处理函数打断 / 超时。

可以看出,epoll_wait 函数会将数据写入到第二个参数 mPendingEventItems 中,并根据 epoll_wait 函数的返回值判断是否出现错误或超时的情况。正常情况更新等待执行的数量。

总结

最后回顾整个 EventHub 的 getEvents 函数,再来看整体的事件传递流程:

  1. 从 Linux 内核的 epoll 机制读取事件保存到 mPendingEventItems;
  2. 将 mPendingEventItems 中的事件和其他设备增删事件等添加到 mEventBuffer;
  3. 通过 mEventBuffer 传递给 InputReader 传递给下一层。

最后再梳理一下 EventHub 的 getEvents 函数中的流程:

  1. 定义一些在函数中用到的成员变量;

  2. 开启死循环

    1. 检查是否需要重新打开输入设备
    2. 待移除的设备生成对应的事件,加入事件队列
    3. 如有需要重新扫描设备
    4. 待添加的设备生成对应的事件,加入事件队列
    5. 设备扫描完成后生成对应的事件,加入事件队列
    6. 处理待执行的事件,这里是核心的一步
    7. 更新设备信息并生成设备变化事件,加入事件队列
    8. 如果收集到事件或被唤醒,跳出循环
    9. 若没有收集到事件或被唤醒,通过 epoll 机制轮询待执行的事件,保存到待执行的数组中
  3. 全部完成,返回我们读取到的事件数量(通过读取事件前后的指针的内存地址差值得出)。


一直

原文始发于微信公众号(八千里路山与海):Android 输入系统【3】输入事件的起点

版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 举报,一经查实,本站将立刻删除。

文章由极客之音整理,本文链接:https://www.bmabk.com/index.php/post/84996.html

(0)
小半的头像小半

相关推荐

发表回复

登录后才能评论
极客之音——专业性很强的中文编程技术网站,欢迎收藏到浏览器,订阅我们!