關聯絡列
解析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事件分發過程,簡化為下圖。
- Motion事件在InputReaderThread執行緒中的InputReader進行加工,加工完畢後會判斷是否要喚醒InputDispatcherThread,如果需要喚醒,會在InputDispatcherThread的執行緒迴圈中不斷的用InputDispatcher來分發 Motion事件。
- 將Motion事件交由InputFilter過濾,如果返回值為false,這次Motion事件就會被忽略掉。
- InputReader對Motion事件加工後的資料結構為NotifyMotionArgs,在InputDispatcher的notifyMotion函式中,用NotifyMotionArgs中的事件引數資訊構造一個MotionEntry物件。這個MotionEntry物件會被新增到InputDispatcher的mInboundQueue佇列的末尾。
- 如果mInboundQueue不為空,取出mInboundQueue佇列頭部的EventEntry賦值給mPendingEvent。
- 根據mPendingEvent的值,進行事件丟棄處理。
- 呼叫InputDispatcher的findTouchedWindowTargetsLocked函式,在mWindowHandles視窗列表中為Motion事件找到目標視窗,併為該視窗生成inputTarget。
- 根據inputTarget獲取一個Connection,依賴Connection將輸入事件傳送給目標視窗。
這裡只是簡單的總結了Motion事件分發過程,和Motion事件類似的還有key事件,就需要讀者自行去閱讀原始碼了。
後記
實際上輸入系統還有很多內容需要去講解,比如inputChannel如何和視窗進行程式間通訊,InputDispatcher如何得到視窗的反饋,這些內容會在本系列的後續文章中進行講解。
感謝 《深入理解Android》卷三
分享大前端、Java、跨平臺等技術,關注職業發展和行業動態。