Android 10.0 Input System 源碼分析

前言

這篇文章主要解決以下問題:

  1. 什麼是Linux標準輸入協議?
  2. Android Input System架構是怎樣的?
  3. 如何InputEvent擴展協議?
  4. Android ANR產生的原理是什麼?如何避免ANR?
  5. 如何調試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圖

Android Input System UML批註.png

2.1.1 InputReader主要做三件事
  1. mEventHub獲取驅動的RawEvent進行處理
  2. mDevice將RawEvent 加工成成熟的NotifyArgs,並添加到mQueueInputListener
  3. 調用mQueueInputListener.flush(),觸發隊列裏的NotifyArgs.notify(),Dispatcher把NotifyArgs加工成EventEntry,並添加到mInboundQueue。
2.1.2 InputDispatcher主要做三件事,
  1. 從mInboundQueue獲取EventEntry
  2. 通過mWindowHandlesByDisplay找到焦點窗口
  3. mConnectionsByFd把EventEntry轉行成InputMessage並分發到InputChannel
2.1.3 ViewRootImpl主要做三件事
  1. 在scheduleTraversals()中mChoreographer.postCallback()一個mConsumedBatchedInputRunnable
  2. 等待下一幀到來時,調用mInputEventReceiver.consumeBatchedInputEvents()開始取出InputChannel內的InputMessage轉化成InputEvent。
  3. mFirstInputStage根據不同的策略把InputEvent分發到不同字View處理。
    ViewRootImpl對InputEvent分發過程如下圖。
    ViewRootImpl對InputEvent分發過程.png

2. 2 InputEvent 處理流程圖。

根據UML我們可以得出InputEvent數據加工流程圖,如下圖。
image.png

  1. 首先EventHub從驅動獲取RawEvent,接着InputReader根據事件的type把RawEvent加工成Android事件NotifyArgs,比如NotifyMotionArgs或NotifyKeyArgs,然後InputDispatcher把NotifyArgs轉化成EventEntry,然後根據當前焦點窗口,把EventEntry轉化成InputMessage,存放到InputChannel。
  2. 當處於焦點窗口的應用下一幀渲染觸發的時候,會從InputChannel取出InputMessage,再把InputMessage轉化成InputEvent,比如MotionEvent或KeyEvent,最終發到ViewRootImpl分發到給對應的子View處理。

三、詳細設計

3.1 關鍵類的職責

  • InputManager:事件處理的核心,負責事件使用
  1. InputReaderThread(稱爲“InputReader”)讀取和預處理原始輸入事件,應用策略,並將消息發送到DispatcherThread管理的隊列中。
  2. 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。如下圖
image.png
image.png

這樣當我們想在驅動層對Pressure增加協議時就需要這個知識點了,我們應用上層要先對Pressure進行還原成原始數據,最後才能獲取我們協議的內容。

有了這些知識之後,如果提一個需求,底層要用傾斜角作爲協議字段,上層解析協議,技術可行嗎?
首先我們找到Liunx Input定義的文件
https://source.android.com/devices/input/touch-devices#orientation-and-tilt-fields
image.png

接着我們查看官方文檔 Liunx Input ABS_TILT_X和ABS_TILT_Y對應着raw.tiltX和raw.tiltY
最後從Android源碼中找到TouchInputMapper::cookPointerData()中raw.tiltX和raw.tiltY的部分
image.png
把raw.tiltX和raw.tiltY三角函數操作得出tilt,tilt是無法逆還原成raw.tiltX和raw.tiltY,所以我們計劃傾斜角作爲協議字段技術是不可行的。
image.png

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本文暫時沒有分析,留在下節。

六、資料

  1. 官方輸入系統文檔
  2. 輸入系統源碼
  3. Android 5.0(Lollipop)事件輸入系統(Input System)
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章