Android的用戶輸入處理
Android的用戶輸入系統獲取用戶按鍵(或模擬按鍵)輸入,分發給特定的模塊(Framework或應用程序)進行處理,它涉及到如下一些模塊:html
- 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 處理。
它們之間的關係以下圖所示(黑色箭頭表明控制信號傳遞方向,而紅色箭頭表明用戶輸入數據的傳遞方向)。java
這塊代碼不少,但相對來講不難理解,按照慣例,咱們先用一張大圖(點擊看大圖)鳥瞰一下全貌先。android
四種不一樣顏色表明了四個不一樣的線程, InputReader Thread,InputDispatch Thread 和 Server Thread 存在於SystemServer進程裏。UI Thread則存在於Activity所在進程。顏色較深部分是比較重要,須要重點分析的模塊。app
初始化異步
整個輸入系統的初始化能夠劃分爲Java 和 Native兩個部分,能夠用兩張時序圖分別描述,首先看Java端,
函數
- 在SystemServer的初始化過程當中,InputManagerService 被建立出來,它作的第一件事情就是初始化Native層,包括EventHub, InputReader 和 InputDispatcher,這一部分咱們將在後面詳細介紹。
- 當InputManager Service 以及其餘的System Service 初始化完成以後,應用程序就開始啓動。若是一個應用程序有Activity(只有Activit可以接受用戶輸入),它要將本身的Window(ViewRoot)經過setView()註冊到Window Manager Service 中。(詳見圖解Android - Android GUI 系統 (2) - 窗口管理 (View, Canvas, Window Manager))。
- 用戶輸入的捕捉和處理髮生在不一樣的進程裏(生產者: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。oop
- 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 線程啓動,等待用戶輸入的發生。
至此,一切準備工做就緒,萬事具有,之欠用戶一擊了。 ui
Eventhub 和 Input Reader
Android設備能夠同時鏈接多個輸入設備,好比說觸摸屏,鍵盤,鼠標等等。用戶在任何一個設備上的輸入就會產生一箇中斷,經由Linux內核的中斷處理以及設備驅動轉換成一個Event,並傳遞給用戶空間的應用程序進行處理。每一個輸入設備都有本身的驅動程序,數據接口也不盡相同,如何在一個線程裏(上面說過只有一個InputReader Thread)把全部的用戶輸入都給捕捉到? 這首先要歸功於Linux 內核的輸入子系統(Input Subsystem), 它在各類各樣的設備驅動程序上加了一個抽象層,只要底層的設備驅動程序按照這層抽象接口來實現,上層應用就能夠經過統一的接口來訪問全部的輸入設備。這個抽象層有三個重要的概念,input handler, input handle 和 input_dev,它們的關係以下圖所示:spa
- 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。