android的視窗機制分析------事件處理

yangxi_001發表於2014-02-21

    由於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

[java] view plaincopy
  1. requestLayout();  
  2. mInputChannel = new InputChannel();  
  3. try {  
  4.     res = sWindowSession.add(mWindow, mWindowAttributes,  
  5.             getHostVisibility(), mAttachInfo.mContentInsets,  
  6.             mInputChannel);  
  7. catch (RemoteException e) {  

    在ViewRoot和WMS(WindowManagerService)建立起連線之前首先會建立一個InputChannel物件,同樣的WMS端也會建立一個InputChannel物件,不過WMS的建立過程是在ViewRoot呼叫add()方法時呼叫的。InputChannel的構造不做任何操作,所以在ViewRoot中建立InputChannel時尚未初始化,它的初始化過程是在呼叫WMS方法add()時進行的,看到上面程式碼中將mInputChannel作為引數傳遞給WMS,目的就是為了初始化。下面轉到WMS程式碼看看InputChannel的初始化過程。

    addWindow()@WindowManagerService.java

[java] view plaincopy
  1. if (outInputChannel != null) {  
  2.     String name = win.makeInputChannelName();  
  3.     InputChannel[] inputChannels = InputChannel.openInputChannelPair(name);  
  4.     win.mInputChannel = inputChannels[0];  
  5.     inputChannels[1].transferToBinderOutParameter(outInputChannel);  
  6.       
  7.     mInputManager.registerInputChannel(win.mInputChannel);  
  8. }  

    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()的函式中,

  1. void InputReader::process(const RawEvent* rawEvent) {  
  2.     switch (rawEvent->type) {  
  3.     case EventHubInterface::DEVICE_ADDED:  
  4.         addDevice(rawEvent->deviceId);  
  5.         break;  
  6.   
  7.     case EventHubInterface::DEVICE_REMOVED:  
  8.         removeDevice(rawEvent->deviceId);  
  9.         break;  
  10.   
  11.     case EventHubInterface::FINISHED_DEVICE_SCAN:  
  12.         handleConfigurationChanged(rawEvent->when);  
  13.         break;  
  14.   
  15.     default:  
  16.         consumeEvent(rawEvent);  
  17.         break;  
  18.     }  
  19. }  

   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()

    其中EV_REL為事件相對座標,EV_ABS為絕對座標,EV_SYN表示Motion的一系列動作結束。

    Notify InputDispatcher表示不同的事件通知InputDispatcher的函式呼叫,這幾個函式雖然是被InputReaderThread呼叫的,單卻是在InputDispatcher定義的。

    

    2.1.1 notifySwitch()

  1. void InputDispatcher::notifySwitch(nsecs_t when, int32_t switchCode, int32_t switchValue,  
  2.         uint32_t policyFlags) {  
  3. #if DEBUG_INBOUND_EVENT_DETAILS  
  4.     LOGD("notifySwitch - switchCode=%d, switchValue=%d, policyFlags=0x%x",  
  5.             switchCode, switchValue, policyFlags);  
  6. #endif  
  7.   
  8.     policyFlags |= POLICY_FLAG_TRUSTED;  
  9.     mPolicy->notifySwitch(when, switchCode, switchValue, policyFlags);  
  10. }  

    Switch事件的處理是比較簡單的,這是一個與Activity無關的事件,因此我們根本不需要將其dispatch到ViewRoot,所以在notifySwitch()方法中直接通知給PhoneWindowManager去處理即可。從上面的類圖中我們其實可以發現mPolicy指向的就是NativeInputManager,

  1. void NativeInputManager::notifySwitch(nsecs_t when, int32_t switchCode,  
  2.         int32_t switchValue, uint32_t policyFlags) {  
  3. #if DEBUG_INPUT_DISPATCHER_POLICY  
  4.     LOGD("notifySwitch - when=%lld, switchCode=%d, switchValue=%d, policyFlags=0x%x",  
  5.             when, switchCode, switchValue, policyFlags);  
  6. #endif  
  7.   
  8.     JNIEnv* env = jniEnv();  
  9.   
  10.     switch (switchCode) {  
  11.     case SW_LID:  
  12.         env->CallVoidMethod(mCallbacksObj, gCallbacksClassInfo.notifyLidSwitchChanged,  
  13.                 when, switchValue == 0);  
  14.         checkAndClearExceptionFromCallback(env, "notifyLidSwitchChanged");  
  15.         break;  
  16.     }  
  17. }  
    NativeInputManager的notifySwitch()最終會呼叫到notifySwitch()@PhoneWindowManager.java

    2.1.2 notifyKey()

  1. void InputDispatcher::notifyKey(nsecs_t eventTime, int32_t deviceId, int32_t source,  
  2.         uint32_t policyFlags, int32_t action, int32_t flags,  
  3.         int32_t keyCode, int32_t scanCode, int32_t metaState, nsecs_t downTime) {  
  4. #if DEBUG_INBOUND_EVENT_DETAILS  
  5.     LOGD("notifyKey - eventTime=%lld, deviceId=0x%x, source=0x%x, policyFlags=0x%x, action=0x%x, "  
  6.             "flags=0x%x, keyCode=0x%x, scanCode=0x%x, metaState=0x%x, downTime=%lld",  
  7.             eventTime, deviceId, source, policyFlags, action, flags,  
  8.             keyCode, scanCode, metaState, downTime);  
  9. #endif  
  10.     if (! validateKeyEvent(action)) {  
  11.         return;  
  12.     }  
  13.   
  14.     policyFlags |= POLICY_FLAG_TRUSTED;  
  15.     mPolicy->interceptKeyBeforeQueueing(eventTime, deviceId, action, /*byref*/ flags,  
  16.             keyCode, scanCode, /*byref*/ policyFlags);  
  17.   
  18.     bool needWake;  
  19.     { // acquire lock  
  20.         AutoMutex _l(mLock);  
  21.   
  22.         int32_t repeatCount = 0;  
  23.         KeyEntry* newEntry = mAllocator.obtainKeyEntry(eventTime,  
  24.                 deviceId, source, policyFlags, action, flags, keyCode, scanCode,  
  25.                 metaState, repeatCount, downTime);  
  26.   
  27.         needWake = enqueueInboundEventLocked(newEntry);  
  28.     } // release lock  
  29.   
  30.     if (needWake) {  
  31.         mLooper->wake();  
  32.     }  
  33. }  
    InputDispatcher對KeyBoard事件的處理如上述程式碼所述,
    首先,InputDispatcher會擷取這個按鍵事件,根據當前裝置的狀況來優先消化這個事件,這個過程當然是在將事件dispatch給ViewRoot之前。同樣的就像notifySwitch()一樣,最終該過程交由interceptKeyBeforeQueueing()@PhoneWindowManager.java來處理。interceptKeyBeforeQueueing()主要是對一些特殊案件的特殊處理,並判斷該按鍵是夠應該傳遞給ViewRoot。通過設定標誌位policyFlags的值來判斷是否給ViewRoot,例如policyFlags&POLICY_FLAG_PASS_TO_USER == 1 則應該傳遞給ViewRoot。
    interceptKeyBeforeQueueing()特殊處理主要是針對在鎖屏或者螢幕不亮的情況的下收到特殊的鍵值,如音量鍵或者wake鍵。wake鍵是指能夠點亮螢幕的鍵時的操作。
    其次,InputDispatcher再將該按鍵資訊儲存在一個佇列中(enqueueInboundEventLocked()@InputDispatcher.cpp)。
    
  1. Queue<EventEntry> mInboundQueue;  

    2.1.3 notifyMotion()

  1. mPolicy->interceptGenericBeforeQueueing(eventTime, /*byref*/ policyFlags);  
    首先,同樣的,InputDispatcher會擷取這個motion事件,不同的是motion事件的擷取處理NativeInputManager完全有能力處理,所以並沒有交給PhoneWindowManager來處理。檢視程式碼interceptGenericBeforeQueueing()@com_android_server_InputManager.cpp.
    其次,InputDispatcher再將該motion事件資訊儲存在mInboundQueue佇列中(enqueueInboundEventLocked()@InputDispatcher.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中定義了一個包含有丟棄原因的列舉:

  1. enum DropReason {  
  2.     DROP_REASON_NOT_DROPPED = 0,  
  3.     DROP_REASON_POLICY = 1,  
  4.     DROP_REASON_APP_SWITCH = 2,  
  5.     DROP_REASON_DISABLED = 3,  
  6. };  
    1. DROP_REASON_NOT_DROPPED

     不需要丟棄

    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任何事件,比如當系統休眠時或者正在關機時會用到。

相關文章