android的窗口机制分析------事件处理
由于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? ??在ViewRoot和WMS(WindowManagerService)建立起連接之前首先會創建一個InputChannel對象,同樣的WMS端也會創建一個InputChannel對象,不過WMS的創建過程是在ViewRoot調用add()方法時調用的。InputChannel的構造不做任何操作,所以在ViewRoot中創建InputChannel時尚未初始化,它的初始化過程是在調用WMS方法add()時進行的,看到上面代碼中將mInputChannel作為參數傳遞給WMS,目的就是為了初始化。下面轉到WMS代碼看看InputChannel的初始化過程。
? ??addWindow()@WindowManagerService.java
[java]?view plaincopy? ??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()的函數中,
[cpp]?view plaincopy? ?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()
[cpp]?view plaincopy? ? Switch事件的處理是比較簡單的,這是一個與Activity無關的事件,因此我們根本不需要將其dispatch到ViewRoot,所以在notifySwitch()方法中直接通知給PhoneWindowManager去處理即可。從上面的類圖中我們其實可以發現mPolicy指向的就是NativeInputManager, [cpp]?view plaincopy
? ? 2.1.2?notifyKey()
[cpp]?view plaincopy? ? 2.1.3?notifyMotion()
[cpp]?view plaincopy? ? 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中定義了一個包含有丟棄原因的枚舉:
[cpp]?view plaincopy? ? ?不需要丟棄
? ? 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任何事件,比如當系統休眠時或者正在關機時會用到。
原文地址:?http://blog.csdn.net/windskier/article/details/6966264
總結
以上是生活随笔為你收集整理的android的窗口机制分析------事件处理的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: getSystemService() i
- 下一篇: android的窗口机制分析------