Input系统分发策略及其应用示例详解
大胃粥 人气:0引言
Input系统: 按键事件分发 从整体上描绘了通用的事件分发过程,其中有两个比较的环节,一个是截断策略,一个是分发策略。Input系统:截断策略的分析与应用 分析了截断策略及其应用,本文来分析分发策略及其应用。
在正式开始分析前,读者务必仔细地阅读 Input系统: 按键事件分发 ,了解截断策略和分发策略的执行时机。否则,阅读本文没有意义,反而是浪费时间。
分发策略原理
根据 Input系统: 按键事件分发 可知,分发策略发生在事件分发的过程中,并且发生在事件分发循环前,如下
bool InputDispatcher::dispatchKeyLocked(nsecs_t currentTime, std::shared_ptr<KeyEntry> entry, DropReason* dropReason, nsecs_t* nextWakeupTime) { // ... if (entry->interceptKeyResult == KeyEntry::INTERCEPT_KEY_RESULT_TRY_AGAIN_LATER) { // ... } if (entry->interceptKeyResult == KeyEntry::INTERCEPT_KEY_RESULT_UNKNOWN) { if (entry->policyFlags & POLICY_FLAG_PASS_TO_USER) { if (INPUTDISPATCHER_SKIP_EVENT_KEY != 0) { // ... } // 创建一个命令,当命令被执行的时候, // 回调 doInterceptKeyBeforeDispatchingLockedInterruptible() std::unique_ptr<CommandEntry> commandEntry = std::make_unique<CommandEntry>( &InputDispatcher::doInterceptKeyBeforeDispatchingLockedInterruptible); sp<IBinder> focusedWindowToken = mFocusResolver.getFocusedWindowToken(getTargetDisplayId(*entry)); commandEntry->connectionToken = focusedWindowToken; commandEntry->keyEntry = entry; // 把刚创建的命令,加入到队列 mCommandQueue 中 postCommandLocked(std::move(commandEntry)); // 返回 false 等待命令执行 return false; // wait for the command to run } else { // ... } } else if (entry->interceptKeyResult == KeyEntry::INTERCEPT_KEY_RESULT_SKIP) { // ... } // ... // 启动分发循环,把事件分发给目标窗口 dispatchEventLocked(currentTime, entry, inputTargets); return true; }
如代码所示,事件在分发给窗口前,会先执行分发策略。而执行分发策略的方式是创建一个命令 CommandEntry,然后保存到命令队列中。
当命令被执行的时候,会执行 InputDispatcher::doInterceptKeyBeforeDispatchingLockedInterruptible() 函数。
那么,为何要执行分发策略呢?有如下两点原因
- 截断事件,给系统一个优先处理事件的机会。
- 实现组合按键功能。
例如,导航栏上的 home, app switch 按键的功能就是在这里实现的,分发策略会截断它们。
从 Input系统:截断策略的分析与应用 可知,截断策略也可以截断事件,让系统优先处理事件。那么截断策略与分发策略有什么区别呢?
由 Input系统: 按键事件分发 可知,截断策略是处理一些系统级的事件,例如 power 键亮灭屏,这些事件的处理必须让用户感觉没有延时。假如 power 键的事件是在分发流程中处理的,那么必须等到 power 事件前面的所有事件都处理完毕,才能轮到 power 事件被处理,这就可能让用户感觉系统有点不流畅。
而分发策略处理一些优先级相对较低的系统事件,例如 home,app switch 事件。由于分发策略处于分发过程中,因此当一个 app 在发生 anr 期间,无论我们按多少次 home, app switch 按键,系统都会没有响应。
好,回归正题,如上面代码所示,为了执行分发策略,创建了一个命令,并保存到命令队列,然后就返回了。由 Input系统: 按键事件分发 可知,返回到了 InputDispatcher 的线程循环,如下
void InputDispatcher::dispatchOnce() { nsecs_t nextWakeupTime = LONG_LONG_MAX; { // acquire lock std::scoped_lock _l(mLock); mDispatcherIsAlive.notify_all(); // 1. 如果没有命令,分发一次事件 if (!haveCommandsLocked()) { dispatchOnceInnerLocked(&nextWakeupTime); } // 2. 执行命令 // 这个命令来自于前一步的事件分发 if (runCommandsLockedInterruptible()) { // 马上开始下一次的线程循环 nextWakeupTime = LONG_LONG_MIN; } // 处理 ANR ,并返回下一次线程唤醒的时间。 const nsecs_t nextAnrCheck = processAnrsLocked(); nextWakeupTime = std::min(nextWakeupTime, nextAnrCheck); if (nextWakeupTime == LONG_LONG_MAX) { mDispatcherEnteredIdle.notify_all(); } } // release lock nsecs_t currentTime = now(); int timeoutMillis = toMillisecondTimeoutDelay(currentTime, nextWakeupTime); // 3. 线程休眠 timeoutMillis 毫秒 mLooper->pollOnce(timeoutMillis); }
第1步,执行事件分发,不过事件为了执行分发策略,创建了一个命令并保存到命令队列中。
第2步,执行命令队列中的命令。根据前面创建命令时所分析的,会调用如下函数
void InputDispatcher::doInterceptKeyBeforeDispatchingLockedInterruptible( CommandEntry* commandEntry) { // 取出命令中保存的按键事件 KeyEntry& entry = *(commandEntry->keyEntry); KeyEvent event = createKeyEvent(entry); mLock.unlock(); android::base::Timer t; const sp<IBinder>& token = commandEntry->connectionToken; // 执行分发策略 nsecs_t delay = mPolicy->interceptKeyBeforeDispatching(token, &event, entry.policyFlags); if (t.duration() > SLOW_INTERCEPTION_THRESHOLD) { ALOGW("Excessive delay in interceptKeyBeforeDispatching; took %s ms", std::to_string(t.duration().count()).c_str()); } mLock.lock(); // 分发策略的结果保存到 KeyEntry::interceptKeyResult if (delay < 0) { entry.interceptKeyResult = KeyEntry::INTERCEPT_KEY_RESULT_SKIP; } else if (!delay) { entry.interceptKeyResult = KeyEntry::INTERCEPT_KEY_RESULT_CONTINUE; } else { entry.interceptKeyResult = KeyEntry::INTERCEPT_KEY_RESULT_TRY_AGAIN_LATER; entry.interceptKeyWakeupTime = now() + delay; } }
果然,命令在执行时候,为事件 KeyEntry 查询了分发策略,并把分发策略的结果保存到 KeyEntry::interceptKeyResult。
注意,分发策略最终是由上层执行的,如果要截断事件,那么需要返回负值,如果不截断,返回0,如果暂时不知道如何处理事件,那么返回正值。
第2步执行完毕后,会立刻开始下一次的线程循环。如果要理解这一点,需要理解底层的消息机制,读者可能参考我写的 深入理解Native层的消息机制。
在下一次线程循环时,执行第1步时,在事件分发给窗口前,需要根据分发策略的结果,对事件做进一步的处理,如下
bool InputDispatcher::dispatchKeyLocked(nsecs_t currentTime, std::shared_ptr<KeyEntry> entry, DropReason* dropReason, nsecs_t* nextWakeupTime) { // ... // 1. 分发策略的结果表示稍后再尝试分发事件 if (entry->interceptKeyResult == KeyEntry::INTERCEPT_KEY_RESULT_TRY_AGAIN_LATER) { // 还没到超时的时间,计算线程休眠的时间,让线程休眠 if (currentTime < entry->interceptKeyWakeupTime) { if (entry->interceptKeyWakeupTime < *nextWakeupTime) { *nextWakeupTime = entry->interceptKeyWakeupTime; } return false; // wait until next wakeup } // 重置分发策略的结果,为了再一次查询分发策略 // 当再次查询分发策略时,分发策略会给出是否截断的结果 entry->interceptKeyResult = KeyEntry::INTERCEPT_KEY_RESULT_UNKNOWN; entry->interceptKeyWakeupTime = 0; } // Give the policy a chance to intercept the key. if (entry->interceptKeyResult == KeyEntry::INTERCEPT_KEY_RESULT_UNKNOWN) { // 执行分发策略 // ... } else if (entry->interceptKeyResult == KeyEntry::INTERCEPT_KEY_RESULT_SKIP) { // 2. 分发策略的结果表示路过这个事件,也就是丢弃这个事件 // 这里设置了丢弃的原因,下面会根据这个原因,丢弃事件,不会分发给窗口 if (*dropReason == DropReason::NOT_DROPPED) { *dropReason = DropReason::POLICY; } } // 事件有原因需要丢弃,不执行后面的分发循环 if (*dropReason != DropReason::NOT_DROPPED) { setInjectionResult(*entry, *dropReason == DropReason::POLICY ? InputEventInjectionResult::SUCCEEDED : InputEventInjectionResult::FAILED); mReporter->reportDroppedKey(entry->id); return true; } // ... // 启动分发循环,把事件分发给目标窗口 dispatchEventLocked(currentTime, entry, inputTargets); return true; }
对各种分发结果的处理如下
- INTERCEPT_KEY_RESULT_TRY_AGAIN_LATER : 上层暂时不知道如何处理这个事件,所以告诉底层等一会再看看。底层收到这个结果,会让线程休眠指定时间。当时间到了后,会把重置分发策略结果为 INTERCEPT_KEY_RESULT_UNKNOWN,然后再次查询分发策略,此时分发策略会给出一个明确的结果,到底是截断还是不截断。
- INTERCEPT_KEY_RESULT_SKIP :上层截断了这个事件,因此让底层跳过这个事件,也就是不丢弃这个事件。
- INTERCEPT_KEY_RESULT_CONTINUE : 源码中没有明确处理这个结果,很简单嘛,那就是继续后面的事件分发流程。
那么,什么时候上层不知道如何处理一个事件呢?这是为了实现组合键的功能。
当第一个按键按下时,分发策略不知道用户到底会不会按下第二个按键,因此它会告诉底层再等等吧,底层因此休眠了。
如果在底层休眠期间,如果用户按下了第二个按键,那么成功触发组合键的功能,当底层醒来时,再次为第一个按键的事件查询分发策略,此时分发策略知道第一个按键的事件已经触发了组合键功能,因此告诉底层,第一个按键事件截断了,也就是被上层处理了,那么底层就不会分发这第一个按键的事件。
如果在底层休眠期间,如果没有用户按下了第二个按键。当底层醒来时,再次为第一个按键的事件查询分发策略,此时分发策略知道第一个按键事件没有触发组合键的功能,因此告诉底层这个事件不截断,继续分发处理吧。
下面以一个具体的组合键以例,来理解分发策略,因此读者务必仔细理解上面所分析的。
分发策略的应用 - 组合键
以手机上最常见的截断组合键为例,也就是 电源键 + 音量下键,来理解分发策略。但是,请读者务必,先仔细理解上面所分析的。
组合键的功能是由 KeyCombinationManager 管理,它在 PhoneWindowManager 的初始化如下
// PhoneWindowManager.java private void initKeyCombinationRules() { // KeyCombinationManager 是用来实现组合按键功能的类 mKeyCombinationManager = new KeyCombinationManager(); // 配置默认为 true final boolean screenshotChordEnabled = mContext.getResources().getBoolean( com.android.internal.R.bool.config_enableScreenshotChord); if (screenshotChordEnabled) { // 添加 电源键 + 音量下键 组合按键规则 mKeyCombinationManager.addRule( new TwoKeysCombinationRule(KEYCODE_VOLUME_DOWN, KEYCODE_POWER) { @Override void execute() { mPowerKeyHandled = true; // 截屏 interceptScreenshotChord(); } @Override void cancel() { cancelPendingScreenshotChordAction(); } }); } // ... 省略其它组合键的规则 }
很简单,创建一个规则用于实现截屏,并保存到了 KeyCombinationManager#mRules 中。
当按下电源键,首先会经过截断策略处理,注意不是分发策略
// PhoneWindowManager.java public int interceptKeyBeforeQueueing(KeyEvent event, int policyFlags) { // ... if ((event.getFlags() & KeyEvent.FLAG_FALLBACK) == 0) { // 1. 处理按键手势 // 包括组合键 handleKeyGesture(event, interactiveAndOn); } switch (keyCode) { // ... case KeyEvent.KEYCODE_POWER: { // 2. power 按键事件是不传递给用户的 result &= ~ACTION_PASS_TO_USER; // .. break; } // ... } // ... return result; }
第2步,截断策略会截断电源按键事件。
第1步,截断策略处理按键手势,这其中就包括组合键
// PhoneWindowManager.java private void handleKeyGesture(KeyEvent event, boolean interactive) { if (mKeyCombinationManager.interceptKey(event, interactive)) { // handled by combo keys manager. mSingleKeyGestureDetector.reset(); return; } // ... }
现在来看下 KeyCombinationManager 如何处理截屏功能的第一个按键事件,也就是电源事件
boolean interceptKey(KeyEvent event, boolean interactive) { final boolean down = event.getAction() == KeyEvent.ACTION_DOWN; final int keyCode = event.getKeyCode(); final int count = mActiveRules.size(); final long eventTime = event.getEventTime(); // 交互状态,一般指亮屏的状态 // 从这里可以看出,组合键的功能,必须在交互状态下执行 if (interactive && down) { if (mDownTimes.size() > 0) { // ... } if (mDownTimes.get(keyCode) == 0) { // 1. 记录按键按下的时间 mDownTimes.put(keyCode, eventTime); } else { // ignore old key, maybe a repeat key. return false; } if (mDownTimes.size() == 1) { mTriggeredRule = null; // 2. 获取所有与按键相关的规则,保存到 mActiveRules forAllRules(mRules, (rule)-> { if (rule.shouldInterceptKey(keyCode)) { mActiveRules.add(rule); } }); } else { // ... } } else { // ... } return false; }
KeyCombinationManager 处理组合键的第一个按键事件很简单,保存了按键按下的时间,并找到与这个按键相关的规则并保存。
由于电源按键事件被截断,当执行到分发策略时,如下
bool InputDispatcher::dispatchKeyLocked(nsecs_t currentTime, std::shared_ptr<KeyEntry> entry, DropReason* dropReason, nsecs_t* nextWakeupTime) { // ... if (entry->interceptKeyResult == KeyEntry::INTERCEPT_KEY_RESULT_TRY_AGAIN_LATER) { // ... } if (entry->interceptKeyResult == KeyEntry::INTERCEPT_KEY_RESULT_UNKNOWN) { if (entry->policyFlags & POLICY_FLAG_PASS_TO_USER) { // ...不被截断的事件,才会创建命令,用于执行分发策略... return false; // wait for the command to run } else { // 1. 被截断的事件,继续后面的分发流程,最终会被丢弃 entry->interceptKeyResult = KeyEntry::INTERCEPT_KEY_RESULT_CONTINUE; } } else if (entry->interceptKeyResult == KeyEntry::INTERCEPT_KEY_RESULT_SKIP) { // ... } // 2. 如果事件被截断了,就会在这里被丢弃 if (*dropReason != DropReason::NOT_DROPPED) { setInjectionResult(*entry, *dropReason == DropReason::POLICY ? InputEventInjectionResult::SUCCEEDED : InputEventInjectionResult::FAILED); mReporter->reportDroppedKey(entry->id); return true; } // ... // 启动分发循环,把事件分发给窗口 dispatchEventLocked(currentTime, entry, inputTargets); return true; }
被截断策略截断的事件,不会经过分发策略的处理,并且直接被丢弃。这就是窗口为何收不到 power 按键事件的根本原因。
截断的第一个事件,电源事件,已经分析完毕。现在假设用户在很短的时间内,按键下了音量下键。经过截断策略时,仍然首先经过手势处理,此时 KeyCombinationManager 处理第二个按键的过程如下
boolean interceptKey(KeyEvent event, boolean interactive) { final boolean down = event.getAction() == KeyEvent.ACTION_DOWN; final int keyCode = event.getKeyCode(); final int count = mActiveRules.size(); final long eventTime = event.getEventTime(); if (interactive && down) { if (mDownTimes.size() > 0) { if (count > 0 && eventTime > mDownTimes.valueAt(0) + COMBINE_KEY_DELAY_MILLIS) { // 第二个按键按下超时 forAllRules(mActiveRules, (rule)-> rule.cancel()); mActiveRules.clear(); return false; } else if (count == 0) { // has some key down but no active rule exist. return false; } } if (mDownTimes.get(keyCode) == 0) { // 保存第二个按键按下的时间 mDownTimes.put(keyCode, eventTime); } else { // ignore old key, maybe a repeat key. return false; } if (mDownTimes.size() == 1) { // ... } else { // Ignore if rule already triggered. if (mTriggeredRule != null) { return true; } // check if second key can trigger rule, or remove the non-match rule. forAllActiveRules((rule) -> { // 需要在规则的时间内按下第二个按键,才能触发规则 if (!rule.shouldInterceptKeys(mDownTimes)) { return false; } Log.v(TAG, "Performing combination rule : " + rule); // 触发组合键规则 rule.execute(); // 保存已经触发的规则 mTriggeredRule = rule; return true; }); // 清空 mActiveRules,保存已经触发的规则 mActiveRules.clear(); if (mTriggeredRule != null) { mActiveRules.add(mTriggeredRule); return true; } } } else { // ... } return false; }
根据代码可知,只有组合键的第二个按键在规定的时间内按下(150ms),才能触发规则。对于 电源键 + 音量下键,就是触发截屏。
截断策略在处理按键手势时,现在已经触发截屏,那么它是否截断音量下键呢?如果音量下键不用来挂断电话,那就不截断,这段代码请读者自行分析。
我们假设音量下键没有被截断策略截断,那么当它经过分发策略时,如何处理呢?如下
bool InputDispatcher::dispatchKeyLocked(nsecs_t currentTime, std::shared_ptr<KeyEntry> entry, DropReason* dropReason, nsecs_t* nextWakeupTime) { // ... if (entry->interceptKeyResult == KeyEntry::INTERCEPT_KEY_RESULT_TRY_AGAIN_LATER) { // ... } if (entry->interceptKeyResult == KeyEntry::INTERCEPT_KEY_RESULT_UNKNOWN) { // 1. 对于不被截断的事件,创建命令执行分发策略 if (entry->policyFlags & POLICY_FLAG_PASS_TO_USER) { std::unique_ptr<CommandEntry> commandEntry = std::make_unique<CommandEntry>( &InputDispatcher::doInterceptKeyBeforeDispatchingLockedInterruptible); sp<IBinder> focusedWindowToken = mFocusResolver.getFocusedWindowToken(getTargetDisplayId(*entry)); commandEntry->connectionToken = focusedWindowToken; commandEntry->keyEntry = entry; postCommandLocked(std::move(commandEntry)); return false; // wait for the command to run } else { // ... } } else if (entry->interceptKeyResult == KeyEntry::INTERCEPT_KEY_RESULT_SKIP) { // ... } // ... // 启动分发循环,分发事件 dispatchEventLocked(currentTime, entry, inputTargets); return true; }
音量下键事件要执行分发策略,分发策略最终由上层的 PhoneWindowManager 实现,如下
// PhoneWindowManager.java public long interceptKeyBeforeDispatching(IBinder focusedToken, KeyEvent event, int policyFlags) { // ... final long key_consumed = -1; if (mKeyCombinationManager.isKeyConsumed(event)) { // 返回 -1,表示截断事件 return key_consumed; } } // KeyCombinationManager.java boolean isKeyConsumed(KeyEvent event) { if ((event.getFlags() & KeyEvent.FLAG_FALLBACK) != 0) { return false; } // 在触发组合键功能时,mTriggeredRule 保存了触发的规则 return mTriggeredRule != null && mTriggeredRule.shouldInterceptKey(event.getKeyCode()); }
由于已经触发了截屏功能,因此分发策略对音量下键的处理结果是 -1,也就是截断它。
底层收到这个截断信息时,就会丢弃音量下键这个事件,如下
bool InputDispatcher::dispatchKeyLocked(nsecs_t currentTime, std::shared_ptr<KeyEntry> entry, DropReason* dropReason, nsecs_t* nextWakeupTime) { // ... if (entry->interceptKeyResult == KeyEntry::INTERCEPT_KEY_RESULT_TRY_AGAIN_LATER) { // ... } // Give the policy a chance to intercept the key. if (entry->interceptKeyResult == KeyEntry::INTERCEPT_KEY_RESULT_UNKNOWN) { // ... } else if (entry->interceptKeyResult == KeyEntry::INTERCEPT_KEY_RESULT_SKIP) { // 1. 分发策略的结果是事件被截断 if (*dropReason == DropReason::NOT_DROPPED) { *dropReason = DropReason::POLICY; } } // 2. 丢弃被截断的事件 if (*dropReason != DropReason::NOT_DROPPED) { setInjectionResult(*entry, *dropReason == DropReason::POLICY ? InputEventInjectionResult::SUCCEEDED : InputEventInjectionResult::FAILED); mReporter->reportDroppedKey(entry->id); return true; } // ... // 启动分发循环,发送事件给窗口 dispatchEventLocked(currentTime, entry, inputTargets); return true; }
由于音量下键事件被丢弃,因此窗口也收不到这个事件。其实,组合键功能只要触发,两个按键事件,窗口都收不到。
截屏功能不是只能通过 电源键 + 音量下键 触发,还可以通过 音量下键 + 电源键触发,但是分析过程却和上面不一样。如果音量下键先按,那么分发策略会返回一个稍后再试的结果,如果读者有兴趣,可以自行分析。
结束
通过学习本文,我们要达到学以致用的目的,其实最主要的,就是要学会如何自定义组合键。对于硬件上新增的按键事件,如果要截断,可以在截断策略,也可以在分发策略,根据自己所认为的重要性级别来决定。
加载全部内容