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;
}
因为音量下键事情被丢掉,因而窗口也收不到这个事情。其实,组合键功用只要触发,两个按键事情,窗口都收不到。
截屏功用不是只能经过 电源键 + 音量下键 触发,还能够经过 音量下键 + 电源键触发,可是剖析进程却和上面不一样。假如音量下键先按,那么分发战略会回来一个稍后再试的成果,假如读者有兴趣,能够自行剖析。
结束
经过学习本文,咱们要达到学以致用的意图,其实最主要的,便是要学会怎么自定义组合键。关于硬件上新增的按键事情,假如要切断,能够在切断战略,也能够在分发战略,依据自己所以为的重要性级别来决定。
写文章真是一个体力活,我一个星期发两篇文章,更是破天荒。别的,本文值反复研究,专家建议点赞,保藏,加关注,有事留言,溜了,溜了~