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 按键的功能就是在这里实现的,分发策略会截断它们。

而分发策略处理一些优先级相对较低的系统事件,例如 home,app switch 事件。由于分发策略处于分发过程中,因此当一个 app 在发生 anr 期间,无论我们按多少次 home, app switch 按键,系统都会没有响应。

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,如果暂时不知道如何处理事件,那么返回正值。

在下一次线程循环时,执行第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;
}

由于音量下键事件被丢弃,因此窗口也收不到这个事件。其实,组合键功能只要触发,两个按键事件,窗口都收不到。

截屏功能不是只能通过 电源键 + 音量下键 触发,还可以通过 音量下键 + 电源键触发,但是分析过程却和上面不一样。如果音量下键先按,那么分发策略会返回一个稍后再试的结果,如果读者有兴趣,可以自行分析。

结束

通过学习本文,我们要达到学以致用的目的,其实最主要的,就是要学会如何自定义组合键。对于硬件上新增的按键事件,如果要截断,可以在截断策略,也可以在分发策略,根据自己所认为的重要性级别来决定。

作者:大胃粥

%s 个评论

要回复文章请先登录注册