Input系统分发策略及其应用示例详解

引言

Input系统: 按键事件分发 从整体上描绘了通用的事件分发过程,其中有两个比较的环节,一个是截断策略,一个是分发策略。Input系统:截断策略的分析与应用 分析了截断策略及其应用,本文来分析分发策略及其应用。

在正式开始分析前,读者务必仔细地阅读 Input系统: 按键事件分发 ,了解截断策略和分发策略的执行时机。否则,阅读本文没有意义,反而是浪费时间。

分发策略原理

根据 Input系统: 按键事件分发 可知,分发策略发生在事件分发的过程中,并且发生在事件分发循环前,如下

bool InputDispatcher::dispatchKeyLocked(nsecs_t currentTime, std::shared_ptr 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 = std::make_unique(
                    &InputDispatcher::doInterceptKeyBeforeDispatchingLockedInterruptible);
            sp 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& 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 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 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 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 = std::make_unique(
                    &InputDispatcher::doInterceptKeyBeforeDispatchingLockedInterruptible);
            sp 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 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系统分发策略的资料请关注脚本之家其它相关文章!

你可能感兴趣的:(Input系统分发策略及其应用示例详解)