由於Android是linux內核的,所以它的事件處理也在linux的基礎上完成的,因此本文我們從linux 內核往應用這個方向慢慢理清它的處理過程。
linux內核提供了一個Input子系統來實現的,Input子系統會在/dev/input/路徑下創建我們硬件輸入設備的節點,一般情況下在我們的手機中這些節點是以eventXX來命名的,如event0,event1等等,但是如果是虛擬機的話,我們可以看到一個mice,這個mice代表鼠標設備,這是由於PC需要使用鼠標來模擬觸屏。由於這些設備節點是硬件相關的,所以每款設備都是不盡相同的。看到了這些輸入的設備節點,我們可能比較困惑這些eventXX到底代表什麼含義呢,也就是說到底是什麼樣的設備創建了這個節點呢?我們可以從/proc/bus/input/devices中讀出eventXX相關的硬件設備,這裏具體的就不多說了,我們只需要知道android讀取事件信息就是從/dev/input/目錄下的設備節點中讀取出來的,算是android事件處理的起源吧,可以讓大家知道按鍵、觸屏等事件是從哪裏來的,不是我們的重點。
首先,簡而言之的介紹一下android事件傳遞的流程,按鍵,觸屏等事件是經由WindowManagerService獲取,並通過共享內存和管道的方式傳遞給ViewRoot,ViewRoot再dispatch給Application的View。當有事件從硬件設備輸入時,system_server端在檢測到事件發生時,通過管道(pipe)通知ViewRoot事件發生,此時ViewRoot再去的內存中讀取這個事件信息。
至於android在事件處理上爲什麼使用共享內存而不是直接使用Binder機制,我的猜測應該是google爲了保證事件響應的實時性,因此在選擇進程間傳遞事件的方式中,選擇了高的共享內存的方式,由於共享內存在數據管理過程中基本不涉及到內存的數據拷貝,只是在進程讀寫時涉及到2次數據拷貝,這個是不可避免的數據拷貝,因此這種方式能夠很好的保證系統對事件的響應,但是僅僅是共享內存是不夠的,因爲共享內存的通信方式並不能夠通知對方有數據更新,因此android在事件處理過程中加入了另一種進程間通信方式管道(pipe),管道的效率不如共享內存高,會不會影響事件處理的實時性?沒關係,每次system_serve通知ViewRoot只是向其傳遞一個字符,即輕巧有簡單,一個字符的多次數據拷貝,我想google還是能夠接受的。
好的,瞭解了一些基本知識後,我們從底層往上層來分析事件的傳遞過程,這裏爲了下文便於理解,首先列出整個事件處理的結構圖。
1. 事件處理系統的初始化過程
前文講到android的事件處理系統,這裏稱爲事件傳遞系統更貼切一些,因爲android事件系統中比較複雜就是其傳遞過程,下面我們就以事件傳遞系統來代替事件處理系統。android事件傳遞系統是以共享內存和管道的進程間通信方式來實現傳遞的,爲了便於理解它的傳遞機制,事件傳遞系統的初始化工作的理解則會顯得非常的重要。
1.1 創建管道連接
事件傳遞系統中的管道的主要作用是在有事件被存儲到共享內存中時,system_server端通知ViewRoot去讀取事件的通信機制。既然是ViewRoot和system_server之間建立管道通信,那麼ViewRoot和WindowManagerService(負責事件傳遞,運行在system_server進程中)各需維護管道的一個文件描述符,其實ViewRoot和WindowManagerService不是各維護了一個管道的文件描述符,而是兩個,當然了這兩個描述符不屬於同一管道,實際上也就是ViewRoot和WindowManagerService之間實現了全雙工的管道通信。
WindowManagerService--->ViewRoot方向的管道通信,表示WMS通知ViewRoot有新事件被寫入到共享內存;
ViewRoot-->WindowManagerService方向的管道通信,表示ViewRoot已經消化完共享內存中的新事件,特此通知WMS。
ViewRoot和WindowManagerService的管道的文件描述符都是被存儲在一個名爲InputChannel的類中,這個InputChannel類是管道通信的載體。
首先來看ViewRoot端的管道的建立。
setView()@ViewRoot.java
- requestLayout();
- mInputChannel = new InputChannel();
- try {
- res = sWindowSession.add(mWindow, mWindowAttributes,
- getHostVisibility(), mAttachInfo.mContentInsets,
- mInputChannel);
- } catch (RemoteException e) {
在ViewRoot和WMS(WindowManagerService)建立起連接之前首先會創建一個InputChannel對象,同樣的WMS端也會創建一個InputChannel對象,不過WMS的創建過程是在ViewRoot調用add()方法時調用的。InputChannel的構造不做任何操作,所以在ViewRoot中創建InputChannel時尚未初始化,它的初始化過程是在調用WMS方法add()時進行的,看到上面代碼中將mInputChannel作爲參數傳遞給WMS,目的就是爲了初始化。下面轉到WMS代碼看看InputChannel的初始化過程。
addWindow()@WindowManagerService.java
- if (outInputChannel != null) {
- String name = win.makeInputChannelName();
- InputChannel[] inputChannels = InputChannel.openInputChannelPair(name);
- win.mInputChannel = inputChannels[0];
- inputChannels[1].transferToBinderOutParameter(outInputChannel);
- mInputManager.registerInputChannel(win.mInputChannel);
- }
outInputChannel爲ViewRoot傳遞來的InputChannel對象,上述代碼主要的工作其實就是創建一對InputChannel,這一對InputChannel中實現了一組全雙工管道。 在創建InputChannel對的同時,會申請共享內存,並向2個InputChannel對象中各自保存一個共享內存的文件描述符。InputChannel創建完成後,會將其中一個的native InputChannel 賦值給outInputChannel,也就是對ViewRoot端InputChannel對象的初始化,這樣隨着ViewRoot和WMS兩端的InputChannel對象的創建,事件傳輸系統的管道通信也就建立了起來。
創建InputChannel pair的過程以及管道建立,共享內存申請的過程就不再列出它的代碼了,請參考openInputChannelPair()@InputTransport.cpp。下圖爲ViewRoot和WMS兩端創建InputChannel pair之後的結構。
1.2 InputChannel的註冊過程
上一節介紹了InputChannel對象的創建過程,這個過程將管道通信建立了起來,但是我們需要清楚的一點是,一個管道通信只是對應一個Activity的事件處理,也就是當前系統中有多少個Activity就會有多少個全雙工管道,那麼系統需要一個管理者來管理以及調度每一個管道通信,因此我們在創建完InputChannel對象後,需要將其註冊到這個管理者中去。
明白了InputChannel對象需要註冊的原因之後,我們再看ViewRoot和WMS端的InputChannel對象各自需要註冊到哪裏?其實也很好理解,兩個InputChannel對象WMS端的是管道通信的sender, ViewRoot端的是Receiver(儘管創建的全雙工,但是目前只使用到了它的一向的通信,另一方向的通信尚未使用),那麼着兩個InputChannel對象肯定需要被兩個不同的管理者來管理。ViewRoot端的一般情況下會註冊到一個NativeInputQueue對象中(這是一個Native的對象,而JAVA端的InputQueue類僅僅是提供了一些static方法與NativeInputQueue通信),只要當用到NativeActivity時,會是另外一種處理機制,這裏我們不管它,NativeActivity畢竟很少用到;WMS端註冊在InputManager對象中。其實從NativeInputQueue和InputManager的名字中也就能知道各自的功能了。
1.2.1 註冊到NativeInputQueue
ViewRoot端InputChannel對象在向NativeInputQueue註冊時,需要註冊3個參數:
1. 將InputChannel對象對應的Native InputChannel傳遞給NativeInputQueue;
2. 將ViewRoot的成員變量InputHandler傳遞給NativeInputQueue,這個InputHandler則是事件的處理函數,傳遞它的作用主要是明確當前ViewRoot的事件處理函數;
3. 還有一個很重要的參數需要傳遞給NativeInputQueue,那就是當前Application的主進程的MessageQueue。
其實,android在實現事件傳輸時,很大程度上借用了線程Looper和MessageQueue的輪詢(poll)機制,通過它的輪詢機制來檢測管道上是否有消息通知事件發生,借用Looper機制能夠很大限度的保證事件能夠第一時間被Application知曉, Looper這塊會單獨分析一下。
在註冊過程中,android會將InputChannel對象中保存的管道的文件描述符交給MessageQueue的native looper去監聽,同時向native looper指示一個回調函數,一旦有事件發生,native looper就會檢測到管道上的數據,同時會去調用指示的回調函數。這個回調函數爲handleReceiveCallback()@android_view_InputQueue.cpp.
當然了,NativeInputQueue對象,整個系統中只有這麼一個,它爲了負責管理這麼多的Application的事件傳遞,android在NativeInputQueue類中定義了一個子類Connection,每個InputChannel對象在註冊時都會創建一個自己的Connection對象。
這一塊的代碼在registerInputChannel()@android_view_InputQueue.cpp
1.2.2 註冊到InputManager
由於WMS端的對linux Input 系統的檢測和ViewRoot對管道接收端的檢測機制不同,前面分析過了,ViewRoot端很好的複用了Application 主線程的Looper輪詢機制來實現對事件響應的實時性,而WMS儘管也有自己的Looper,WMS卻沒像ViewRoot一樣複用自己的Looper機制,至於原因android的code上沒有明確說明,我的猜測應該是WMS是整個系統的,不像ViewRoot一樣每個Activity都有一套,爲了不影響系統的整體性能,儘量不要去影響WMS。
不採用Looper來輪詢是否有事件發生,InputManager啓動了2個進程來管理事件發生與傳遞,InputReaderThread和InputDispatcherThread,InputReaderThread進程負責輪詢事件發生; InputDispatcherThread負責dispatch事件。爲什麼需要2個進程來管理,用一個會出現什麼問題?很明顯,如果用一個話,在輪詢input系統event的時間間隔會變長,有可能丟失事件。
雖然沒有使用Looper來輪詢事件的發生,但是InputDispatcher使用了native looper來輪詢檢查管道通信,這個管道通信表示InputQueue是否消化完成dispatch過去的事件。注意的是這個native looper並不是WMS線程的,而是線程InputDispatcher自定定義的,因此所有的輪詢過程,需要InputDispatcher主動去調用,如
mLooper->pollOnce(timeoutMillis);或者mLooper->wake();。而不像NativeInputQueue一樣,完全不用操心對looper的操作。
WMS在初始化時會創建這麼一個InputManager實例,當然了,它也是系統唯一的。JAVA層的InputManager實例並沒有實現太多的業務,真正實現Input Manager業務是Native的NativeInputManager實例,它在被創建時,建立起了整個WMS端事件傳遞系統的靜態邏輯,如下圖:
NativeInputManager的整個業務的核心其實是InputReader和InputDispatcher兩個模塊,下面簡單介紹一下這兩個模塊。
A. InputReader
InputReader從名稱就可以看出主要任務是讀事件,基本上它所有的業務都包含在了process()的函數中,
- void InputReader::process(const RawEvent* rawEvent) {
- switch (rawEvent->type) {
- case EventHubInterface::DEVICE_ADDED:
- addDevice(rawEvent->deviceId);
- break;
- case EventHubInterface::DEVICE_REMOVED:
- removeDevice(rawEvent->deviceId);
- break;
- case EventHubInterface::FINISHED_DEVICE_SCAN:
- handleConfigurationChanged(rawEvent->when);
- break;
- default:
- consumeEvent(rawEvent);
- break;
- }
- }
process()函數的輸入參數時EventHub模塊提供的,
1.當EventHub尚未打開input系統eventXX設備時,InputReader去向EventHub獲取事件時,EventHub會首先去打開所有的設備,並將每個設備信息以RawEvent的形式返給InputReader,也就是process()中處理的EventHubInterface::DEVICE_ADDED類型,該過程會根據每個設備的deviceId去創建InputDevice,並根據設備的classes來創建對應的InputMapper。如上圖所示。
2.當所有的設備均被打開之後,InputReader去向EventHub獲取事件時,EventHub回去輪詢event節點,如果有事件,InputReader則會消化該事件consumeEvent(rawEvent);
B. InputDispatcher
數據傳輸管理的核心業務是在InputDispatcher中完成的,因此最終WMS端InputChannel對象會註冊到InputDispatcher中,同樣的由於整個系統中InputDispatcher實例只有一個,而WMS端InputChannel對象是和ViewRoot一一對應的,因此InputDispatcher類中也定義了一個內部類Connect來管理各自的InputChannel對象。不同於NativeInputQueue類中的Connect類,InputDispatcher中的Connect類的核心業務是由InputPublisher對象來實現的,該對象負責將發生的事件信息寫入到共享內存。
相關代碼在registerInputChannel()@InputDispatcher.cpp
2. 事件傳遞
經過分析事件處理系統的初始化過程之後,我們已經對事件處理系統的整體架構有了一定程度的理解,那麼下面的事件傳遞過程就會顯得很easy了。
2.1 InputReaderThread線程操作
當input系統有事件發生時,會被InputReaderThread線程輪詢到,InputReader會根據事件的device id來選擇的InputDevice,然後再根據事件的類型來選擇InputDevice中的InputMapper,InputMapper會將事件信息通知給InputDispatcher;
目前adroid在InputReader中實現了5種設備類型的InputMapper,分別爲滑蓋/翻蓋SwitchInputMapper、鍵盤KeyboardInputMapper、軌跡球TrackballInputMapper、多點觸屏MultiTouchInputMapper以及單點觸屏SingleTouchInputMapper。
設備類型 |
InputManager |
EventType |
Notify InputDispatcher |
滑蓋/翻蓋 |
SwitchInputMapper |
EV_SW |
notifySwitch() |
鍵盤 |
KeyboardInputMapper |
EV_KEY |
notifyKey() |
軌跡球 |
TrackballInputMapper |
EV_KEY, EV_REL, EV_SYN |
notifyMotion() |
單點觸屏 |
SingleTouchInputMapper |
EV_KEY, EV_ABS, EV_SYN |
notifyMotion() |
多點觸屏 |
MultiTouchInputMapper |
EV_ABS, EV_SYN |
notifyMotion() |
Notify InputDispatcher表示不同的事件通知InputDispatcher的函數調用,這幾個函數雖然是被InputReaderThread調用的,單卻是在InputDispatcher定義的。
2.1.1 notifySwitch()
- void InputDispatcher::notifySwitch(nsecs_t when, int32_t switchCode, int32_t switchValue,
- uint32_t policyFlags) {
- #if DEBUG_INBOUND_EVENT_DETAILS
- LOGD("notifySwitch - switchCode=%d, switchValue=%d, policyFlags=0x%x",
- switchCode, switchValue, policyFlags);
- #endif
- policyFlags |= POLICY_FLAG_TRUSTED;
- mPolicy->notifySwitch(when, switchCode, switchValue, policyFlags);
- }
- void NativeInputManager::notifySwitch(nsecs_t when, int32_t switchCode,
- int32_t switchValue, uint32_t policyFlags) {
- #if DEBUG_INPUT_DISPATCHER_POLICY
- LOGD("notifySwitch - when=%lld, switchCode=%d, switchValue=%d, policyFlags=0x%x",
- when, switchCode, switchValue, policyFlags);
- #endif
- JNIEnv* env = jniEnv();
- switch (switchCode) {
- case SW_LID:
- env->CallVoidMethod(mCallbacksObj, gCallbacksClassInfo.notifyLidSwitchChanged,
- when, switchValue == 0);
- checkAndClearExceptionFromCallback(env, "notifyLidSwitchChanged");
- break;
- }
- }
2.1.2 notifyKey()
- void InputDispatcher::notifyKey(nsecs_t eventTime, int32_t deviceId, int32_t source,
- uint32_t policyFlags, int32_t action, int32_t flags,
- int32_t keyCode, int32_t scanCode, int32_t metaState, nsecs_t downTime) {
- #if DEBUG_INBOUND_EVENT_DETAILS
- LOGD("notifyKey - eventTime=%lld, deviceId=0x%x, source=0x%x, policyFlags=0x%x, action=0x%x, "
- "flags=0x%x, keyCode=0x%x, scanCode=0x%x, metaState=0x%x, downTime=%lld",
- eventTime, deviceId, source, policyFlags, action, flags,
- keyCode, scanCode, metaState, downTime);
- #endif
- if (! validateKeyEvent(action)) {
- return;
- }
- policyFlags |= POLICY_FLAG_TRUSTED;
- mPolicy->interceptKeyBeforeQueueing(eventTime, deviceId, action, /*byref*/ flags,
- keyCode, scanCode, /*byref*/ policyFlags);
- bool needWake;
- { // acquire lock
- AutoMutex _l(mLock);
- int32_t repeatCount = 0;
- KeyEntry* newEntry = mAllocator.obtainKeyEntry(eventTime,
- deviceId, source, policyFlags, action, flags, keyCode, scanCode,
- metaState, repeatCount, downTime);
- needWake = enqueueInboundEventLocked(newEntry);
- } // release lock
- if (needWake) {
- mLooper->wake();
- }
- }
- Queue<EventEntry> mInboundQueue;
- mPolicy->interceptGenericBeforeQueueing(eventTime, /*byref*/ policyFlags);
首先,同樣的,InputDispatcher會截取這個motion事件,不同的是motion事件的截取處理NativeInputManager完全有能力處理,所以並沒有交給PhoneWindowManager來處理。查看代碼interceptGenericBeforeQueueing()@com_android_server_InputManager.cpp.
2.2 InputDispatcherThread線程操作
InputDispatcherThread線程的輪詢過程dispatchOnce()-->dispatchOnceInnerLocked(), InputDispatcherThread線程不停的執行該操作,以達到輪詢的目的,我們的研究重點也就放在這2個函數處理上。
2.2.1 InputDispatcherThread基本流程
InputDispatcherThread的主要操作是分兩塊同時進行的,
一部分是對InputReader傳遞過來的事件進行dispatch前處理,比如確定focus window,特殊按鍵處理如HOME/ENDCALL等,在預處理完成 後,InputDispatcher會將事件存儲到對應的focus window的outBoundQueue,這個outBoundQueue隊列是InputDispatcher::Connection的成員函數,因此它是和ViewRoot相關的。
一部分是對looper的輪詢,這個輪詢過程是檢查NativeInputQueue是否處理完成上一個事件,如果NativeInputQueue處理完成事件,它就會向通過管道向InputDispatcher發送消息指示consume完成,只有NativeInputQueue consume完成一個事件,InputDispatcher纔會向共享內存寫入另一個事件。
2.2.3 丟棄事件
並不是所有的InputReader發送來的事件我們都需要傳遞給應用,比如上節講到的翻蓋/滑蓋事件,除此之外的按鍵,觸屏,軌跡球(後兩者統一按motion事件處理),也會有部分的事件被丟棄,InputDispatcher總會根據一些規則來丟棄掉一部分事件,我們來分析以下哪些情況下我們需要丟棄掉部分事件?
InputDispatcher.h中定義了一個包含有丟棄原因的枚舉:
- enum DropReason {
- DROP_REASON_NOT_DROPPED = 0,
- DROP_REASON_POLICY = 1,
- DROP_REASON_APP_SWITCH = 2,
- DROP_REASON_DISABLED = 3,
- };
不需要丟棄
2. DROP_REASON_POLICY
設置爲DROP_REASON_POLICY主要有兩種情形:
A. 在InputReader notify InputDispatcher之前,Policy會判斷不需要傳遞給應用的事件。如上一節所述。
B. 在InputDispatcher dispatch事件前,PhoneWindowManager使用方法interceptKeyBeforeDispatching()提前consume掉一些按鍵事件,如上面的流程圖所示。
interceptKeyBeforeDispatching()主要對HOME/MENU/SEARCH按鍵的特殊處理,如果此時能被consume掉,那麼在InputDispatcher 中將被丟棄。
3.DROP_REASON_APP_SWITCH
當有App switch 按鍵如HOME/ENDCALL按鍵發生時,當InputReader向InputDispatcher 傳遞app switch按鍵時,會設置一個APP_SWITCH_TIMEOUT 0.5S的超時時間,當0.5s超時時,InputDispatcher 尚未dispatch到這個app switch按鍵時,InputDispatcher 將會丟棄掉mInboundQueue中所有處在app switch按鍵前的按鍵事件。這麼做的目的是保證app switch按鍵能夠確保被處理。此時被丟棄掉的按鍵會被置爲DROP_REASON_APP_SWITCH。
4. DROP_REASON_DISABLED
這個標誌表示當前的InputDispatcher 被disable掉了,不能dispatch任何事件,比如當系統休眠時或者正在關機時會用到。