前言
這篇文章主要解決以下問題:
- 什麼是Linux標準輸入協議?
- Android Input System架構是怎樣的?
- 如何InputEvent擴展協議?
- Android ANR產生的原理是什麼?如何避免ANR?
- 如何調試Android Input System?
本文結構
1、功能介紹
2、總體設計
3、詳細設計
4、開發調試
5、總結
6、資料
一、功能介紹
Linux 輸入協議在 linux/input.h 內核頭文件中定義了一組標準事件類型和代碼,輸入設備驅動程序負責通過 Linux 輸入協議將設備特定信號轉換爲標準輸入事件格式。
接下來,Android EventHub 組件通過打開與每個輸入設備關聯的 evdev 驅動程序從內核讀取輸入事件。然後,Android InputReader 組件根據設備類別解碼輸入事件,並生成 Android 輸入事件流。在此過程中,Linux 輸入協議事件代碼將根據輸入設備配置、鍵盤佈局文件和各種映射表,轉化爲 Android 事件代碼。
最後,InputReader 將輸入事件發送到 InputDispatcher,後者將這些事件轉發到相應的窗口。(注:這段話選自官方輸入系統文檔)
二、總體設計
2.1 Input System UML圖
2.1.1 InputReader主要做三件事
- mEventHub獲取驅動的RawEvent進行處理
- mDevice將RawEvent 加工成成熟的NotifyArgs,並添加到mQueueInputListener
- 調用mQueueInputListener.flush(),觸發隊列裏的NotifyArgs.notify(),Dispatcher把NotifyArgs加工成EventEntry,並添加到mInboundQueue。
2.1.2 InputDispatcher主要做三件事,
- 從mInboundQueue獲取EventEntry
- 通過mWindowHandlesByDisplay找到焦點窗口
- mConnectionsByFd把EventEntry轉行成InputMessage並分發到InputChannel
2.1.3 ViewRootImpl主要做三件事
- 在scheduleTraversals()中mChoreographer.postCallback()一個mConsumedBatchedInputRunnable
- 等待下一幀到來時,調用mInputEventReceiver.consumeBatchedInputEvents()開始取出InputChannel內的InputMessage轉化成InputEvent。
- mFirstInputStage根據不同的策略把InputEvent分發到不同字View處理。
ViewRootImpl對InputEvent分發過程如下圖。
2. 2 InputEvent 處理流程圖。
根據UML我們可以得出InputEvent數據加工流程圖,如下圖。
- 首先EventHub從驅動獲取RawEvent,接着InputReader根據事件的type把RawEvent加工成Android事件NotifyArgs,比如NotifyMotionArgs或NotifyKeyArgs,然後InputDispatcher把NotifyArgs轉化成EventEntry,然後根據當前焦點窗口,把EventEntry轉化成InputMessage,存放到InputChannel。
- 當處於焦點窗口的應用下一幀渲染觸發的時候,會從InputChannel取出InputMessage,再把InputMessage轉化成InputEvent,比如MotionEvent或KeyEvent,最終發到ViewRootImpl分發到給對應的子View處理。
三、詳細設計
3.1 關鍵類的職責
- InputManager:事件處理的核心,負責事件使用
- InputReaderThread(稱爲“InputReader”)讀取和預處理原始輸入事件,應用策略,並將消息發送到DispatcherThread管理的隊列中。
- InputDispatcherThread(稱爲“InputDispatcher”)線程等待隊列上的新事件,並異步地將它們分派給應用程序。
根據設計,InputReaderThread類和InputDispatcherThread類不共享任何內部狀態。 而且,所有通信都是從InputReaderThread到InputDispatcherThread的一種方式,絕不會相反。 但是,這兩個類都可以與InputDispatchPolicy交互。
InputManager類從不對Java本身進行任何調用。 相反,InputDispatchPolicy負責與系統執行所有外部交互,包括調用DVM服務。
-
EventHub: 事件的中心車站。
EventHub彙總系統上所有輸入設備(包括模擬器)接收的輸入事件。 此外,EventHub通過生成僞造的輸入事件以指示何時添加或刪除設備。
EventHub還提供輸入事件流(通過getEvent方法)。
它還支持查詢輸入設備的當前狀態,例如識別當前按下的鍵。 最後,EventHub還有跟蹤各個輸入設備的功能,例如它們的類別和它們支持的鍵控代碼集。 -
InputReader: InputReader從EventHub讀取原始事件RawEvent數據,並將其處理爲InputEvent,並將其發送到InputListener。 InputReader的某些功能(例如低功耗狀態下的早期事件過濾)由單獨的策略對象控制。
InputReader擁有InputMappers的集合。 它所做的大部分工作都在InputReaderThread上進行,InputReader也可以接收在任意線程上運行的其他系統組件的查詢。 爲了使內容易於管理,InputReader使用單個Mutex來保護其狀態。 互斥對象可以在調用EventHub或InputReaderPolicy時保留,但從不保留在調用InputListener時保留。 -
InputDevice: 表示單個輸入設備的狀態。
InputMapper :輸入映射器將源數據(raw data)轉成熟數據(cooked data),單個輸入設備可以具有多個關聯的輸入映射器,以便解釋事件的不同類別。 -
InputReaderThread:InputReaderThread實現了類Threads.cpp的threadLoop(),Threads內部實現了一個線程循環,會不斷調用threadLoop(),threadLoop()內調用InputReader loopOnce(),loopOnce()方法先從EventHub的getEvent獲取RawEvent,然後把RawEvent傳遞給EventDevice的process(),process()方法內調用EventDevice內所有的InputMapper,InputMapper調用process()把RawEvent加工成不同類型的Event
-
InputDispatcherThread:InputDispatcherThread循環處理入隊和調度事件。
-
InputDispatcherPolicyInterface :輸入調度程序策略接口。
輸入閱讀器策略由InputReader用來與Window Manager和其他系統組件進行交互。
通過JNI到DVM的回調部分支持了實際的實現。 單元測試中也模擬了此接口。 -
InputChannel InputChannel由一個本地unix域套接字組成,該套接字用於跨進程發送和接收輸入消息。 每個通道都有一個用於調試目的的描述性名稱。每個端點都有自己的InputChannel對象,用於指定其文件描述符。釋放所有對輸入通道的引用後,將關閉該通道。
-
InputPublisher: InputDispatcher通過InputPublisher將InputEvent發佈到InputChannel。
-
InputConsumer 消費來自InputChannel的Input Event。
3.2 InputMapper如何加工Raw數據
我們以InputMapper的子類TouchInputMapper爲例分析下Raw數據是如何加工成Touch數據的
首先TouchMapper調用process(),接着調用sync(),然後調用processRawTouches(), 在調用cookAndDispatch(),最後cookPointerData(),這個方法纔是處理對raw數據處理成觸摸數據的。這樣處理完數據後,回到cookAndDispatch()方法,它接下去調用dispatchPointerUsage()分發cookEvent
dispatchMotion()發送事件.
InputDispatcher的dispatchOnce() 接着dispatchOnceInnerLocked(),然後dispatchMotionLocked(),接着dispatchEventLocked() prepareDispatchCycleLocked() ->enqueueDispatchEntriesLocked()->enqueueDispatchEntryLocked() -> traceOutboundQueueLength()
比如我們在MotionEvent收到的壓感Pressure,是乘以mPressureScale纔得到我們MotionEvent中獲取的壓感Pressure。如下圖
這樣當我們想在驅動層對Pressure增加協議時就需要這個知識點了,我們應用上層要先對Pressure進行還原成原始數據,最後才能獲取我們協議的內容。
有了這些知識之後,如果提一個需求,底層要用傾斜角作爲協議字段,上層解析協議,技術可行嗎?
首先我們找到Liunx Input定義的文件
https://source.android.com/devices/input/touch-devices#orientation-and-tilt-fields
接着我們查看官方文檔 Liunx Input ABS_TILT_X和ABS_TILT_Y對應着raw.tiltX和raw.tiltY
最後從Android源碼中找到TouchInputMapper::cookPointerData()中raw.tiltX和raw.tiltY的部分
把raw.tiltX和raw.tiltY三角函數操作得出tilt,tilt是無法逆還原成raw.tiltX和raw.tiltY,所以我們計劃傾斜角作爲協議字段技術是不可行的。
3.1 InputDispatcher是如何調度數據的
分析思路:下面我們圍繞MotionEvent來分析調度流程。
當InputReader處理完事件後,會調用mQueuedInputListener.flush(),flush()會把集合所有的NotifyArgs都notify() ,notify()最終調用InputDispatcher的notify(),notify()內調用mLooper.wake() 喚醒線程loop,最後觸發dispatcherOnce()。
dispatcherOnce()內部實際上是調用dispatchOnceInnerLocked()來調度事件, dispatchOnceInnerLocked()主要作用是從mInboundQueue取出mPendingEvent,然後根據事件類型調度事件。dispatchOnceInnerLocked()代碼如下:
void InputDispatcher::dispatchOnceInnerLocked(nsecs_t* nextWakeupTime) {
// 準備處理新的Event
if (!mPendingEvent) {
if (mInboundQueue.isEmpty()) {
return;
} else {
// 入站隊列中至少有一個Entry
mPendingEvent = mInboundQueue.dequeueAtHead();
traceInboundQueueLengthLocked();
}
// 準備發送事件
resetANRTimeoutsLocked();
}
// 現在我們有一個事件要調度,根據事件type調度事件
// 所有事件最終都會以這種方式出隊和處理,即使我們打算刪除它們也是如此
bool done = false;
switch (mPendingEvent->type) {
case EventEntry::TYPE_CONFIGURATION_CHANGED: {
//省略
break;
}
case EventEntry::TYPE_DEVICE_RESET: {
//省略
break;
}
case EventEntry::TYPE_KEY: {
//省略
break;
}
case EventEntry::TYPE_MOTION: {
MotionEntry* typedEntry = static_cast<MotionEntry*>(mPendingEvent);
// 調度MotionEvent
done = dispatchMotionLocked(currentTime, typedEntry, &dropReason, nextWakeupTime);
break;
}
default:
ALOG_ASSERT(false);
break;
}
if (done) {
// 調度完事件後,重置全局變量狀態,比如mPendingEvent置空。
releasePendingEventLocked();
// 強制下一個輪詢強制喚醒。
*nextWakeupTime = LONG_LONG_MIN;
}
}
從上面代碼我們知道,dispatchOnceInnerLocked()內調用dispatchMotionLocked()來處理MotionEvent,代碼如下:
bool InputDispatcher::dispatchMotionLocked(nsecs_t currentTime, MotionEntry* entry,
DropReason* dropReason, nsecs_t* nextWakeupTime) {
bool isPointerEvent = entry->source & AINPUT_SOURCE_CLASS_POINTER;
// 確定輸入目標。
std::vector<InputTarget> inputTargets;
bool conflictingPointerActions = false;
int32_t injectionResult;
if (isPointerEvent) {
// 指針 event. (eg. 觸摸屏)
// 1. 找到事件所在的焦點窗口inputTargets
injectionResult =
findTouchedWindowTargetsLocked(currentTime, entry, inputTargets, nextWakeupTime,
&conflictingPointerActions);
}
// 2. 從事件或重點顯示中添加監視頻道。
addGlobalMonitoringTargetsLocked(inputTargets, getTargetDisplayId(entry));
// 省略代碼
// 3.調度MotionEvent
dispatchEventLocked(currentTime, entry, inputTargets);
return true;
}
通過上面的代碼我們發現dispatchMotionLocked()方法主要做三件事,首先通過findTouchedWindowTargetsLocked()找到事件所在的焦點窗口,然後inputTargets保存焦點窗口的副本。最後dispatchEventLocked()調度MotionEvent。
findTouchedWindowTargetsLocked()是如何獲取焦點窗口呢?我們看看findTouchedWindowTargetsLocked()方法的實現,代碼如下:
int32_t InputDispatcher::findTouchedWindowTargetsLocked(nsecs_t currentTime,
const MotionEntry* entry,
std::vector<InputTarget>& inputTargets,
nsecs_t* nextWakeupTime,
bool* outConflictingPointerActions) {
// 1.根據Touch類型判斷是否刷新mTempTouchState(可以理解爲緩存了焦點窗口狀態)裏面保存的窗口
// 當新手勢或是Down時,查找焦點窗口
if (newGesture || (isSplit && maskedAction == AMOTION_EVENT_ACTION_POINTER_DOWN)) {
bool isDown = maskedAction == AMOTION_EVENT_ACTION_DOWN;
// 找焦點窗口
sp<InputWindowHandle> newTouchedWindowHandle =
findTouchedWindowAtLocked(displayId, x, y, isDown /*addOutsideTargets*/,
true /*addPortalWindows*/);
if (newTouchedWindowHandle != nullptr) {
// mTempTouchState添加查找到的newTouchedWindowHandle
mTempTouchState.addOrUpdateWindow(newTouchedWindowHandle, targetFlags, pointerIds);
}
mTempTouchState.addGestureMonitors(newGestureMonitors);
} else {
// 事件類型 move/up/cancel/不可分割的指針down
// 從mTempTouchState獲取焦點窗口
}
// 2.檢查mTempTouchState.windows所有窗口是否都已經準備好了。
for (const TouchedWindow& touchedWindow : mTempTouchState.windows) {
if (touchedWindow.targetFlags & InputTarget::FLAG_FOREGROUND) {
// 通過checkWindowReadyForMoreInputLocked()檢測窗口是否ready。
std::string reason =
checkWindowReadyForMoreInputLocked(currentTime, touchedWindow.windowHandle,
entry, "touched");
if (!reason.empty()) {
// 沒有ready則發送ANR
injectionResult = handleTargetsNotReadyLocked(currentTime, entry, nullptr,
touchedWindow.windowHandle,
nextWakeupTime, reason.c_str());
// 代碼跳轉到無響應部分
goto Unresponsive;
}
}
}
}
我們知道方法checkWindowReadyForMoreInputLocked()用於檢測窗口是否準備好了,那他檢測窗口準備好的條件是哪些呢?老方法我們從方法代碼入手,代碼如下:
std::string InputDispatcher::checkWindowReadyForMoreInputLocked(
nsecs_t currentTime, const sp<InputWindowHandle>& windowHandle,
const EventEntry* eventEntry, const char* targetType) {
// 如果窗口被暫停了,則繼續等待
if (windowHandle->getInfo()->paused) {
return StringPrintf("Waiting because the %s window is paused.", targetType);
}
if (eventEntry->type == EventEntry::TYPE_KEY) {
// 判斷key事件的邏輯
} else {
// Touch事件窗口是否ready邏輯
// 會導致暫停輸入事件傳遞的一種情況是:由於應用程序沒有響應,因此waitQueue等待隊列中堆積了很多事件。
if (!connection->waitQueue.isEmpty() &&
currentTime >= connection->waitQueue.head->deliveryTime + STREAM_AHEAD_EVENT_TIMEOUT) {
return StringPrintf("Waiting to send non-key event because the %s window has not "
"finished processing certain input events that were delivered to "
"it over "
"%0.1fms ago. Wait queue length: %d. Wait queue head age: "
"%0.1fms.",
targetType, STREAM_AHEAD_EVENT_TIMEOUT * 0.000001f,
connection->waitQueue.count(),
(currentTime - connection->waitQueue.head->deliveryTime) *
0.000001f);
}
}
}
以上代碼我們可以看出會導致Touch事件無法消費的一種情況是,應用程序的等待隊列堆積了事件,並且等待隊列的頭部事件deliveryTime + STREAM_AHEAD_EVENT_TIMEOUT超過當前事件,就會導致ANR。checkWindowReadyForMoreInputLocked()返回超時原因之後,就會執行handleTargetsNotReadyLocked()發送ANR,代碼如下:
// 默認超時時間5s
constexpr nsecs_t DEFAULT_INPUT_DISPATCHING_TIMEOUT = 5000 * 1000000LL;
int32_t InputDispatcher::handleTargetsNotReadyLocked(
nsecs_t currentTime, const EventEntry* entry,
const sp<InputApplicationHandle>& applicationHandle,
const sp<InputWindowHandle>& windowHandle, nsecs_t* nextWakeupTime, const char* reason) {
nsecs_t timeout;
// 窗口
if (windowHandle != nullptr) {
timeout = windowHandle->getDispatchingTimeout(DEFAULT_INPUT_DISPATCHING_TIMEOUT);
// Application
} else if (applicationHandle != nullptr) {
timeout =
applicationHandle->getDispatchingTimeout(DEFAULT_INPUT_DISPATCHING_TIMEOUT);
} else {
timeout = DEFAULT_INPUT_DISPATCHING_TIMEOUT;
}
onANRLocked(currentTime, applicationHandle, windowHandle, entry->eventTime,
mInputTargetWaitStartTime, reason);
}
handleTargetsNotReadyLocked() 方法會調到onANRLocked(),onANRLocked()會doNotifyANRLockedInterruptible(),doNotifyANRLockedInterruptible()會調用NativeInputManager::notifyANR(),最終調到Java層的InputManagerService.notifyANR(),最終通過WindowManagerServer的InputMonitor把ANR拋給對應的窗口。這就解答了爲什麼ANR?這是因爲應用的UI線程有耗時的操作,導致InputDispatcher的TouchEvent堆積,當超過設置的超時時間(5s)時,就拋出ANR,所以我們把耗時操作儘量放在子線程執行,避免ANR的產生。
擴展問題:在View的onTouchEvent()做耗時操作會導致後續的MotionEvent接收變慢嗎?
答案:是會,原因同樣onTouchEvent()是執行在UI線程,如果做耗時操作,TouchEvent會無法及時分發導致堆積,分發事件的速率變慢。
接着我們回到方法dispatchMotionLocked(),獲取MotionEvent所在焦點窗口後,我們看看dispatchEventLocked()是如何調度MotionEvent,看下代碼
void InputDispatcher::dispatchEventLocked(nsecs_t currentTime, EventEntry* eventEntry,
const std::vector<InputTarget>& inputTargets) {
}
dispatchEventLocked() -> prepareDispatchCycleLocked() -> enqueueDispatchEntriesLocked() -> startDispatchCycleLocked() -> connection->InputPublisher .publishMotionEvent()
InputDispatcher最終把InputEvent發佈到InputPublisher,那Android系統什麼時候處理事件呢?
我們追蹤代碼,發現處理InputEvent的觸發是下一幀渲染開始執行的,下面是方法調用鏈。
ViewRootImpl.scheduleTraversals() -> ViewRootImpl.scheduleConsumeBatchedInput()
FrameDisplayEventReceiver.onVsync() -> Choreographer.doFrame() ->
mConsumedBatchedInputRunnable.run() -> android_view_InputEventReceiver.nativeConsumeBatchedInputEvents() -> NativeInputEventReceiver.consumeEvents() -> InputEventReceiver.dispatchInputEvent() ->WindowInputEventReceiver.onInputEvent()
方法調用鏈大概執行了這些處理,ViewRootImpl執行scheduleTraversals()後在Choreographer內註冊一個mConsumedBatchedInputRunnable,在下一幀開始的時候,mConsumedBatchedInputRunnable.run()會執行,調到native層nativeConsumeBatchedInputEvents(),方法內部實際上是調用NativeInputEventReceiver.consumeEvents()來調批量處理InputEvents,consumeEvents()代碼如下:
status_t NativeInputEventReceiver::consumeEvents(JNIEnv* env,
bool consumeBatches, nsecs_t frameTime, bool* outConsumedBatch) {
for (;;) {
uint32_t seq;
InputEvent* inputEvent;
// 1. 從InputConsumer取出事件
status_t status = mInputConsumer.consume(&mInputEventFactory,
consumeBatches, frameTime, &seq, &inputEvent);
jobject inputEventObj;
switch (inputEvent->getType()) {
case AINPUT_EVENT_TYPE_KEY:
// key 事件
break;
case AINPUT_EVENT_TYPE_MOTION: {
// 2. inputEvent強轉成MotionEvent,再生成Java MotionEvent對象
MotionEvent* motionEvent = static_cast<MotionEvent*>(inputEvent);
inputEventObj = android_view_MotionEvent_obtainAsCopy(env, motionEvent);
break;
}
default:
inputEventObj = NULL;
}
// 3.inputEventObj分發到ViewRootImpl內部類WindowInputEventReceiver.onInputEvent()
env->CallVoidMethod(receiverObj.get(),
gInputEventReceiverClassInfo.dispatchInputEvent, seq, inputEventObj);
}
}
大概是從InputConsumer取出數據InputEvent,並根據InputEvent生成Java成的MotionEvent,最後InputEventReceiver.dispatchInputEvent()把數據分發出去,
##四、開發調試
4.1 dumpsys input
比如我們系統觸摸不穩定,我們懷疑Android Input System傳上來的Input Event有問題,我們如何獲取EventHub/InputReader/InputDispatcher信息呢?
Android 中提供了dumpsys 輸入命令可轉儲系統輸入設備(例如鍵盤和觸摸屏)的狀態以及輸入事件的處理。
命令如下:
adb shell dumpsys input
我們根據InputEvent 的流程分析EventHub/InputReader/InputDispatcher這三種狀態數據是否正確或符合預期,從而排查問題。
dumpsys 輸入診斷詳細官方文檔
4.2 getevent與sendevent
網上有很多資料這裏不再贅述。
getevent/sendevent 使用說明
五、總結
我們關於Android Input System源碼分析就到這裏,源碼分析總的思路就是順着RawEvent的處理過程爲突破口,研究RawEvent是如何加工成InputEvent的?爲何要這樣加工,加工完InputEvent是如何分發到對應的焦點窗口所在的應用進程,應用進程在什麼時機讀取InputEvent,ViewRootImpl如何把InputEvent分發到對應的子View。數據WindowManager與InputManager的關係、ViewRootImpl接收到事件是如何分發的,WindowManager本文暫時沒有分析,留在下節。