Vol.12 InputManagerService 与输入事件

这块是对《深入理解andriod内核设计思想》相关章节的总结和拓展, 这本书写的也非常的好,很推荐!

12.1 事件的分类

  • 按键事件(keyEvent)
  • 触摸事件(TouchEvent)
  • 鼠标事件(MouseEvent)
  • ......

提取统一的抽象接口, InputEvent , Event 属于 I/O 设备中的 Input 部分。

InputEvent 下面有两个子类, KeyEvent 和 MotionEvent

  • KeyEvent ------ 表达按键事件
  • MotionEvent ------ 将所有能产生 Movement 的事件源进行统一管理。

InputEvent.getDevice() 函数可以获得当前事件的"硬件源"(基本涵盖一些常见的输入设备) 对于应用开发人员来说,他们与事件接触的方法一般是通过 View 组件进行的。 通过回调的形式实现的。或者通过重载 View 类里的函数来完成同样的功能。

java 复制代码
setOnKeyListener(OnKeyListener)
setOnTouchListener(OnTouchListener)
...

整个接口的定义:

  1. View : 处理此 KeyEvent 的 View 对象
  2. KeyCode: 具体对应的物理按键
  3. KeyEvent: 事件描述体, 可以获取到更多的按键信息, getRepeatCount()、getScanCode()、getSource()

12.2 事件的投递流程

1.采集

对"硬件源"所产生的原始信息进行收集的过程,需要 Linux 内核驱动的支持,Android 系统则通过 /dev/input 下的节点来访问当前发生的事件。

2. 前期处理

有一部分内容对于应用程序而言并不是必须的,而且格式上也繁琐,所以需要先经过前期的提炼和转化。

3. WMS 分配

WMS 是窗口的大管家,同时也是 InputEvent 的派发者,这样的设计是自然的,因为 WMS 记录了当前系统中所有窗口的完整状态信息,所以也只有它才能判断出应该把事件投递给哪一个具体的应用进程进行处理。

其派发策略也因事件类别的不同而有所差异------ 比如说按键消息直接发送给"最前端"的窗口即可,而如果是触摸消息则要先计算出触摸点落在哪个区域,然后才能传递给相应的窗口单元。

4. 应用程序处理

12.2.1 InputManagerService

IMS 和 WMS 类似, 都由 SystemServer 统一启动:

scss 复制代码
// framework/base/services/java/com/android/server/SystemServer.java
public void run() { ...
	inputManager = new  InputManagerService(context, wmHandler);
    // 这里将 IMS 实例传入
    wm = WindowManagerService.main(context, power, display, inputManager, uiHandler,
                                  wmHandler, factoryTest != SystemServer.FACTORY_TEST_LOW_LEVEL,
                                  !firstBoot, onlyCore);
    ServiceManager.addService(Context.WINDOW_SERVICE, wm);
    ServiceManager.addService(Context.INPUT_SERVICE, inputManager);
    ...
    inputManager.setWindowManagerCallbacks(wm.getInputMonitor());
    inputManager.start();
}

InputManagerService.start() 是否另外开启了一个工作线程?

csharp 复制代码
//framework/base/services/java/com/android/server/input/InputManagerService.java
public void start() {
	nativeStart(mPtr); // 本地函数
    ...
}

Java 层的 IMS 实际是对 Native 层 InputManager 的一层 Java 包装,因而这个类有大量的本地函数声明。

上述函数 start 直接调用了本地实现 nativeStart ,其中变量 mPtr 是 IMS 在构造过程中,调用 nativeInit 所创建的 NativeInputManager 对象 ------ 本质上是一个 C++ 指针,所以可以通过 int 类型进行保存。

scss 复制代码
// framework/base/services/jni/com_android_server_input_InputManagerService.cpp

static void nativeStart(JNIEnv* env, jclass clazz, jint ptr) {
    // 将int类型的指针强制类型转换为 NativeInputManager 对象
	NativeInputManager* im = reinterpret_cast<NativeInputManager*>(ptr);
    // 这里返回的是 InputManager对象,最终执行的是 InputManager.start()
    status_t result = im->getInputManager()->start();
    ...
}

// framework/base/services/input/InputManager.cpp
status_t InputManager::start() {
    status_t result = mDispatcherThread->run("InputDispatcher", PRIORITY_URGENT_DISPLAY);
    ...
    result = mReaderThread->run("InputReader", PRIORITY_URGENT_DISPLAY);
    ...
    return OK;
}

果然,InputManager 为 IMS 创建了新的线程:

  • InputReaderThread (从驱动节点中读取 Event)
  • InputDispatcherThread (专门分发)

12.2.2 InputReaderThread

一个独立的循环线程加上一些必要的辅助类,工作相对单一,不断轮询相关设备节点中是否有新的事件发生。

arduino 复制代码
// framework/base/services/input/InputReader.h
class InputReaderThread : public Thread { // 继承自 Thread
public:
    InputReaderThread(const sp<InputReaderInterface>& reader);
    virtual ~InputReaderThread();
private:
    sp<InputReaderInterface> mReader; // 辅助类
    virtual bool threadLoop();
};

重点:

  1. InputReader 实际上并不直接去访问设备节点,而是通过 EventHub 来完成这一工作
  2. EventHub 通过读取 /dev/input/ 下的相关文件来判断是否有新的事件,并通知 InputReader

12.2.3 InputDispatcherThread

IMS 的构造过程中,InputReaderThread 和 InputDispatcherThread 是独立的线程,而且跟 WMS 都运行于系统进程中,IMS 实现的核心是 InputDispatcher 类。

这里 InputManager 中对他们进行了统一的管理

scss 复制代码
// framework/base/services/input/InputManager.cpp

InputManaegr::InputManager(const sp<EventHubInterface>& eventHub,
                           const sp<InputReaderPolicyInterface>& readerPolicy,
                           const sp<InputDispatcherPolicyInterface>& dispatchPolicy) {
    mDispatcher = new InputDispatcher(dispatchPolicy);
    mReader = new InputReader(eventHub, readerPolicy, mDispatcher);
    // 这里两者联系了起来
    initialize();
}

在 InputReader 的 loopOnce() 中,会把发生的事件通过 InputDispatcher 实例告知 Listener。

这样 InputDispatcher 就能不断地获知系统设备中实时发生的事件了,它还可以向 InputReader 注册监听多种事件。

scss 复制代码
// framework/base/services/input/InputReader.cpp
void InputReader::loopOnce() {...
	mQueueListener->flush(); // 这个变量实质上是上面 mDispatcher 的进一步封装
    ...
}

// 相关的 callback 函数
notifyConfigurationChanged(const NotifyConfigurationChangedArgs*);
notifyKey(const NotifyKeyArgs*);
...

相关的 calllback 函数:

notifyConfigurationChanged(const NotifyConfigurationChangedArgs*);

notifyKey(const NotifyKeyArgs)

......

上图中与"分发" 策略直接关联的是 dispatchKeyLocked

arduino 复制代码
// frameworks/base/services/input/InputDispatcher.cpp
bool InputDispatcher::dispatchKeyLocked(nsecs_t currentTime, KeyEntry* entry, DropReason*
                                        dropReason, nsecs_t* nextWakeupTime) {
    // 1. 前期处理,主要针对按键的 repeatCount
    ...
    // 2. 检查是否 INTERCEPT_KEY_RESULT_TRY_AGAIN_LATER
    // 如果是的话,还要判断当前时间点满足要求与否------不满足的话,直接返回。
    ...
    // 3. 在将事件发送出去前,先要检查当前系统策略(Policy)是否要对它进行先期处理
    // 比如系统按键 HOME,就需要先由 WMS 来处理
    ...
    // 4. 判断是否要放弃当前事件
    ...
    // 5. 确定事件的接收方(Targets), 这是最重要的一步
    Vector<InputTarget> inputTargets:
    int32_t injectionResult = findFocusWindowTargetLocked(currentTime, entry,
                                                          input Targets, nextWakeupTime);
    // 查找符合要求的事件投递目标
    ...
    setInjectionResultLocked(entry, injectionResult);
    ...
    dispatchEventLocked(currentTime, entry, inputTargets); // 投递消息到目标中
    return true;
}

这里我们最需要关注的是 step 5 ------ 找到目标窗口。

arduino 复制代码
int32_t InputDispatcher::findFocusWindowTargetsLocked(nsecs_t currentTime, const EventEntry*
                                                      entry, ector<InputTarget>& inputTargets,
                                                      nsecs_t* nextWakeupTime) {
    int32_t injectionResult;
    if (mFocusedWindowHandle == NULL) { // 代表当前具有焦点的窗口
        if (mFocusedWindowHandle != NULL) {
            injectionResult = handleTargetsNotReadyLocked(currentTime, entry,
                                            mFocusedApplicationHandle, NULL, nextWakeupTime,...);
            // 如果没有"最前端"窗口,但却有"最前端"的应用存在,这说明很可能此应用还在启动过程中,
            // 因此先等待一段时间后重试
            goto Unresponsive;
        }
        ...
        injectionResult = INPUT_EVENT_INJECTION_FAILED;
        goto Failed; // 既没有焦点窗口,也没有焦点应用,程序将报错。
    }
    // 有焦点窗口
    if (!checkInjectionPermission(mFocusedWindowHandle, entry->injectionState)) {
        // 权限检查
        injectionResult = INPUT_EVENT_INJECTION_PERMISSION_DENIED;
        goto Failed;
    }

    // 如果当前窗口处于暂停状态,也需要等待
    if (mFocusedWindowHandle->getInfo()->paused) {
        ...
    }
    // 当前窗口还在处理上一个事件,和上面一样,都需要等待它完成
    if (!isWindowReadyForMoreInputLocked(currentTime, mFocusedWindowHandle, entry))
    {
        ...
    }

    // 成功找到符合要求的窗口,并且状态值都正确,此时可以将它加入 inputTargets 中
    injectionResult = INPUT_EVENT_INJECTION_SUCCEEDED;
    addWindowTargetLocked(mFocusedWindowHandle,
                          InputTarget::FLAG_FOREGROUND | InputTarget::FLAG_DISPATCH_AS_IS,
                          BitSet32(0), inputTargets);
    Failed;
    Unresponsive:
    // 异常处理部分
    }

mFocusedWindowHandle 是由 InputMonitor 来赋值的, InputMonitor 是 WMS 和 InputDispatcher 之间的中介。

  • InputDispatcher -> InputMonitor -> WMS
  • WMS -> InputMonitor -> InputDispatcher

InputMonitor 也为 WMS 访问 InputDispatcher 提供了一系列的函数实现,InputDispatcher 当前焦点窗口就是 WMS 通过 InputMonitor 的 updateInputWindowsLw() 告知的。

那么 InputDispatcher 如何通知应用程序窗口有按键事件呢?它和 InputTarget 之间如何通信呢?

arduino 复制代码
// frameworks/base/services/input/InputDispatcher.h
struct InputTarget {
    enum { // 这个枚举类型是关于目标窗口的各种属性描述
        FLAG_FOREGROUND = 1 << 0, // 说明目标窗口是前台应用
        ...
    }};
    // InputDispatcher 通过 inputChnanel 与窗口建立连接
    sp<InputChannel> inputChnanel;

每增加一个 应用程序窗口,都得新建一个 InputChannel.

最后发现是通过 Socket 通信的方式来实现的。

java 复制代码
// frameworks/base/services/core/java/com/android/server/wm/WindowManagerService.java
public int addWindow(Session session, IWindow client, LayoutParams attrs, int viewVisibility,
            int displayId, int requestUserId, InsetsVisibilities requestedVisibilities,
            InputChannel outInputChannel, InsetsState outInsetsState,
            InsetsSourceControl[] outActiveControls, Rect outAttachedFrame,
            float[] outSizeCompatScale) {
    ...
final boolean openInputChannels = (outInputChannel != null
                    && (attrs.inputFeatures & INPUT_FEATURE_NO_INPUT_CHANNEL) == 0);
            if  (openInputChannels) {
                win.openInputChannel(outInputChannel);
            }
    ...
}

总结

现在把所有的流程跟模块串联起来,流程大致如下:

  • 点击屏幕
  • InputManagerService的Read线程捕获事件,预处理后发送给Dispatcher线程
  • Dispatcher找到目标窗口
  • 通过Socket将事件发送到目标窗口
  • APP端被唤醒
  • 找到目标窗口处理事件

参考文档:

juejin.cn/post/684490...

相关推荐
三少爷的鞋5 小时前
AI 时代,我们都将成为通才型开发者:只懂 Android,已经不够了
android
Dovis(誓平步青云)9 小时前
家里设备越来越多,如何用一张空间地图控制灯光和温度![
android·java·前端·javascript·人工智能·电脑
晚风叙码11 小时前
MySQL 数据类型详解:从数值到字符串,一篇讲透
android·mysql·adb
传奇开心果编程11 小时前
【Compose Multiplatform 跨端开发学与练】第3课 布局与组件
android·windows·学习·ui·ios·kotlin·composer
传奇开心果编程14 小时前
【Compose Multiplatform 跨端开发学与练】第8课 资源管理与主题
android·windows·学习·ios·kotlin·web·composer
传奇开心果编程15 小时前
【Compose Multiplatform 跨端开发学与练】第9课 测试与调试
android·学习·macos·ios·kotlin·web·composer
传奇开心果编程16 小时前
【Compose Multiplatform 跨端开发学与练】第4课 导航与路由
android·windows·学习·ui·ios·kotlin·composer
传奇开心果编程16 小时前
【Compose Multiplatform 跨端开发学与练】第6课 状态管理与架构
android·学习·ui·ios·架构·kotlin·composer
事圆则缓16 小时前
Android AOSP 定制常见概念:源码目录、系统镜像与刷机流程
android
传奇开心果编程16 小时前
【Compose Multiplatform 跨端开发学与练】第2课 Compose 基础语法
android·windows·学习·ui·ios·kotlin·composer