Android輸入系統(四)輸入事件是如何分發到Window的?

劉望舒發表於2019-01-14

關聯絡列
解析WindowManager系列
解析WMS系列
深入理解JNI系列
輸入系統系列

基於Android 8.1

前言

Android輸入系統(三)InputReader的加工型別和InputDispatcher的分發過程這篇文章中,由於文章篇幅的原因,InputDispatcher的分發過程還有一部分沒有講解,這一部分就是事件分發到目標視窗的過程。

1. 為事件尋找合適的分發目標

我們先來回顧上一篇文章講解的InputDispatcher的dispatchOnceInnerLocked函式: frameworks/native/services/inputflinger/InputDispatcher.cpp

void InputDispatcher::dispatchOnceInnerLocked(nsecs_t* nextWakeupTime) {
    ...
    DropReason dropReason = DROP_REASON_NOT_DROPPED;//1
   ...
    switch (mPendingEvent->type) {//2
    ...
    case EventEntry::TYPE_MOTION: {
        MotionEntry* typedEntry = static_cast<MotionEntry*>(mPendingEvent);
        //如果沒有及時響應視窗切換操作
        if (dropReason == DROP_REASON_NOT_DROPPED && isAppSwitchDue) {
            dropReason = DROP_REASON_APP_SWITCH;
        }
        //事件過期
        if (dropReason == DROP_REASON_NOT_DROPPED
                && isStaleEventLocked(currentTime, typedEntry)) {
            dropReason = DROP_REASON_STALE;
        }
        //阻礙其他視窗獲取事件
        if (dropReason == DROP_REASON_NOT_DROPPED && mNextUnblockedEvent) {
            dropReason = DROP_REASON_BLOCKED;
        }
        done = dispatchMotionLocked(currentTime, typedEntry,
                &dropReason, nextWakeupTime);//3
        break;
    }
    default:
        ALOG_ASSERT(false);
        break;
    }
    ...
}    
複製程式碼

dispatchOnceInnerLocked函式中主要做了5件事,這裡只擷取了其中的一件事:事件的丟棄。 註釋1處的dropReason代表了事件丟棄的原因,它的預設值為DROP_REASON_NOT_DROPPED,代表事件不被丟棄。 註釋2處根據mPendingEvent的type做區分處理,這裡主要擷取了對Motion型別的處理。經過條件語句過濾,會呼叫註釋3處的dispatchMotionLocked函式為Motion事件尋找合適的視窗。 frameworks/native/services/inputflinger/InputDispatcher.cpp

bool InputDispatcher::dispatchMotionLocked(
        nsecs_t currentTime, MotionEntry* entry, DropReason* dropReason, nsecs_t* nextWakeupTime) {
    if (! entry->dispatchInProgress) {
        //標記當前已經進入分發的過程
        entry->dispatchInProgress = true;
        logOutboundMotionDetailsLocked("dispatchMotion - ", entry);
    }
    // 如果事件是需要丟棄的,則返回true,不會去為該事件尋找合適的視窗
    if (*dropReason != DROP_REASON_NOT_DROPPED) {//1
        setInjectionResultLocked(entry, *dropReason == DROP_REASON_POLICY
                ? INPUT_EVENT_INJECTION_SUCCEEDED : INPUT_EVENT_INJECTION_FAILED);
        return true;
    }
    bool isPointerEvent = entry->source & AINPUT_SOURCE_CLASS_POINTER;
    // 目標視窗資訊列表會儲存在inputTargets中
    Vector<InputTarget> inputTargets;//2
    bool conflictingPointerActions = false;
    int32_t injectionResult;
  
    if (isPointerEvent) {
      //處理點選形式的事件,比如觸控螢幕
        injectionResult = findTouchedWindowTargetsLocked(currentTime,
                entry, inputTargets, nextWakeupTime, &conflictingPointerActions);//3
    } else {
        //處理非觸控形式的事件,比如軌跡球
        injectionResult = findFocusedWindowTargetsLocked(currentTime,
                entry, inputTargets, nextWakeupTime);//4
    }
    //輸入事件被掛起,說明找到了視窗並且視窗無響應
    if (injectionResult == INPUT_EVENT_INJECTION_PENDING) {
        return false;
    }
    setInjectionResultLocked(entry, injectionResult);
    //輸入事件沒有分發成功,說明沒有找到合適的視窗
    if (injectionResult != INPUT_EVENT_INJECTION_SUCCEEDED) {
        if (injectionResult != INPUT_EVENT_INJECTION_PERMISSION_DENIED) {
            CancelationOptions::Mode mode(isPointerEvent ?
                    CancelationOptions::CANCEL_POINTER_EVENTS :
                    CancelationOptions::CANCEL_NON_POINTER_EVENTS);
            CancelationOptions options(mode, "input event injection failed");
            synthesizeCancelationEventsForMonitorsLocked(options);
        }
        return true;
    }
   //分發目標新增到inputTargets列表中
    addMonitoringTargetsLocked(inputTargets);//5
    // Dispatch the motion.
    if (conflictingPointerActions) {
        CancelationOptions options(CancelationOptions::CANCEL_POINTER_EVENTS,
                "conflicting pointer actions");
        synthesizeCancelationEventsForAllConnectionsLocked(options);
    }
    //將事件分發給inputTargets列表中的目標
    dispatchEventLocked(currentTime, entry, inputTargets);//6
    return true;
}                                                                 
複製程式碼

註釋1處說明事件是需要丟棄的,這時就會直接返回true,不會為該事件尋找視窗,這次的分發任務就沒有完成,會在下一次InputDispatcherThread的迴圈中再次嘗試分發。註釋3和註釋4處會對點選形式和非觸控形式的事件進行處理,將事件處理的結果交由injectionResult。後面會判斷injectionResult的值,如果injectionResult的值為INPUT_EVENT_INJECTION_PENDING,這說明找到了視窗並且視窗無響應輸入事件被掛起,這時就會返回false;如果injectionResult的值不為INPUT_EVENT_INJECTION_SUCCEEDED,這說明沒有找到合適的視窗,輸入事件沒有分發成功,這時就會返回true。 註釋5處會將分發的目標新增到inputTargets列表中,最終在註釋6處將事件分發給inputTargets列表中的目標。 從註釋2處可以看出inputTargets列表中的儲存的是InputTarget結構體: frameworks/native/services/inputflinger/InputDispatcher.h

struct InputTarget {
  enum {
    //此標記表示事件正在交付給前臺應用程式
    FLAG_FOREGROUND = 1 << 0,
    //此標記指示MotionEvent位於目標區域內
    FLAG_WINDOW_IS_OBSCURED = 1 << 1,
    ...
};
    //inputDispatcher與目標視窗的通訊管道
    sp<InputChannel> inputChannel;//1
    //事件派發的標記
    int32_t flags;
    //螢幕座標系相對於目標視窗座標系的偏移量
    float xOffset, yOffset;//2
    //螢幕座標系相對於目標視窗座標系的縮放係數
    float scaleFactor;//3
    BitSet32 pointerIds;
}                                                                                              
複製程式碼

InputTarget結構體可以說是inputDispatcher與目標視窗的轉換器,其分為兩大部分,一個是列舉中儲存的inputDispatcher與目標視窗互動的標記,另一部分是inputDispatcher與目標視窗互動引數,比如註釋1處的inputChannel,它實際上是一個SocketPair,SocketPair用於程式間雙向通訊,這非常適合inputDispatcher與目標視窗之間的通訊,因為inputDispatcher不僅要將事件分發到目標視窗,同時inputDispatcher也需要得到目標視窗對事件的響應。註釋2處的xOffset和yOffset,螢幕座標系相對於目標視窗座標系的偏移量,MotionEntry(MotionEvent)中的儲存的座標是螢幕座標系,因此就需要註釋2和註釋3處的引數,來將螢幕座標系轉換為目標視窗的座標系。

2. 處理點選形式的事件

在InputDispatcher的dispatchMotionLocked函式的註釋3和註釋4處,分別對Motion事件中的點選形式事件和非觸控形式事件做了處理,由於非觸控形式事件不是很常見,這裡對點選形式事件進行解析。InputDispatcher的findTouchedWindowTargetsLocked函式如有400多行,這裡擷取了需要了解的部分,並且分兩個部分來講解。 frameworks/native/services/inputflinger/InputDispatcher.cpp

1.findTouchedWindowTargetsLocked函式part1:

int32_t InputDispatcher::findTouchedWindowTargetsLocked(nsecs_t currentTime,
        const MotionEntry* entry, Vector<InputTarget>& inputTargets, nsecs_t* nextWakeupTime,
        bool* outConflictingPointerActions) {
    ...
    if (newGesture || (isSplit && maskedAction == AMOTION_EVENT_ACTION_POINTER_DOWN)) {
       //從MotionEntry中獲取座標點
        int32_t pointerIndex = getMotionEventActionPointerIndex(action);
        int32_t x = int32_t(entry->pointerCoords[pointerIndex].
                getAxisValue(AMOTION_EVENT_AXIS_X));
        int32_t y = int32_t(entry->pointerCoords[pointerIndex].
                getAxisValue(AMOTION_EVENT_AXIS_Y));        
        sp<InputWindowHandle> newTouchedWindowHandle;
        bool isTouchModal = false;
        size_t numWindows = mWindowHandles.size();//1
        // 遍歷視窗,找到觸控過的視窗和視窗之外的外部目標
        for (size_t i = 0; i < numWindows; i++) {//2
            //獲取InputDispatcher中代表視窗的windowHandle 
            sp<InputWindowHandle> windowHandle = mWindowHandles.itemAt(i);
            //得到視窗資訊windowInfo 
            const InputWindowInfo* windowInfo = windowHandle->getInfo();
            if (windowInfo->displayId != displayId) {
            //如果displayId不匹配,開始下一次迴圈
                continue; 
            }
            //獲取視窗的flag
            int32_t flags = windowInfo->layoutParamsFlags;
            //如果視窗時可見的
            if (windowInfo->visible) {
               //如果視窗的flag不為FLAG_NOT_TOUCHABLE(視窗是touchable)
                if (! (flags & InputWindowInfo::FLAG_NOT_TOUCHABLE)) {
                   // 如果視窗是focusable或者flag不為FLAG_NOT_FOCUSABLE,則說明該視窗是”可觸控模式“
                    isTouchModal = (flags & (InputWindowInfo::FLAG_NOT_FOCUSABLE
                            | InputWindowInfo::FLAG_NOT_TOUCH_MODAL)) == 0;//3
                   //如果視窗是”可觸控模式或者座標點落在視窗之上             
                    if (isTouchModal || windowInfo->touchableRegionContainsPoint(x, y)) {
                        newTouchedWindowHandle = windowHandle;//4
                        break; // found touched window, exit window loop
                    }
                }
                if (maskedAction == AMOTION_EVENT_ACTION_DOWN
                        && (flags & InputWindowInfo::FLAG_WATCH_OUTSIDE_TOUCH)) {
                    //將符合條件的視窗放入TempTouchState中,以便後續處理。
                    mTempTouchState.addOrUpdateWindow(
                            windowHandle, InputTarget::FLAG_DISPATCH_AS_OUTSIDE, BitSet32(0));//5
                }
            }
        }
複製程式碼

開頭先從MotionEntry中獲取座標,為了後面篩選視窗用。註釋1處獲取列表mWindowHandles的InputWindowHandle數量,InputWindowHandle中儲存儲存了InputWindowInfo,InputWindowInfo中又包含了WindowManager.LayoutParams定義的視窗標誌,關於視窗標誌見Android解析WindowManager(二)Window的屬性這篇文章。除了視窗標誌,InputWindowInfo中還包含了InputChannel和視窗各種屬性,InputWindowInfo描述了可以接收輸入事件的視窗的屬性。這麼看來,InputWindowHandle和WMS中的WindowState很相似。通俗來講,WindowState用來代表WMS中的視窗,而InputWindowHandle用來代表輸入系統中的視窗。 那麼輸入系統是如何得到視窗資訊的呢?這是因為mWindowHandles列表就是WMS更新到InputDispatcher中的。 註釋2處開始遍歷mWindowHandles列表中的視窗,找到觸控過的視窗和視窗之外的外部目標。註釋3處,如果視窗是focusable或者flag不為FLAG_NOT_FOCUSABLE,則說明該視窗是”可觸控模式“。經過層層的篩選,如果視窗是”可觸控模式“或者座標點落在視窗之上,會在註釋4處,將windowHandle賦值給newTouchedWindowHandle。最後在註釋5處,將newTouchedWindowHandle新增到TempTouchState中,以便後續處理。

2.findTouchedWindowTargetsLocked函式part2:

...
    // 確保所有觸控過的前臺視窗都為新的輸入做好了準備
    for (size_t i = 0; i < mTempTouchState.windows.size(); i++) {
        const TouchedWindow& touchedWindow = mTempTouchState.windows[i];
        if (touchedWindow.targetFlags & InputTarget::FLAG_FOREGROUND) {
            // 檢查視窗是否準備好接收更多的輸入
            String8 reason = checkWindowReadyForMoreInputLocked(currentTime,
                    touchedWindow.windowHandle, entry, "touched");//1
            if (!reason.isEmpty()) {//2
            //如果視窗沒有準備好,則將原因賦值給injectionResult 
                injectionResult = handleTargetsNotReadyLocked(currentTime, entry,
                        NULL, touchedWindow.windowHandle, nextWakeupTime, reason.string());//3
             //不做後續的處理,直接跳到Unresponsive標籤          
                goto Unresponsive;//3
            }
        }
    }
    ...
    //程式碼走到這裡,說明視窗已經查詢成功
    injectionResult = INPUT_EVENT_INJECTION_SUCCEEDED;//5
    //遍歷TempTouchState中的視窗
    for (size_t i = 0; i < mTempTouchState.windows.size(); i++) {
        const TouchedWindow& touchedWindow = mTempTouchState.windows.itemAt(i);
        //為每個mTempTouchState中的視窗生成InputTargets 
        addWindowTargetLocked(touchedWindow.windowHandle, touchedWindow.targetFlags,
                touchedWindow.pointerIds, inputTargets);//6
    }
   //在下一次迭代中,刪除外部視窗或懸停觸控視窗
    mTempTouchState.filterNonAsIsTouchWindows();
...
Unresponsive:
    //重置TempTouchState
    mTempTouchState.reset();
    nsecs_t timeSpentWaitingForApplication = getTimeSpentWaitingForApplicationLocked(currentTime);
    updateDispatchStatisticsLocked(currentTime, entry,
            injectionResult, timeSpentWaitingForApplication);
#if DEBUG_FOCUS
    ALOGD("findTouchedWindow finished: injectionResult=%d, injectionPermission=%d, "
            "timeSpentWaitingForApplication=%0.1fms",
            injectionResult, injectionPermission, timeSpentWaitingForApplication / 1000000.0);
#endif
    return injectionResult;
}                                                  
複製程式碼

註釋1處用於檢查視窗是否準備好接收更多的輸入,並將結果賦值給reason。註釋2處,如果reason的值不為空,說明該視窗無法接收更多的輸入,註釋3處的handleTargetsNotReadyLocked函式會得到無法接收更多輸入的原因,賦值給injectionResult,其函式內部會計算視窗處理的時間,如果超時(預設為5秒),就會報ANR,並設定nextWakeupTime的值為LONG_LONG_MIN,強制InputDispatcherThread在下一次迴圈中立即被喚醒,InputDispatcher會重新開始分發輸入事件。這個時候,injectionResult的值為INPUT_EVENT_INJECTION_PENDING。因為視窗無法接收更多的輸入,因此會在註釋4處,呼叫goto語句跳到Unresponsive標籤,Unresponsive標籤中會呼叫TempTouchState的reset函式來重置TempTouchState。 如果程式碼已經走到了註釋5處,說明視窗已經查詢成功,會遍歷TempTouchState中的視窗,在註釋6處為每個TempTouchState中 的視窗生成inputTargets。 在第一小節,InputDispatcher的dispatchMotionLocked函式的註釋6處,會呼叫InputDispatcher的dispatchEventLocked函式 將事件分發給inputTargets列表中的分發目標,接下來我們來檢視下是如何實現的。

3. 向目標視窗傳送事件

InputDispatcher的dispatchEventLocked函式如下所示。 frameworks/native/services/inputflinger/InputDispatcher.cpp

void InputDispatcher::dispatchEventLocked(nsecs_t currentTime,
        EventEntry* eventEntry, const Vector<InputTarget>& inputTargets) {
#if DEBUG_DISPATCH_CYCLE
    ALOGD("dispatchEventToCurrentInputTargets");
#endif
    ALOG_ASSERT(eventEntry->dispatchInProgress); // should already have been set to true
    pokeUserActivityLocked(eventEntry);
    //遍歷inputTargets列表
    for (size_t i = 0; i < inputTargets.size(); i++) {
        const InputTarget& inputTarget = inputTargets.itemAt(i);
        //根據inputTarget內部的inputChannel來獲取Connection的索引
        ssize_t connectionIndex = getConnectionIndexLocked(inputTarget.inputChannel);//1
        if (connectionIndex >= 0) {
              //獲取儲存在mConnectionsByFd容器中的Connection
            sp<Connection> connection = mConnectionsByFd.valueAt(connectionIndex);
            //根據inputTarget,開始事件傳送迴圈
            prepareDispatchCycleLocked(currentTime, connection, eventEntry, &inputTarget);//2
        } else {
#if DEBUG_FOCUS
            ALOGD("Dropping event delivery to target with channel '%s' because it "
                    "is no longer registered with the input dispatcher.",
                    inputTarget.inputChannel->getName().string());
#endif
        }
    }
}   
複製程式碼

遍歷inputTargets列表,獲取每一個inputTarget,註釋1處,根據inputTarget內部的inputChannel來獲取Connection的索引,再根據這個索引作為Key值來獲取mConnectionsByFd容器中的Connection。Connection可以理解為InputDispatcher和目標視窗的連線,其內部包含了連線的狀態、InputChannel、InputWindowHandle和事件佇列等等。註釋2處呼叫prepareDispatchCycleLocked函式根據當前的inputTarget,開始事件傳送迴圈。最終會通過inputTarget中的inputChannel來和視窗進行程式間通訊,最終將Motion事件傳送給目標視窗。

4. Motion事件分發過程總結

結合Android輸入系統(二)IMS的啟動過程和輸入事件的處理Android輸入系統(三)InputReader的加工型別和InputDispatcher的分發過程這兩篇文章,可以總結一下Motion事件分發過程,簡化為下圖。

Android輸入系統(四)輸入事件是如何分發到Window的?

  1. Motion事件在InputReaderThread執行緒中的InputReader進行加工,加工完畢後會判斷是否要喚醒InputDispatcherThread,如果需要喚醒,會在InputDispatcherThread的執行緒迴圈中不斷的用InputDispatcher來分發 Motion事件。
  2. 將Motion事件交由InputFilter過濾,如果返回值為false,這次Motion事件就會被忽略掉。
  3. InputReader對Motion事件加工後的資料結構為NotifyMotionArgs,在InputDispatcher的notifyMotion函式中,用NotifyMotionArgs中的事件引數資訊構造一個MotionEntry物件。這個MotionEntry物件會被新增到InputDispatcher的mInboundQueue佇列的末尾。
  4. 如果mInboundQueue不為空,取出mInboundQueue佇列頭部的EventEntry賦值給mPendingEvent。
  5. 根據mPendingEvent的值,進行事件丟棄處理。
  6. 呼叫InputDispatcher的findTouchedWindowTargetsLocked函式,在mWindowHandles視窗列表中為Motion事件找到目標視窗,併為該視窗生成inputTarget。
  7. 根據inputTarget獲取一個Connection,依賴Connection將輸入事件傳送給目標視窗。

這裡只是簡單的總結了Motion事件分發過程,和Motion事件類似的還有key事件,就需要讀者自行去閱讀原始碼了。

後記

實際上輸入系統還有很多內容需要去講解,比如inputChannel如何和視窗進行程式間通訊,InputDispatcher如何得到視窗的反饋,這些內容會在本系列的後續文章中進行講解。

感謝 《深入理解Android》卷三


分享大前端、Java、跨平臺等技術,關注職業發展和行業動態。

Android輸入系統(四)輸入事件是如何分發到Window的?

相關文章