Input系统分发策略及其应用示例详解
引言
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;
}
由于音量下键事件被丢弃,因此窗口也收不到这个事件。其实,组合键功能只要触发,两个按键事件,窗口都收不到。
截屏功能不是只能通过 电源键 + 音量下键 触发,还可以通过 音量下键 + 电源键触发,但是分析过程却和上面不一样。如果音量下键先按,那么分发策略会返回一个稍后再试的结果,如果读者有兴趣,可以自行分析。
结束
通过学习本文,我们要达到学以致用的目的,其实最主要的,就是要学会如何自定义组合键。对于硬件上新增的按键事件,如果要截断,可以在截断策略,也可以在分发策略,根据自己所认为的重要性级别来决定。
以上就是Input系统分发策略及其应用示例详解的详细内容,更多关于Input系统分发策略的资料请关注编程网其它相关文章!
免责声明:
① 本站未注明“稿件来源”的信息均来自网络整理。其文字、图片和音视频稿件的所属权归原作者所有。本站收集整理出于非商业性的教育和科研之目的,并不意味着本站赞同其观点或证实其内容的真实性。仅作为临时的测试数据,供内部测试之用。本站并未授权任何人以任何方式主动获取本站任何信息。
② 本站未注明“稿件来源”的临时测试数据将在测试完成后最终做删除处理。有问题或投稿请发送至: 邮箱/279061341@qq.com QQ/279061341