Android GUI 系統 (5)
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端,
- 在SystemServer的初始化過程中,InputManagerService 被創建出來,它做的第一件事情就是初始化Native層,包括EventHub, InputReader 和 InputDispatcher,這一部分我們將在後面詳細介紹。
- 當InputManager Service 以及其他的System Service 初始化完成之後,應用程式就開始啟動。如果一個應用程式有Activity(只有Activit能夠接受使用者輸入),它要將自己的Window(ViewRoot)通過setView()註冊到Window Manager Service 中。(詳見
- 使用者輸入的捕捉和處理髮生在不同的程序裡(生產者: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還是高效多了。
- 通過RegisterInputChannel, Window Manager Service 將剛剛建立的一個Socket FD,封裝在InputWindowHandle(代表一個WindowState) 裡傳給InputManagerService。
- InputManagerService 通過JNI(NativeInputManager)最終呼叫到了InputDispatchor 的 RegisterInputChannel()方法,這裡,一個Connection 物件被創建出來,代表與遠端某個視窗(InputWindowHandle)的一條使用者輸入資料通道。一個Dispatcher可能有多個Connection(多個Window)同時存在。為了監聽來自於Window的訊息,InputDispator 通過AddFd 將這些個FD 加入到Looper中,這樣,只要某個Window在Socket的另一端寫入資料,Looper就會馬上從睡眠中醒來,進行處理。
- 到這裡,ViewRootImpl 的 AddWindow 返回,WMS 將SocketPair的另外一個FD 放在返回引數 OutputChannel 裡。
- 接著ViewRootImpl 建立了WindowInputEventReceiver 用於接受InputDispatchor 傳過來的事件,後者同樣通過AddFd() 將讀端的Socket FD 加入到Looper中,這樣一旦InputDispatchor傳送Event,Looper就會立即醒來處理。
接下來看剛才沒有講完的NativeInit。
- NativeInit 是 NativeInputManager類的一個方法,在InputManagerService的建構函式中被呼叫。程式碼在 frameworks/base/services/jni/com_android_server_input_inputManagerService.cpp.
- 首先建立一個EventHub, 用來監聽所有的event輸入。
- 建立一個InputDispatchor物件。
- 建立一個InputReader物件,他的輸入是EventHub, 輸出是InputDispatchor。
- 然後分別為InputReader 和 InputDispatchor 建立各自的執行緒。注意,當前執行在System Server 的 WMThread執行緒裡。
- 接著,InputManagerService 呼叫NativeStart 通知InputReader 和 InputDispatchor 開始工作。
- InputDispatchor是InputReader的消費者,它的執行緒首先啟動,進入Looper等待狀態。
- 接著 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獲取使用者輸入的流程簡單如下:
- 裝置通過input_register_dev 將自己的驅動註冊到Input 系統。
- 各種Handler 通過 input_register_handler將自己註冊到Input系統中。
- 每一個註冊進來的input_dev 或 Input_handler 都會通過input_connect() 尋找對方,生成對應的 input_handle,並在/dev/input/下產成一個裝置節點檔案.
- 應用程式通過開啟(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 的工作流程如下圖所示:
- NativeInputManager的建構函式裡第一件事情就是建立一個EventHub物件,它的建構函式裡主要生成並初始化幾個控制的FD:
- mINotifyFd: 用來監控""/dev/input"目錄下是否有檔案生成,有的話說明有新的輸入裝置接入,EventHub將從epool_wait中喚醒,來開啟新加入的裝置。
- mWakeReaderFD, mWakeWriterFD: 一個Pipe的兩端,當往mWakeWriteFD 寫入資料的時候,等待在mWakeReaderFD的執行緒被喚醒,這裡用來給上層應用提供喚醒等待執行緒,比如說,當上層應用改變輸入屬性需要EventHub進行相應更新時。
- mEpollFD,用於epoll_wait()的阻塞等待,這裡通過epoll_ctrl(EPOLL_ADD_FD, fd) 可以等待多個fd的事件,包括上面提到的mINotifyFD, mWakeReaderFD, 以及輸入裝置的FD。
- 緊接著,InputManagerService啟動InputReader 執行緒,進入無限的迴圈,每次迴圈呼叫loopOnce(). 第一次迴圈,會主動掃描 "/dev/input/" 目錄,並開啟下面的所有檔案,通過ioctl()從底層驅動獲取裝置資訊,並判斷它的裝置型別。這裡處理的裝置型別有:INPUT_DEVICE_CLASS_KEYBOARD, INPUT_DEVICE_CLASS_TOUCH, INPUT_DEVICE_CLASS_DPAD,INPUT_DEVICE_CLASS_JOYSTICK 等。
- 找到每個裝置對應的鍵值對映檔案,讀取並生產一個KeyMap 物件。一般來說,裝置對應的鍵值對映檔案是 "/system/usr/keylayout/Vendor_%04x_Product_%04x".
- 將剛才掃描到的/dev/input 下所有檔案的FD 加到epool等待佇列中,呼叫epool_wait() 開始等待事件的發生。
- 某個時間發生,可能是使用者按鍵輸入,也可能是某個裝置插入,亦或使用者調整了裝置屬性,epoll_wait() 返回,將發生的Event 存放在mPendingEventItems 裡。如果這是一個使用者輸入,系統呼叫Read() 從驅動讀到這個按鍵的資訊,存放在rawEvents裡。
- getEvents() 返回,進入InputReader的processEventLocked函式。
- 通過rawEvent 找到產生時間的Device,再找到這個Device對應的InputMapper物件,最終生成一個NotifyArgs物件,將其放到NotifyArgs的佇列中。
- 第一次迴圈,或者後面發生裝置變化的時候(比如說裝置拔插),呼叫 NativeInputManager 提供的回撥,通過JNI通知Java 層的Input Manager Service 做裝置變化的相應處理,比如彈出一個提示框提示新裝置插入。這部分細節會在後面介紹。
- 呼叫NotifyArgs裡面的Notify()方法,最終呼叫到InputDispatchor 對應的Notify介面(比如NotifyKey) 將接下來的處理交給InputDispatchor,EventHub 和 InputReader 工作結束,但馬上又開始新的一輪等待,重複6~9的迴圈。
Input Dispatcher
接下來看看目前為止最長一張時序圖,通過下面18個步驟,事件將傳送到應用程式進行處理。
- 接上節的最後一步,NotifyKey() 的實現在Input Dispatcher 內部,他首先做簡單的校驗,對於按鍵事件,只有Action 是 AKEY_EVENT_ACTION_DOWN 和 AKEY_EVENT_ACTION_UP,即按下和彈起這兩個Event別接受。
- Input Reader 傳給Input Dispather的資料型別是 NotifyKeyArgs, 後者在這裡將其轉換為 KeyEvent, 然後交由 Policy 來進行第一步的解析和過濾,interceptKeyBeforeQueuing, 對於手機產品,這個工作是在PhoneWindowManager 裡完成,(不同型別的產品可以定義不同的WindowManager, 比如GoogleTV 裡用到的是TVWindowManager)。KeyEvent 在這裡將會被分為三類:
- System Key: 比如說 音量鍵,Power鍵,電話鍵,以及一些特殊的組合鍵,如用於截圖的音量+Power,等等。部分System Key 會在這裡立即處理,比如說電話鍵,但有一些會放到後面去做處理,比如說音量鍵,但不管怎樣,這些鍵不會傳給應用程式,所以稱為系統鍵。
- Global Key:最終產品中可能會有一些特殊的按鍵,它不屬於某個特定的應用,在所有應用中的行為都是一樣,但也不包含在Andrioid的系統鍵中,比如說GoogleTV 裡會有一個“TV” 按鍵,按它會直接呼起“TV”應用然後收看電視直播,這類按鍵在Android定義為Global Key.
- User Key:除此之外的按鍵就是User Key, 它最終會傳遞到當前的應用視窗。
- phoneWindowManager的interceptKeyBeforeQueuing() 最後返回了wmActiions,裡面包含若干個flags,NativeInputManager在handleInterceptActions(), 假如使用者按了Power鍵,這裡會通知Android睡眠或喚醒。最後,返回一個 policyFlags,結束第一次的intercept 過程。
- 接下來,按鍵馬上進入第二輪處理。如果使用者在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.
- 這個時候,InputDispather 還在Looper中睡眠等待,injectInputEvent()通過wake() 將其喚醒。這是進入Input Dispatcher 執行緒。
- InputDispatcher 大部分的工作在 dispatcherOnce 裡完成。首先從mInBoundQueue 中讀出佇列頭部的事件 mPendingEvent, 然後呼叫 pokeUserActivity(). poke的英文意思是"搓一下, 捅一下“, 這個函式的目的也就是”捅一下“PowerManagerService 提醒它”別睡眠啊,我還活著呢“,最終呼叫到PowerManagerService 的 updatePowerStateLocked(),防止手機進入休眠狀態。需要注意的是,上述動作不會馬上執行,而是儲存在命令佇列,mCommandQueue裡,這裡面的命令會在後面依次被執行。
- 接下來是dispatchKeyLocked(), 第一次進去這個函式的時候,先檢查Event是否已經過處理(interceptBeforeDispatching), 如果沒有,則生成一個命令,同樣放入mCommandQueue裡。
- runCommandsLockedInterruptible() 依次執行mCommandQueue 裡的命令,前面說過,pokeUserActivity 會呼叫PowerManagerService 的 updatePowerStateLocked(), 而 interceptKeyBeforeDispatching() 則最終呼叫到PhoneWindowManager的同名函式。我們在interceptBeforeQueuing 裡面提到的一些系統按鍵在這個被執行,比如 HOME/MENU/SEARCH 等。
- 接下來,處理前面提過GlobalKey,GlobalKeyManager 通過broadcast將這些全域性的Event傳送給感興趣的應用。最終,interceptKeyBeforeDispatching 將返回一個Int值,-1 代表Skip,這個Event將不會發送給應用程式。0 代表 Continue, 將進入下一步的處理。1 則表明還需要後續的Event才能做出決定。
- 命令執行完之後,退出 dispatchOnce, 然後呼叫pollOnce 進入下一輪等待。但這裡不會被阻塞,因為timeout值被設成了0.
- 第二次進入dispatchKeyLocked(), 這是Event的狀態已經設為”已處理“,這時候才真正進入了發射階段。
- 接下來呼叫 findFocusedWindowTargetLocked() 獲取當前的焦點視窗,這裡面會做一件非常重要的事情,就是檢測目標應用是否有ANR發生,如果下訴條件滿足,則說明可能發生了ANR:
- 目標應用不會空,而目標視窗為空。說明應用程式在啟動過程中出現了問題。
- 目標 Activity 的狀態是Pause,即不再是Focused的應用。
- 目標視窗還在處理上一個事件。這個我們下面會說到。
- 如果目標視窗處於正常狀態,呼叫dispatchEventLocked() 進入真正的傳送程式。
- 這裡,事件又換了一件馬甲,從EventEntry 變成 DispatchEntry, 並送人mOutBoundQueue。然後呼叫startDispatchCycle() 開始傳送。
- 最終的傳送發生在InputPublish的sendMessage()。這裡就用到了我們前面提到的SocketPair, 一旦sendMessage() 執行,目標視窗所在程序的Looper執行緒就會被喚醒,然後讀取鍵值並進行處理,這個過程我們下面馬上就會談到。
- 乖乖,還沒走完啊?是的,工作還差最後一步,Input Dispatcher給這個視窗傳送下一個命令之前,必須等待該視窗的回覆,如果超過5s沒有收到,就會通過Input Manager Service 向Activity Manager 彙報,後者會彈出我們熟知的 "Application No Response" 視窗。所以,事件會放入mWaitQueue進行暫存。如果視窗一切正常,完成按鍵處理後它會呼叫InputConsumer的sendFinishedSignal() 往SocketPair 裡寫入完成訊號,Input Dispatcher 從 Loop中醒來,並從Socket中讀取該訊號,然後從mWaitQueue 裡清除該事件標誌其處理完畢。
- 並非所有的事件應用程式都會處理,如果沒有處理,視窗程式返回的完成訊息裡的 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裡輸出的字元 }
- 經歷了重重關卡,一個按鍵傳送的流程終於完成了,不管有沒有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執行緒就會從睡眠中醒來,開始事件的處理流程。時序圖如下所示:
- 收到的時間首先會送到佇列中,ViewRootImpl 通過 deliverInputEvent() 向InputStage傳遞訊息。
- InputStage 是 Android 4.3 新推出的實現,它將輸入事件的處理分成若干個階段(Stage), 如果當前有輸入法視窗,則事件處理從 NativePreIme 開始,否則的話,從EarlyPostIme 開始。事件會依次經過每個Stage,如果該事件沒有被標識為 “Finished”, 該Stage就會處理它,然後返回處理結果,Forward 或 Finish, Forward 執行下一Stage繼續處理,而Finished事件將會簡單的Forward到下一級,直到最後一級 Synthetic
InputStage。流程圖和每個階段完成的事情如下圖所示。
- 最後 通過finishInputEvent() 回覆InputDispatcher。