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;
}

对各种分发成果的处理如下

  1. INTERCEPT_KEY_RESULT_TRY_AGAIN_LATER : 上层暂时不知道怎么处理这个事情,所以告知底层等一会再看看。底层收到这个成果,会让线程休眠指定时刻。当时刻到了后,会把重置分发战略成果为 INTERCEPT_KEY_RESULT_UNKNOWN,然后再次查询分发战略,此刻分发战略会给出一个清晰的成果,到底是切断还是不切断。
  2. INTERCEPT_KEY_RESULT_SKIP :上层切断了这个事情,因而让底层跳过这个事情,也便是不丢掉这个事情。
  3. 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;
}

因为音量下键事情被丢掉,因而窗口也收不到这个事情。其实,组合键功用只要触发,两个按键事情,窗口都收不到。

截屏功用不是只能经过 电源键 + 音量下键 触发,还能够经过 音量下键 + 电源键触发,可是剖析进程却和上面不一样。假如音量下键先按,那么分发战略会回来一个稍后再试的成果,假如读者有兴趣,能够自行剖析。

结束

经过学习本文,咱们要达到学以致用的意图,其实最主要的,便是要学会怎么自定义组合键。关于硬件上新增的按键事情,假如要切断,能够在切断战略,也能够在分发战略,依据自己所以为的重要性级别来决定。

写文章真是一个体力活,我一个星期发两篇文章,更是破天荒。别的,本文值反复研究,专家建议点赞,保藏,加关注,有事留言,溜了,溜了~