圖解Android - Android GUI 系統 (5) - Android的Event Input System

Android的用戶輸入處理

Android的用戶輸入系統獲取用戶按鍵(或模擬按鍵)輸入,分發給特定的模塊(Framework或應用程序)進行處理,它涉及到以下一些模塊:

  • Input Reader: 負責從硬件獲取輸入,轉換成事件(Event), 並分發給Input Dispatcher.
  • Input Dispatcher: 將Input Reader傳送過來的Events 分發給合適的窗口,並監控ANR。
  • Input Manager Service: 負責Input Reader 和 Input Dispatchor的創建,並提供Policy 用於Events的預處理。
  • Window Manager Service:管理Input Manager 與 View(Window) 以及 ActivityManager 之間的通信。
  • View and Activity:接收按鍵並處理。
  • ActivityManager Service:ANR 處理。

它們之間的關係如下圖所示(黑色箭頭代表控制信號傳遞方向,而紅色箭頭代表用戶輸入數據的傳遞方向)。

這塊代碼很多,但相對來說不難理解,按照慣例,我們先用一張大圖(點擊看大圖)鳥瞰一下全貌先。

四種不同顏色代表了四個不同的線程, InputReader Thread,InputDispatch Thread 和 Server Thread 存在於SystemServer進程裏。UI Thread則存在於Activity所在進程。顏色較深部分是比較重要,需要重點分析的模塊。

初始化

整個輸入系統的初始化可以劃分爲Java 和 Native兩個部分,可以用兩張時序圖分別描述,首先看Java端,

  1. 在SystemServer的初始化過程中,InputManagerService 被創建出來,它做的第一件事情就是初始化Native層,包括EventHub, InputReader 和 InputDispatcher,這一部分我們將在後面詳細介紹。
  2. 當InputManager Service 以及其他的System Service 初始化完成之後,應用程序就開始啓動。如果一個應用程序有Activity(只有Activit能夠接受用戶輸入),它要將自己的Window(ViewRoot)通過setView()註冊到Window Manager Service 中。(詳見圖解Android - Android GUI 系統 (2) - 窗口管理 (View, Canvas, Window Manager))。
  3. 用戶輸入的捕捉和處理髮生在不同的進程裏(生產者:Input Reader 和 Input Dispatcher 在System Server 進程裏,而消耗者,應用程序運行在自己的進程裏),因此用戶輸入事件(Event)的傳遞需要跨進程。在這裏,Android使用了Socket 而不是 Binder來完成。OpenInputChannelPair 生成了兩個Socket的FD, 代表一個雙向通道的兩端,向一端寫入數據,另外一端便可以讀出,反之依然,如果一端沒有寫入數據,另外一端去讀,則陷入阻塞等待。OpenInputChannelPair() 發生在WindowManager Service 內部。爲什麼不用binder? 個人的分析是,Socket可以實現異步的通知,且只需要兩個線程參與(Pipe兩端各一個),假設系統有N個應用程序,跟輸入處理相關的線程數目是 n+1 (1是發送(Input Dispatcher)線程)。然而,如果用Binder實現的話,爲了實現異步接收,每個應用程序需要兩個線程,一個Binder線程,一個後臺處理線程,(不能在Binder線程裏處理輸入,因爲這樣太耗時,將會堵塞住發送端的調用線程)。在發送端,同樣需要兩個線程,一個發送線程,一個接收線程來接收應用的完成通知,所以,N個應用程序需要 2(N+1)個線程。相比之下,Socket還是高效多了。
  4. 通過RegisterInputChannel, Window Manager Service 將剛剛創建的一個Socket FD,封裝在InputWindowHandle(代表一個WindowState) 裏傳給InputManagerService。
  5. InputManagerService 通過JNI(NativeInputManager)最終調用到了InputDispatchor 的 RegisterInputChannel()方法,這裏,一個Connection 對象被創建出來,代表與遠端某個窗口(InputWindowHandle)的一條用戶輸入數據通道。一個Dispatcher可能有多個Connection(多個Window)同時存在。爲了監聽來自於Window的消息,InputDispator 通過AddFd 將這些個FD 加入到Looper中,這樣,只要某個Window在Socket的另一端寫入數據,Looper就會馬上從睡眠中醒來,進行處理。
  6. 到這裏,ViewRootImpl 的 AddWindow 返回,WMS 將SocketPair的另外一個FD 放在返回參數 OutputChannel 裏。
  7. 接着ViewRootImpl 創建了WindowInputEventReceiver 用於接受InputDispatchor 傳過來的事件,後者同樣通過AddFd() 將讀端的Socket FD 加入到Looper中,這樣一旦InputDispatchor發送Event,Looper就會立即醒來處理。

 接下來看剛纔沒有講完的NativeInit。

  1. NativeInit 是 NativeInputManager類的一個方法,在InputManagerService的構造函數中被調用。代碼在 frameworks/base/services/jni/com_android_server_input_inputManagerService.cpp.
  2. 首先創建一個EventHub, 用來監聽所有的event輸入。
  3. 創建一個InputDispatchor對象。
  4. 創建一個InputReader對象,他的輸入是EventHub, 輸出是InputDispatchor。
  5. 然後分別爲InputReader 和 InputDispatchor 創建各自的線程。注意,當前運行在System Server 的 WMThread線程裏。
  6. 接着,InputManagerService 調用NativeStart 通知InputReader 和 InputDispatchor 開始工作。
  7. InputDispatchor是InputReader的消費者,它的線程首先啓動,進入Looper等待狀態。
  8. 接着 InputReader 線程啓動,等待用戶輸入的發生。

至此,一切準備工作就緒,萬事具備,之欠用戶一擊了。

Eventhub 和 Input Reader

Android設備可以同時連接多個輸入設備,比如說觸摸屏,鍵盤,鼠標等等。用戶在任何一個設備上的輸入就會產生一箇中斷,經由Linux內核的中斷處理以及設備驅動轉換成一個Event,並傳遞給用戶空間的應用程序進行處理。每個輸入設備都有自己的驅動程序,數據接口也不盡相同,如何在一個線程裏(上面說過只有一個InputReader Thread)把所有的用戶輸入都給捕捉到? 這首先要歸功於Linux 內核的輸入子系統(Input Subsystem), 它在各種各樣的設備驅動程序上加了一個抽象層,只要底層的設備驅動程序按照這層抽象接口來實現,上層應用就可以通過統一的接口來訪問所有的輸入設備。這個抽象層有三個重要的概念,input handler, input handle 和 input_dev,它們的關係如下圖所示:

    

 

  • input_dev 代表底層的設備,比如圖中的“USB keyboard" 或 "Power Button" (PC的電源鍵),所有設備的input_dev 對象保存在一個全局的input_dev 隊列裏。
  • input_handler 代表某類輸入設備的處理方法,比如說 evdev就是專門處理輸入設備產成的Event(事件),而“sysrq" 是專門處理鍵盤上“sysrq"與其他按鍵組合產生的系統請求,比如“ALT+SysRq+p"(先Ctrl+ALT+F1切換到虛擬終端)可以打印當前CPU的寄存器值。所有的input_handler 存放在 input_handler隊列裏。
  • 一個input_dev 可以有多個input_handler, 比如下圖中“USB Mouse" 設備可以由”evdev" 和 “mousedev" 來分別處理它產生的輸入。
  • 同樣,一個input_handler 可以用於多種輸入設備,比如“USB Keyboard", "Power Button" 都可以產成Event,所以,這些Event都可以交由evdev進行處理。
  • Input handle 用來關聯某個input_dev 和 某個 input_handler, 它對應於下圖中的紫色的原點。每個input handle 都會生成一個文件節點,比如圖中4個 evdev的handle就對應與 /dev/input/下的四個文件"event0~3". 通過input handle, 可以找到對應的input_handler 和 input_dev.

簡單說來,input_dev對應於底層驅動,而input_handler是個上層驅動,而input_handle 提供給應用程序標準的文件訪問接口來打通這條上下通道。通過Linux input system獲取用戶輸入的流程簡單如下:

  1. 設備通過input_register_dev 將自己的驅動註冊到Input 系統。
  2. 各種Handler 通過 input_register_handler將自己註冊到Input系統中。
  3. 每一個註冊進來的input_dev 或 Input_handler 都會通過input_connect() 尋找對方,生成對應的 input_handle,並在/dev/input/下產成一個設備節點文件. 
  4. 應用程序通過打開(Open)Input_handle對應的文件節點,打開其對應的input_dev 和 input_handler的驅動。這樣,當用戶按鍵時,底層驅動就能捕捉到,並交給對應的上次驅動(handler)進行處理,然後返回給應用程序,流程如下圖中紅色箭頭所示。

上圖中的深色點就是 Input Handle, 左邊垂直方向是Input Handler, 而水平方向是Input Dev。 下面是更爲詳細的一個流程圖,感興趣的同學可以點擊大圖看看。

 

所以,只要打開 /dev/input/ 下的所有 event* 設備文件,我們就可以有辦法獲取所有輸入設備的輸入事件,不管它是觸摸屏,還是一個USB 設備,還是一個紅外遙控器。Android中完成這個工作的就是EventHub。

EventHub實現在 framework/base/services/input/EventHub.cpp, 它和InputReader 的工作流程如下圖所示:

 

  1.  NativeInputManager的構造函數裏第一件事情就是創建一個EventHub對象,它的構造函數裏主要生成並初始化幾個控制的FD:
    1. mINotifyFd: 用來監控""/dev/input"目錄下是否有文件生成,有的話說明有新的輸入設備接入,EventHub將從epool_wait中喚醒,來打開新加入的設備。
    2. mWakeReaderFD, mWakeWriterFD: 一個Pipe的兩端,當往mWakeWriteFD 寫入數據的時候,等待在mWakeReaderFD的線程被喚醒,這裏用來給上層應用提供喚醒等待線程,比如說,當上層應用改變輸入屬性需要EventHub進行相應更新時。
    3. mEpollFD,用於epoll_wait()的阻塞等待,這裏通過epoll_ctrl(EPOLL_ADD_FD, fd) 可以等待多個fd的事件,包括上面提到的mINotifyFD, mWakeReaderFD, 以及輸入設備的FD。
  2. 緊接着,InputManagerService啓動InputReader 線程,進入無限的循環,每次循環調用loopOnce(). 第一次循環,會主動掃描 "/dev/input/" 目錄,並打開下面的所有文件,通過ioctl()從底層驅動獲取設備信息,並判斷它的設備類型。這裏處理的設備類型有:INPUT_DEVICE_CLASS_KEYBOARD, INPUT_DEVICE_CLASS_TOUCH, INPUT_DEVICE_CLASS_DPAD,INPUT_DEVICE_CLASS_JOYSTICK 等。
  3. 找到每個設備對應的鍵值映射文件,讀取並生產一個KeyMap 對象。一般來說,設備對應的鍵值映射文件是 "/system/usr/keylayout/Vendor_%04x_Product_%04x".
  4. 將剛纔掃描到的/dev/input 下所有文件的FD 加到epool等待隊列中,調用epool_wait() 開始等待事件的發生。
  5. 某個時間發生,可能是用戶按鍵輸入,也可能是某個設備插入,亦或用戶調整了設備屬性,epoll_wait() 返回,將發生的Event 存放在mPendingEventItems 裏。如果這是一個用戶輸入,系統調用Read() 從驅動讀到這個按鍵的信息,存放在rawEvents裏。
  6. getEvents() 返回,進入InputReader的processEventLocked函數。
  7. 通過rawEvent 找到產生時間的Device,再找到這個Device對應的InputMapper對象,最終生成一個NotifyArgs對象,將其放到NotifyArgs的隊列中。
  8. 第一次循環,或者後面發生設備變化的時候(比如說設備拔插),調用 NativeInputManager 提供的回調,通過JNI通知Java 層的Input Manager Service 做設備變化的相應處理,比如彈出一個提示框提示新設備插入。這部分細節會在後面介紹。
  9. 調用NotifyArgs裏面的Notify()方法,最終調用到InputDispatchor 對應的Notify接口(比如NotifyKey) 將接下來的處理交給InputDispatchor,EventHub 和 InputReader 工作結束,但馬上又開始新的一輪等待,重複6~9的循環。

  

Input Dispatcher

接下來看看目前爲止最長一張時序圖,通過下面18個步驟,事件將發送到應用程序進行處理。

  1. 接上節的最後一步,NotifyKey() 的實現在Input Dispatcher 內部,他首先做簡單的校驗,對於按鍵事件,只有Action 是 AKEY_EVENT_ACTION_DOWN 和 AKEY_EVENT_ACTION_UP,即按下和彈起這兩個Event別接受。
  2. Input Reader 傳給Input Dispather的數據類型是 NotifyKeyArgs, 後者在這裏將其轉換爲 KeyEvent, 然後交由 Policy 來進行第一步的解析和過濾,interceptKeyBeforeQueuing, 對於手機產品,這個工作是在PhoneWindowManager 裏完成,(不同類型的產品可以定義不同的WindowManager, 比如GoogleTV 裏用到的是TVWindowManager)。KeyEvent 在這裏將會被分爲三類:
    1. System Key: 比如說 音量鍵,Power鍵,電話鍵,以及一些特殊的組合鍵,如用於截屏的音量+Power,等等。部分System Key 會在這裏立即處理,比如說電話鍵,但有一些會放到後面去做處理,比如說音量鍵,但不管怎樣,這些鍵不會傳給應用程序,所以稱爲系統鍵。
    2. Global Key:最終產品中可能會有一些特殊的按鍵,它不屬於某個特定的應用,在所有應用中的行爲都是一樣,但也不包含在Andrioid的系統鍵中,比如說GoogleTV 裏會有一個“TV” 按鍵,按它會直接呼起“TV”應用然後收看電視直播,這類按鍵在Android定義爲Global Key.
    3. User Key:除此之外的按鍵就是User Key, 它最終會傳遞到當前的應用窗口。
  3. phoneWindowManager的interceptKeyBeforeQueuing() 最後返回了wmActiions,裏面包含若干個flags,NativeInputManager在handleInterceptActions(), 假如用戶按了Power鍵,這裏會通知Android睡眠或喚醒。最後,返回一個 policyFlags,結束第一次的intercept 過程。
  4. 接下來,按鍵馬上進入第二輪處理。如果用戶在Setting->Accessibility 中選擇打開某些功能,比如說手勢識別,Android的AccessbilityManagerService(輔助功能服務) 會創建一個 InputFilter 對象,它會檢查輸入的事件,根據需要可能會轉換成新的Event,比如說兩根手指頭捏動的手勢最終會變成ZOOM的event. 目前,InputManagerService 只支持一個InputFilter, 新註冊的InputFilter會把老的覆蓋。InputFilter 運行在SystemServer 的 ServerThread 線程裏(除了繪製,窗口管理和Binder調用外,大部分的System Service 都運行在這個線程裏)。而filterInput() 的調用是發生在Input Reader線程裏,通過InputManagerService 裏的 InputFilterHost 對象通知另外一個線程裏的InputFilter 開始真正的解析工作。所以,InputReader 線程從這裏結束一輪的工作,重新進入epoll_wait() 等待新的用戶輸入。InputFilter 的工作也分爲兩個步驟,首先由InputEventConsistencyVerifier 對象(InputEventConsistencyVerifier.java)對輸入事件的完整性做一個檢查,檢查事件的ACTION_DOWN 和 ACTION_UP 是否一一配對。很多同學可能在Android Logcat 裏看到過以下一些類似的打印:"ACTION_UP but key was not down." 就出自此處。接下來,進入到AccessibilityInputFilter 的 onInputEvent(),這裏將把輸入事件(主要是MotionEvent)進行處理,根據需要變成另外一個Event,然後通過sendInputEvent()將事件發回給InputDispatcher。最終調用到injectInputEvent() 將這個事件送入 mInBoundQueue.
  5. 這個時候,InputDispather 還在Looper中睡眠等待,injectInputEvent()通過wake() 將其喚醒。這是進入Input Dispatcher 線程。
  6. InputDispatcher 大部分的工作在 dispatcherOnce 裏完成。首先從mInBoundQueue 中讀出隊列頭部的事件 mPendingEvent, 然後調用 pokeUserActivity(). poke的英文意思是"搓一下, 捅一下“, 這個函數的目的也就是”捅一下“PowerManagerService 提醒它”別睡眠啊,我還活着呢“,最終調用到PowerManagerService 的 updatePowerStateLocked(),防止手機進入休眠狀態。需要注意的是,上述動作不會馬上執行,而是存儲在命令隊列,mCommandQueue裏,這裏面的命令會在後面依次被執行。
  7. 接下來是dispatchKeyLocked(), 第一次進去這個函數的時候,先檢查Event是否已經過處理(interceptBeforeDispatching), 如果沒有,則生成一個命令,同樣放入mCommandQueue裏。
  8. runCommandsLockedInterruptible() 依次執行mCommandQueue 裏的命令,前面說過,pokeUserActivity 會調用PowerManagerService 的 updatePowerStateLocked(), 而 interceptKeyBeforeDispatching() 則最終調用到PhoneWindowManager的同名函數。我們在interceptBeforeQueuing 裏面提到的一些系統按鍵在這個被執行,比如 HOME/MENU/SEARCH 等。
  9. 接下來,處理前面提過GlobalKey,GlobalKeyManager 通過broadcast將這些全局的Event發送給感興趣的應用。最終,interceptKeyBeforeDispatching 將返回一個Int值,-1 代表Skip,這個Event將不會發送給應用程序。0 代表 Continue, 將進入下一步的處理。1 則表明還需要後續的Event才能做出決定。
  10. 命令運行完之後,退出 dispatchOnce, 然後調用pollOnce 進入下一輪等待。但這裏不會被阻塞,因爲timeout值被設成了0.
  11. 第二次進入dispatchKeyLocked(), 這是Event的狀態已經設爲”已處理“,這時候才真正進入了發射階段。
  12. 接下來調用 findFocusedWindowTargetLocked() 獲取當前的焦點窗口,這裏面會做一件非常重要的事情,就是檢測目標應用是否有ANR發生,如果下訴條件滿足,則說明可能發生了ANR:
    1. 目標應用不會空,而目標窗口爲空。說明應用程序在啓動過程中出現了問題。
    2. 目標 Activity 的狀態是Pause,即不再是Focused的應用。
    3. 目標窗口還在處理上一個事件。這個我們下面會說到。
  13. 如果目標窗口處於正常狀態,調用dispatchEventLocked() 進入真正的發送程序。
  14. 這裏,事件又換了一件馬甲,從EventEntry 變成 DispatchEntry, 並送人mOutBoundQueue。然後調用startDispatchCycle() 開始發送。
  15. 最終的發送發生在InputPublish的sendMessage()。這裏就用到了我們前面提到的SocketPair, 一旦sendMessage() 執行,目標窗口所在進程的Looper線程就會被喚醒,然後讀取鍵值並進行處理,這個過程我們下面馬上就會談到。
  16. 乖乖,還沒走完啊?是的,工作還差最後一步,Input Dispatcher給這個窗口發送下一個命令之前,必須等待該窗口的回覆,如果超過5s沒有收到,就會通過Input Manager Service 向Activity Manager 彙報,後者會彈出我們熟知的 "Application No Response" 窗口。所以,事件會放入mWaitQueue進行暫存。如果窗口一切正常,完成按鍵處理後它會調用InputConsumer的sendFinishedSignal() 往SocketPair 裏寫入完成信號,Input Dispatcher 從 Loop中醒來,並從Socket中讀取該信號,然後從mWaitQueue 裏清除該事件標誌其處理完畢。
  17. 並非所有的事件應用程序都會處理,如果沒有處理,窗口程序返回的完成消息裏的 msg.body.finished.handled 會等於false,InputDispatcher 會調用dispatchKeyUnhandled() 將其交給PhoneWindowManager。Android 在這裏提供了一個Fallback機制,如果在 /system/usr/keychars/ 下面的kcm文件裏定義了 fallback關鍵字,Android就識別它爲一個Fallback Keycode。當它的Parent Keycode沒有被應用程序處理,InputDispatcher 會把 Fallback Keycode 當成一個新的Event,重新發給應用程序。下面是一個定義Fallback Key 的例子。如果按了小鍵盤的0且應用程序不受理它,InputDispatcher 會再發送一個'INSERT' event 給應用程序。

    複製代碼
    #/system/usr/keychars/generic.kcm
    ...
    key NUMPAD_0 {
        label: '0'             //打印字符
        base: fallback INSERT  //behavior
        numlock: '0'        //在一個textView裏輸出的字符
    }
    複製代碼
  18. 經歷了重重關卡,一個按鍵發送的流程終於完成了,不管有沒有Fallback Key存在,調用startDispatcherCycle() 開始下一輪征程。。。

史上最長的流程圖終於介紹完了,有點迷糊了?好吧,再看看下面這張圖總結一下:

  • InputDispatcher 是一個異步系統,裏面用到3個Queue(隊列)來保存中間任務和事件,分別是 mInBoundQueue, mOutBoundQueue,mWaitQueue不同隊列的進出劃分了按鍵的不同處理階段。
  • InputReader 採集的輸入實現首先經過InterceptBeforeQueuing處理,Android 系統會將這些按鍵分類(System/Global/User), 這個過程是在InputReader線程裏完成。
  • 如果是Motion Event, filterEvent()可能會將其轉換成其他的Event。然後通過InjectKeyEvent 將這個按鍵發給InputDispatcher。這個過程是在System Process的ServerThread裏完成。
  • 在進入mOutBoundQueue 之前,首先要經過 interceptBeforeDispatching() 的處理,System 和 Global 事件會在這個處理,而不會發送給用戶程序。
  • 通過之前生成的Socket Pair, InputPublish 將 Event發送給當前焦點窗口,然後InputDispatcher將Event放入mWaitQueue 等待窗口的回覆。
  • 如果窗口回覆,該對象被移出mWaitQueue, 一輪事件處理結束。如果窗口沒有處理該事件,從kcm文件裏搜尋Fallback 按鍵,如果有,則重新發送一個新的事件給用戶。
  • 如果超過5s沒有收到用戶回覆,則說明用戶窗口出現阻塞,InputDispather 會通過Input Manager Service發送ANR給ActivityManager。

 

Key processing

前面我們說過,NativeInputEventReceiver() 通過addFd() 將SocketPair的一個FD 加入到UI線程的loop裏,這樣,當Input Dispatcher在Socket的另外一端寫入Event數據,應用程序的UI線程就會從睡眠中醒來,開始事件的處理流程。時序圖如下所示:

 

 

  1.  收到的時間首先會送到隊列中,ViewRootImpl 通過 deliverInputEvent() 向InputStage傳遞消息。
  2. InputStage 是 Android 4.3 新推出的實現,它將輸入事件的處理分成若干個階段(Stage), 如果當前有輸入法窗口,則事件處理從 NativePreIme 開始,否則的話,從EarlyPostIme 開始。事件會依次經過每個Stage,如果該事件沒有被標識爲 “Finished”, 該Stage就會處理它,然後返回處理結果,Forward 或 Finish, Forward 運行下一Stage繼續處理,而Finished事件將會簡單的Forward到下一級,直到最後一級 Synthetic InputStage。流程圖和每個階段完成的事情如下圖所示。

  3. 最後 通過finishInputEvent() 回覆InputDispatcher。

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章