網站首頁 編程語言 正文
前言
前面幾篇文章已經為 Input 系統的分析打好了基礎,現在是時候進行更深入的分析了。
通常,手機是不帶鍵盤的,但是手機上仍然有按鍵,就是我們經常使用的電源鍵以及音量鍵。因此還是有必要分析按鍵事件的處理流程。
那么,掌握按鍵事件的處理流程,對我們有什么用處呢?例如,手機上添加了一個功能按鍵,你知道如何把這個物理按鍵映射到上層,然后處理這個按鍵嗎?又例如,如果設備是不需要電源鍵,但是系統默認把某一個按鍵映射為電源鍵,那么我們如何使這個按鍵不成為電源鍵呢?所有這一切,都與按鍵事件的處理流程相關。
認識按鍵事件
很多讀者可能還不知道按鍵事件到底長什么樣,通過 adb shell getevent 可以獲取輸入設備產生的元輸入事件,按鍵事件當然也包括在內。
當按下電源鍵,可以產生如下的按鍵事件
/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
表示按鍵抬起。
因此,一個輸入設備產生的事件,可以通過 type + code + value 的形式表示。
注意,以上這些數據都是元輸入事件的數據,是由內核直接為輸入設備生成的數據,因此讀起來沒那么直觀。我們可以通過 adb shell getevent -l 顯示輸入系統對這些數據的分析結果,當按下電源鍵,會出現如下結果
/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
第一行數據,EV_KEY + KEY_POWER + DOWN 表示電源鍵按下。
第三行數據,EV_KEY + KEY_POWER + UP 表示電源鍵抬起。
第二行和第四行的數據,EV_SYN + SYN_REPORT 表示之前的一個事件的數據已經發送完畢,需要系統同步之前一個事件的所有數據。對于一個按鍵,一個事件只有一條數據,然而對于觸摸板上的滑動事件,例如手指按下事件,它的數據可不止一條,我們將在后面的文章中看到。
好,既然已經以按鍵事件的數據有了基本的認識,那么接下來開始著手分析按鍵事件的處理流程。
處理按鍵事件
從前面文章可知,InputReader 從 EventHub 中獲取了輸入事件的數據,然后調用如下函數進行處理
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 進行批量處理
// 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 實現類為 KeyboardInputMapper,它對按鍵事件處理如下
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; } } } }
其中,我們只需要關心類型為 EV_KEY 類型的事件,它表示一個按鍵事件,它的處理如下
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)) { // If global meta state changed send it along with the key. // If it has not changed then we'll use what keymap gave us, // since key replacement logic might temporarily reset a few // meta bits for given key. 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 系統需要把掃描碼映射為同一個按鍵碼進行處理。
- 創建一個事件 NotifyKeyArgs,并加入到 QueuedInputListener 隊列中。從前面的文章可知,當 InputReader 處理完從 EventHub 讀到的事件后,會刷新這個隊列,從而把事件發送給 InputClassifier。而對于按鍵事件,InputClassifier 不做任何加工,直接把事件傳遞給 InputDispatcher。
現在只要知道如何把一個按鍵的掃描碼映射為按鍵碼,InputReader 處理按鍵事件的整個流程都一清二楚了。
掃描碼映射按鍵碼
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 可以發現輸入設備的文件節點為 /dev/input/event0,再通過 adb shell dumpsys input 導出所有設備的信息,就可以找到電源鍵屬于哪個設備,以及設備的的按鍵配置文件
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,它的電源鍵映射如下
key 116 POWER
其中,116是十進制,它的十六進制為 0x74,正好就是 adb shell getevent 顯示的電源按鍵的掃描碼。
POWER 就是被映射成的按鍵碼,但是它是一個字符,而實際使用的是 int 類型,這個關系的映射是在下面定義的
// frameworks/native/include/android/keycodes.h /** * Key codes. */ enum { AKEYCODE_POWER = 26, }
因此,電源按鍵的掃描碼 0x74,被映射為按鍵碼 26,正好就是上層 KeyEvent.java 定義的電源按鍵值
// frameworks/base/core/java/android/view/KeyEvent.java public static final int KEYCODE_POWER = 26;
在很早的 Android 版本上,配置文件中,電源鍵還會定義一個策略標志位,如下
key 116 POWER WAKE
其中,WAKE 就是一個策略標志位,在把掃描轉換為按鍵碼時,這個策略標志位也會被解析,它表示需要喚醒設備,上層會根據這個標志位,讓設備喚醒。
結束
InputReader 處理按鍵事件的過程其實很簡單,就是把按鍵事件交給 KeyboardInputMapper 處理,KeyboardInputMapper 根據配置文件,把按鍵的掃描碼轉換為按鍵碼,并同時從配置文件中獲取策略標志位,然后把這些信息包裝成一個事件,發送到下一環。
現在,如果項目上讓你完成功能按鍵的映射,或者解除某個按鍵的電源功能,你會了嗎?
原文鏈接:https://juejin.cn/post/7168875586826764318
相關推薦
- 2022-07-11 UVM中analysis端口的使用方法
- 2022-08-22 Python中可以用三種方法判斷文件是否存在_python
- 2022-07-01 詳解如何在Go語言中調用C源代碼_Golang
- 2022-07-21 各數據庫SQL查詢結果多行數據合并成一行
- 2022-06-17 C#中Abstract方法和Virtual方法的區別_C#教程
- 2023-03-20 c#中Invoke與BeginInvoke的用法及說明_C#教程
- 2022-10-04 python3實現倒計時效果_python
- 2023-04-26 numpy數組切片的使用_python
- 最近更新
-
- window11 系統安裝 yarn
- 超詳細win安裝深度學習環境2025年最新版(
- Linux 中運行的top命令 怎么退出?
- MySQL 中decimal 的用法? 存儲小
- get 、set 、toString 方法的使
- @Resource和 @Autowired注解
- Java基礎操作-- 運算符,流程控制 Flo
- 1. Int 和Integer 的區別,Jav
- spring @retryable不生效的一種
- Spring Security之認證信息的處理
- Spring Security之認證過濾器
- Spring Security概述快速入門
- Spring Security之配置體系
- 【SpringBoot】SpringCache
- Spring Security之基于方法配置權
- redisson分布式鎖中waittime的設
- maven:解決release錯誤:Artif
- restTemplate使用總結
- Spring Security之安全異常處理
- MybatisPlus優雅實現加密?
- Spring ioc容器與Bean的生命周期。
- 【探索SpringCloud】服務發現-Nac
- Spring Security之基于HttpR
- Redis 底層數據結構-簡單動態字符串(SD
- arthas操作spring被代理目標對象命令
- Spring中的單例模式應用詳解
- 聊聊消息隊列,發送消息的4種方式
- bootspring第三方資源配置管理
- GIT同步修改后的遠程分支