日本免费高清视频-国产福利视频导航-黄色在线播放国产-天天操天天操天天操天天操|www.shdianci.com

學無先后,達者為師

網站首頁 編程語言 正文

Input系統分發策略及其應用示例詳解_Android

作者:大胃粥 ? 更新時間: 2023-04-03 編程語言

引言

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

由于音量下鍵事件被丟棄,因此窗口也收不到這個事件。其實,組合鍵功能只要觸發,兩個按鍵事件,窗口都收不到。

截屏功能不是只能通過 電源鍵 + 音量下鍵 觸發,還可以通過 音量下鍵 + 電源鍵觸發,但是分析過程卻和上面不一樣。如果音量下鍵先按,那么分發策略會返回一個稍后再試的結果,如果讀者有興趣,可以自行分析。

結束

通過學習本文,我們要達到學以致用的目的,其實最主要的,就是要學會如何自定義組合鍵。對于硬件上新增的按鍵事件,如果要截斷,可以在截斷策略,也可以在分發策略,根據自己所認為的重要性級別來決定。

原文鏈接:https://juejin.cn/post/7195577258906878007

欄目分類
最近更新