前言
前面几篇文章,已经为 Input 系统的分析打好了基础,现在是时候进行更深入的分析了,请读者务必仔细阅读前面的文章,重复内容本文不再具体说明。
通常,手机是不带键盘的,但是手机上仍然有按键,就是我们经常使用的电源键以及音量键,这就是本文要来分析按键事件的原由。
为了这几个按键,就要把按键事件处理的源码分析一遍,这是否值得呢?我以前觉得没必要浪费时间,但是后来在工作中遇到一些任务,我发现这已经不是一个值不值得的问题,而是一个必须的事情。举一个最简单的例子,如何把一个按键映射为电源键?带着这个问题,让我们开始吧。
认识按键事件
通过 adb shell getevent 可以获取输入设备产生的元输入事件,当按下电源键,可以产生如下的按键事件
1 2 3 4 5 6 |
/dev/input/event0: 0001 0074 00000001 /dev/input/event0: 0000 0000 00000000 /dev/input/event0: 0001 0074 00000000 /dev/input/event0: 0000 0000 00000000 |
每一行数据都有固定格式,/dev/input/event0
是内核为输入设备生成的设备文件,它代表一个输入设备,它后面的数据格式为 type code value
。
以第一行为例,几个数据的含义如下
0001
代表输入设备产生事件的类型。此时电源键产生的是一个按键类型事件,而如果手指在触摸设备上滑动,产生是一个坐标类型事件。0074
表示按键的扫描码(code),这个扫描码是与输入设备相关,因此不同的设备上的电源键,产生的扫描码可能不同。00000001
表示按键值(value)。00000001
表示按键被按下,00000000
表示按键抬起。
上面的数据看起来非常晦涩,可以通过 adb shell getevent -l 显示输入系统对这些数据的分析结果,当按下电源键,会出现如下结果
1 2 3 4 5 6 |
/dev/input/event0: EV_KEY KEY_POWER DOWN /dev/input/event0: EV_SYN SYN_REPORT 00000000 /dev/input/event0: EV_KEY KEY_POWER UP /dev/input/event0: EV_SYN SYN_REPORT 00000000 |
第一行和第三行数据,很清晰,分别表示按键按下和抬起。
第二条和第四条的数据是一样的,它是一个同步事件,表示需要接收这些元事件的系统对之前的数据进行同步。
同步事件,对按键事件并没有太大的作用,因为按键的按下事件或抬起事件,只用一条数据就可以表示。
如果是触摸事件,由于它有很多方面的数据,例如,x, y 坐标,压力值,等等,因此它需要多条数据来表示。那么同步事件的作用就出来了,它要求系统对一个触摸事件的所有数据进行整合。
现在我们已经对按键事件有了基本的认识,那么接下来正式开始分析流程。
处理按键事件
从前面文章可知,InputReader 从 EventHub 中获取了输入事件的数据,然后调用如下函数进行处理
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 |
void InputReader::processEventsLocked(const RawEvent* rawEvents, size_t count) { for (const RawEvent* rawEvent = rawEvents; count;) { int32_t type = rawEvent->type; size_t batchSize = 1; if (type < EventHubInterface::FIRST_SYNTHETIC_EVENT) { int32_t deviceId = rawEvent->deviceId; // 获取同一个设备的元输入事件的数量 while (batchSize < count) { if (rawEvent[batchSize].type >= EventHubInterface::FIRST_SYNTHETIC_EVENT || rawEvent[batchSize].deviceId != deviceId) { break; } batchSize += 1; } // 批量处理同一个设备的元输入事件 processEventsForDeviceLocked(deviceId, rawEvent, batchSize); } else { // ... } count -= batchSize; rawEvent += batchSize; } } void InputReader::processEventsForDeviceLocked(int32_t eventHubId, const RawEvent* rawEvents, size_t count) { auto deviceIt = mDevices.find(eventHubId); if (deviceIt == mDevices.end()) { return; } std::shared_ptr<InputDevice>& device = deviceIt->second; // 如果 InputDevice 没有 InputMapper,那么它不能处理事件 if (device->isIgnored()) { return; } // InputDevice 批量处理元输入事件 device->process(rawEvents, count); } |
InputReader 首先找到属于同一个设备的多个事件,然后交给 InputDevice 进行批量处理
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 |
// frameworks/native/services/inputflinger/reader/InputDevice.cpp void InputDevice::process(const RawEvent* rawEvents, size_t count) { // 虽然 InputReader 把批量的事件交给 InputDevice,但是 InputDevice 还是逐个处理事件 for (const RawEvent* rawEvent = rawEvents; count != 0; rawEvent++) { if (mDropUntilNextSync) { if (rawEvent->type == EV_SYN && rawEvent->code == SYN_REPORT) { mDropUntilNextSync = false; } } else if (rawEvent->type == EV_SYN && rawEvent->code == SYN_DROPPED) { // EV_SYN + SYN_DROPPED 表明要丢弃后面的事件,直到 EV_SYN + SYN_REPORT mDropUntilNextSync = true; reset(rawEvent->when); } else { // 每一个事件交给 InputMapper 处理 for_each_mapper_in_subdevice(rawEvent->deviceId, [rawEvent](InputMapper& mapper) { mapper.process(rawEvent); }); } --count; } } |
虽然 InputReader 把事件交给 InputDevice 进行批量处理,但是 InputDevice 是把事件逐个交给它的 InputMapper 处理。
InputMapper 的作用是为 InputReader 加工事件。
对于键盘类型的输入设备,它的 InputMapper 实现类为 KeyboardInputMapper,它对按键事件处理如下
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 |
void KeyboardInputMapper::process(const RawEvent* rawEvent) { switch (rawEvent->type) { // 处理 EV_KEY case EV_KEY: { int32_t scanCode = rawEvent->code; int32_t usageCode = mCurrentHidUsage; mCurrentHidUsage = 0; if (isKeyboardOrGamepadKey(scanCode)) { // 注意第三个参数,value等于0,才表示按键down, 也就是说,value 为1, 表示按键被按下 processKey(rawEvent->when, rawEvent->readTime, rawEvent->value != 0, scanCode, usageCode); } break; } case EV_MSC: { // ... } // 处理 EV_SYN + SYN_REPORT case EV_SYN: { if (rawEvent->code == SYN_REPORT) { mCurrentHidUsage = 0; } } } } void KeyboardInputMapper::processKey(nsecs_t when, nsecs_t readTime, bool down, int32_t scanCode, int32_t usageCode) { int32_t keyCode; int32_t keyMetaState; uint32_t policyFlags; // 1. 根据键盘配置文件,把 scanCode 转化为 keycode,并获取 flags if (getDeviceContext().mapKey(scanCode, usageCode, mMetaState, &keyCode, &keyMetaState, &policyFlags)) { keyCode = AKEYCODE_UNKNOWN; keyMetaState = mMetaState; policyFlags = 0; } // 按下 if (down) { // 根据屏幕方向,再次转换 keyCode // Rotate key codes according to orientation if needed. if (mParameters.orientationAware) { keyCode = rotateKeyCode(keyCode, getOrientation()); } // Add key down. ssize_t keyDownIndex = findKeyDown(scanCode); if (keyDownIndex >= 0) { // key repeat, be sure to use same keycode as before in case of rotation keyCode = mKeyDowns[keyDownIndex].keyCode; } else { // key down if ((policyFlags & POLICY_FLAG_VIRTUAL) && getContext()->shouldDropVirtualKey(when, keyCode, scanCode)) { return; } if (policyFlags & POLICY_FLAG_GESTURE) { // 如果设备通知支持触摸,那么发送一个 ACTION_CANCEL 事件 getDeviceContext().cancelTouch(when, readTime); } KeyDown keyDown; keyDown.keyCode = keyCode; keyDown.scanCode = scanCode; // 保存按下的按键 mKeyDowns.push_back(keyDown); } mDownTime = when; } else { // 抬起按键 // Remove key down. ssize_t keyDownIndex = findKeyDown(scanCode); if (keyDownIndex >= 0) { // key up, be sure to use same keycode as before in case of rotation keyCode = mKeyDowns[keyDownIndex].keyCode; // 移除 mKeyDowns.erase(mKeyDowns.begin() + (size_t)keyDownIndex); } else { // key was not actually down ALOGI("Dropping key up from device %s because the key was not down. " "keyCode=%d, scanCode=%d", getDeviceName().c_str(), keyCode, scanCode); return; } } // 更新meta状态 if (updateMetaStateIfNeeded(keyCode, down)) { keyMetaState = mMetaState; } nsecs_t downTime = mDownTime; // 外部设备的按键按下时,添加唤醒标志位 if (down && getDeviceContext().isExternal() && !mParameters.doNotWakeByDefault && !isMediaKey(keyCode)) { policyFlags |= POLICY_FLAG_WAKE; } // 设备是否能生成重复按键事件,一般设备都不支持这个功能 // 而是由系统模拟生成重复按键事件 if (mParameters.handlesKeyRepeat) { policyFlags |= POLICY_FLAG_DISABLE_KEY_REPEAT; } // 2. 生成 NotifyKeyArgs, 并加到 QueuedInputListener 队列中 NotifyKeyArgs args(getContext()->getNextId(), when, readTime, getDeviceId(), mSource, getDisplayId(), policyFlags, down ? AKEY_EVENT_ACTION_DOWN : AKEY_EVENT_ACTION_UP/*action*/, AKEY_EVENT_FLAG_FROM_SYSTEM/*flags*/, keyCode, scanCode, keyMetaState, downTime); getListener()->notifyKey(&args); } |
这时有很多与按键相关的东西,但是我现在只关心手机上电源键以及音量键的处理流程,因此这里的处理过程主要分为两步
- 根据按键配置文件,把扫描码(scan code)转换为按键码(key code),并从配置文件中获取策略标志位(policy flag)。不同的输入设备的同一种功能的按键,例如电源键,产生的扫描码不一定都相同,Android 系统需要把扫描码,映射为Android 系统的按键码。如果一来,Android 系统就能统一处理所有输入设备的同一种功能的按键。
- 创建一个事件 NotifyKeyArgs,并加入到 QueuedInputListener 队列中。从前面的文章可知,当 InputReader 处理完从 EventHub 读到的事件后,会刷新这个队列,从而把事件发送给 InputClassifier。而对于按键事件,InputClassifier 不做任何加工,直接把事件传递给 InputDispatcher。
InputReader 对按键事件的处理就这么点事儿,so eary !? 确实 easy,但是我们还有一个问题没有搞清楚,扫描码是如何映射为按键码的?
扫描码映射按键码
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 |
status_t EventHub::mapKey(int32_t deviceId, int32_t scanCode, int32_t usageCode, int32_t metaState, int32_t* outKeycode, int32_t* outMetaState, uint32_t* outFlags) const { std::scoped_lock _l(mLock); Device* device = getDeviceLocked(deviceId); status_t status = NAME_NOT_FOUND; if (device != nullptr) { // Check the key character map first. const std::shared_ptr<KeyCharacterMap> kcm = device->getKeyCharacterMap(); if (kcm) { // 1. KeyCharacterMapFile :转换 scanCode 为 keyCode if (!kcm->mapKey(scanCode, usageCode, outKeycode)) { *outFlags = 0; status = NO_ERROR; } } // Check the key layout next. if (status != NO_ERROR && device->keyMap.haveKeyLayout()) { // 2. KeyLayoutFile: 把 scanCode 转换为 keycode if (!device->keyMap.keyLayoutMap->mapKey(scanCode, usageCode, outKeycode, outFlags)) { status = NO_ERROR; } } if (status == NO_ERROR) { if (kcm) { // 3. KeyCharacterMapFile: 根据meta按键状态,重新映射按键字符 kcm->tryRemapKey(*outKeycode, metaState, outKeycode, outMetaState); } else { *outMetaState = metaState; } } } if (status != NO_ERROR) { *outKeycode = 0; *outFlags = 0; *outMetaState = metaState; } return status; } |
扫描码转化为按键码的过程有点小复杂
- 首先根据 kcm(key character map) 文件进行转换。
- 如果第一步失败,那么根据 kl(key layout) 文件进行转换。
- 如果前两步,有一个成功,那么再根据meta按键状态,重新使用 kcm 文件对按键码再次进行转换。这个只对键盘起作用,例如按下 shift ,再按字母键,那么会产生大写的字母的按键码。而对于电源键和音量键,此步骤可以忽略。
可以发现,kcm 和 kl 文件都可以把按键的扫描码进行转换为按键码,然而 kcm 文件一般都只是针对键盘的按键,而对于电源键和音量键,一般都是通过 kl 文件进行转换的。
注意,键盘类型的输入设备和键盘输入设备,不是同一个意思。键盘类型的输入设备,表示类型为键盘的所有输入设备,而键盘输入设备就是我们常说的键盘。
那么如何找到输入设备的 kl 文件呢?
首先通过 adb shell getevent 找到按键事件关联的设备文件节点,再通过 adb shell dumpsys input 导出所有设备的信息,包括设备节点和按键配置文件。
对比两种信息,就可以的到设备的按键配置文件,如下
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
6: qpnp_pon Classes: KEYBOARD Path: /dev/input/event0 Enabled: true Descriptor: fb60d4f4370f5dbe8267b63d38dea852987571ab Location: qpnp_pon/input0 ControllerNumber: 0 UniqueId: Identifier: bus=0x0000, vendor=0x0000, product=0x0000, version=0x0000 KeyLayoutFile: /system/usr/keylayout/Generic.kl KeyCharacterMapFile: /system/usr/keychars/Generic.kcm ConfigurationFile: VideoDevice: <none> |
从这个信息就可以看出,输入设备的 kl 文件为 /system/usr/keylayout/Generic.kl,它的电源键映射如下
1 2 3 |
key 116 POWER |
其中,116是十进制,它的十六进制为 0x74,正好就是 adb shell getevent 显示的电源按键的扫描码。
POWER 就是被映射成的按键码,但是它是一个字符,而实际使用的是 int 类型,这个关系的映射是在下面定义的
1 2 3 4 5 6 7 8 9 10 11 12 |
// frameworks/native/include/android/keycodes.h /** * Key codes. */ enum { AKEYCODE_POWER = 26, } |
因此,电源按键的扫描码 0x74,被映射为按键码 26,正好就是上层 KeyEvent.java 定义的电源按键值
1 2 3 4 5 |
// frameworks/base/core/java/android/view/KeyEvent.java public static final int KEYCODE_POWER = 26; |
在很早的 Android 版本上,配置文件中,电源键还会定义一个策略标志位,如下
1 2 3 |
key 116 POWER WAKE |
其中,WAKE 就是一个策略标志位,它表示需要唤醒设备,不过在最近的 Android 版本中,去掉了这个标志位,上层在处理电源按键时,会自动唤醒设备。
结束
InputReader 处理按键事件的过程其实很简单,就是把按键事件交给 KeyboardInputMapper 处理,KeyboardInputMapper 根据配置文件,把按键的扫描码转换为按键码,并同时从配置文件中获取策略标志位,然后把这些信息包装成一个事件,发送到下一环。
现在,如果项目上让你完成功能按键的映射,或者解除某个按键的电源功能,你会了吗?
本文转自 https://juejin.cn/post/7168875586826764318,如有侵权,请联系删除。