Input系統(tǒng)之InputReader處理按鍵事件詳解
前言
前面幾篇文章已經(jīng)為 Input 系統(tǒng)的分析打好了基礎(chǔ),現(xiàn)在是時候進行更深入的分析了。
通常,手機是不帶鍵盤的,但是手機上仍然有按鍵,就是我們經(jīng)常使用的電源鍵以及音量鍵。因此還是有必要分析按鍵事件的處理流程。
那么,掌握按鍵事件的處理流程,對我們有什么用處呢?例如,手機上添加了一個功能按鍵,你知道如何把這個物理按鍵映射到上層,然后處理這個按鍵嗎?又例如,如果設(shè)備是不需要電源鍵,但是系統(tǒng)默認把某一個按鍵映射為電源鍵,那么我們?nèi)绾问惯@個按鍵不成為電源鍵呢?所有這一切,都與按鍵事件的處理流程相關(guān)。
認識按鍵事件
很多讀者可能還不知道按鍵事件到底長什么樣,通過 adb shell getevent 可以獲取輸入設(shè)備產(chǎn)生的元輸入事件,按鍵事件當然也包括在內(nèi)。
當按下電源鍵,可以產(chǎn)生如下的按鍵事件
/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 是內(nèi)核為輸入設(shè)備生成的設(shè)備文件,它代表一個輸入設(shè)備,它后面的數(shù)據(jù)格式為 type code value。
以第一行為例,幾個數(shù)據(jù)的含義如下
0001代表輸入設(shè)備產(chǎn)生事件的類型。此時電源鍵產(chǎn)生的是一個按鍵類型事件,而如果手指在觸摸設(shè)備上滑動,產(chǎn)生是一個坐標類型事件。0074表示按鍵的掃描碼(code),這個掃描碼是與輸入設(shè)備相關(guān),因此不同的設(shè)備上的電源鍵,產(chǎn)生的掃描碼可能不同。00000001表示按鍵值(value)。00000001表示按鍵被按下,00000000表示按鍵抬起。
因此,一個輸入設(shè)備產(chǎn)生的事件,可以通過 type + code + value 的形式表示。
注意,以上這些數(shù)據(jù)都是元輸入事件的數(shù)據(jù),是由內(nèi)核直接為輸入設(shè)備生成的數(shù)據(jù),因此讀起來沒那么直觀。我們可以通過 adb shell getevent -l 顯示輸入系統(tǒng)對這些數(shù)據(jù)的分析結(jié)果,當按下電源鍵,會出現(xiàn)如下結(jié)果
/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
第一行數(shù)據(jù),EV_KEY + KEY_POWER + DOWN 表示電源鍵按下。
第三行數(shù)據(jù),EV_KEY + KEY_POWER + UP 表示電源鍵抬起。
第二行和第四行的數(shù)據(jù),EV_SYN + SYN_REPORT 表示之前的一個事件的數(shù)據(jù)已經(jīng)發(fā)送完畢,需要系統(tǒng)同步之前一個事件的所有數(shù)據(jù)。對于一個按鍵,一個事件只有一條數(shù)據(jù),然而對于觸摸板上的滑動事件,例如手指按下事件,它的數(shù)據(jù)可不止一條,我們將在后面的文章中看到。
好,既然已經(jīng)以按鍵事件的數(shù)據(jù)有了基本的認識,那么接下來開始著手分析按鍵事件的處理流程。
處理按鍵事件
從前面文章可知,InputReader 從 EventHub 中獲取了輸入事件的數(shù)據(jù),然后調(diào)用如下函數(shù)進行處理
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;
// 獲取同一個設(shè)備的元輸入事件的數(shù)量
while (batchSize < count) {
if (rawEvent[batchSize].type >= EventHubInterface::FIRST_SYNTHETIC_EVENT ||
rawEvent[batchSize].deviceId != deviceId) {
break;
}
batchSize += 1;
}
// 批量處理同一個設(shè)備的元輸入事件
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 首先找到屬于同一個設(shè)備的多個事件,然后交給 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 處理。
對于鍵盤類型的輸入設(shè)備,它的 InputMapper 實現(xiàn)類為 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)) {
// 注意第三個參數(shù),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;
}
}
}
}
其中,我們只需要關(guān)心類型為 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. 根據(jù)鍵盤配置文件,把 scanCode 轉(zhuǎn)化為 keycode,并獲取 flags
if (getDeviceContext().mapKey(scanCode, usageCode, mMetaState, &keyCode, &keyMetaState,
&policyFlags)) {
keyCode = AKEYCODE_UNKNOWN;
keyMetaState = mMetaState;
policyFlags = 0;
}
// 按下
if (down) {
// 根據(jù)屏幕方向,再次轉(zhuǎn)換 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) {
// 如果設(shè)備通知支持觸摸,那么發(fā)送一個 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狀態(tài)
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;
// 外部設(shè)備的按鍵按下時,添加喚醒標志位
if (down && getDeviceContext().isExternal() && !mParameters.doNotWakeByDefault &&
!isMediaKey(keyCode)) {
policyFlags |= POLICY_FLAG_WAKE;
}
// 設(shè)備是否能生成重復按鍵事件,一般設(shè)備都不支持這個功能
// 而是由系統(tǒng)模擬生成重復按鍵事件
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);
}
我現(xiàn)在只關(guān)心手機上電源鍵以及音量鍵的處理流程,因此這里的處理過程主要分為兩步
- 根據(jù)按鍵配置文件,把掃描碼(scan code)轉(zhuǎn)換為按鍵碼(key code),并從配置文件中獲取策略標志位(policy flag)。不同的輸入設(shè)備的同一種功能的按鍵,例如電源鍵,產(chǎn)生的掃描碼不一定都相同,Android 系統(tǒng)需要把掃描碼映射為同一個按鍵碼進行處理。
- 創(chuàng)建一個事件 NotifyKeyArgs,并加入到 QueuedInputListener 隊列中。從前面的文章可知,當 InputReader 處理完從 EventHub 讀到的事件后,會刷新這個隊列,從而把事件發(fā)送給 InputClassifier。而對于按鍵事件,InputClassifier 不做任何加工,直接把事件傳遞給 InputDispatcher。
現(xiàn)在只要知道如何把一個按鍵的掃描碼映射為按鍵碼,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 :轉(zhuǎn)換 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 轉(zhuǎn)換為 keycode
if (!device->keyMap.keyLayoutMap->mapKey(scanCode, usageCode, outKeycode, outFlags)) {
status = NO_ERROR;
}
}
if (status == NO_ERROR) {
if (kcm) {
// 3. KeyCharacterMapFile: 根據(jù)meta按鍵狀態(tài),重新映射按鍵字符
kcm->tryRemapKey(*outKeycode, metaState, outKeycode, outMetaState);
} else {
*outMetaState = metaState;
}
}
}
if (status != NO_ERROR) {
*outKeycode = 0;
*outFlags = 0;
*outMetaState = metaState;
}
return status;
}
掃描碼轉(zhuǎn)化為按鍵碼的過程有點小復雜
- 首先根據(jù) kcm(key character map) 文件進行轉(zhuǎn)換。
- 如果第一步失敗,那么根據(jù) kl(key layout) 文件進行轉(zhuǎn)換。
- 如果前兩步,有一個成功,那么再根據(jù)meta按鍵狀態(tài),重新使用 kcm 文件對按鍵碼再次進行轉(zhuǎn)換。這個只對鍵盤起作用,例如按下 shift ,再按字母鍵,那么會產(chǎn)生大寫的字母的按鍵碼。而對于電源鍵和音量鍵,此步驟可以忽略。
可以發(fā)現(xiàn),kcm 和 kl 文件都可以把按鍵的掃描碼進行轉(zhuǎn)換為按鍵碼,然而 kcm 文件一般都只是針對鍵盤按鍵,而對于電源鍵和音量鍵,一般都是通過 kl 文件進行轉(zhuǎn)換的。
那么如何找到輸入設(shè)備的 kl 文件呢?前面通過 adb shell getevent 可以發(fā)現(xiàn)輸入設(shè)備的文件節(jié)點為 /dev/input/event0,再通過 adb shell dumpsys input 導出所有設(shè)備的信息,就可以找到電源鍵屬于哪個設(shè)備,以及設(shè)備的的按鍵配置文件
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>
從這個信息就可以看出,輸入設(shè)備的 kl 文件為 /system/usr/keylayout/Generic.kl,它的電源鍵映射如下
key 116 POWER
其中,116是十進制,它的十六進制為 0x74,正好就是 adb shell getevent 顯示的電源按鍵的掃描碼。
POWER 就是被映射成的按鍵碼,但是它是一個字符,而實際使用的是 int 類型,這個關(guān)系的映射是在下面定義的
// 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 就是一個策略標志位,在把掃描轉(zhuǎn)換為按鍵碼時,這個策略標志位也會被解析,它表示需要喚醒設(shè)備,上層會根據(jù)這個標志位,讓設(shè)備喚醒。
結(jié)束
InputReader 處理按鍵事件的過程其實很簡單,就是把按鍵事件交給 KeyboardInputMapper 處理,KeyboardInputMapper 根據(jù)配置文件,把按鍵的掃描碼轉(zhuǎn)換為按鍵碼,并同時從配置文件中獲取策略標志位,然后把這些信息包裝成一個事件,發(fā)送到下一環(huán)。
現(xiàn)在,如果項目上讓你完成功能按鍵的映射,或者解除某個按鍵的電源功能,你會了嗎?
以上就是Input系統(tǒng)之InputReader處理按鍵事件詳解的詳細內(nèi)容,更多關(guān)于InputReader處理按鍵事件的資料請關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
Android 搜索結(jié)果匹配關(guān)鍵字且高亮顯示功能
這篇文章主要介紹了Android 搜索結(jié)果匹配關(guān)鍵字且高亮顯示功能,需要的朋友可以參考下2017-05-05
Android自定義StickinessView粘性滑動效果
這篇文章主要為大家詳細介紹了Android自定義StickinessView粘性滑動效果的相關(guān)資料,具有一定的參考價值,感興趣的小伙伴們可以參考一下2017-03-03
Android Studio實現(xiàn)簡單的QQ登錄界面的示例代碼
這篇文章主要介紹了Android Studio實現(xiàn)簡單的QQ登錄界面的示例代碼,文中通過示例代碼介紹的非常詳細,對大家的學習或者工作具有一定的參考學習價值,需要的朋友們下面隨著小編來一起學習學習吧2020-06-06
AndroidStudio替換項目圖標ic_launcher操作
這篇文章主要介紹了AndroidStudio替換項目圖標ic_launcher操作,具有很好的參考價值,希望對大家有所幫助。一起跟隨小編過來看看吧2020-08-08
Android實現(xiàn)滑動加載數(shù)據(jù)的方法
這篇文章主要介紹了Android實現(xiàn)滑動加載數(shù)據(jù)的方法,實例分析了Android通過滑動實現(xiàn)動態(tài)加載數(shù)據(jù)的技巧,具有一定參考借鑒價值,需要的朋友可以參考下2015-07-07
Android的簡單前后端交互(okHttp+springboot+mysql)
這篇文章主要介紹了Android的簡單前后端交互(okHttp+springboot+mysql),文中通過示例代碼介紹的非常詳細,對大家的學習或者工作具有一定的參考學習價值,需要的朋友們下面隨著小編來一起學習學習吧2021-05-05
Android kotlin RecyclerView遍歷json實現(xiàn)列表數(shù)據(jù)的案例
這篇文章主要介紹了Android kotlin RecyclerView遍歷json實現(xiàn)列表數(shù)據(jù)的案例,本文給大家介紹的非常詳細,對大家的學習或工作具有一定的參考借鑒價值,需要的朋友參考下吧2024-08-08
Android中使用開源框架eventbus3.0實現(xiàn)fragment之間的通信交互
本文主要介紹了Android中使用開源框架eventbus3.0實現(xiàn)fragment之間的通信交互的方法,具有很好的參考價值,下面跟著小編一起來看下吧2017-02-02

