歡迎來到Linux教程網
Linux教程網
Linux教程網
Linux教程網
Linux教程網 >> Linux編程 >> Linux編程 >> Android的窗口機制分析-事件處理

Android的窗口機制分析-事件處理

日期:2017/3/1 10:57:05   编辑:Linux編程
由於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

  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

  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. mInputManager.registerInputChannel(win.mInputChannel);
  7. }

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. case EventHubInterface::DEVICE_REMOVED:
  7. removeDevice(rawEvent->deviceId);
  8. break;
  9. case EventHubInterface::FINISHED_DEVICE_SCAN:
  10. handleConfigurationChanged(rawEvent->when);
  11. break;
  12. default:
  13. consumeEvent(rawEvent);
  14. break;
  15. }
  16. }

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

Copyright © Linux教程網 All Rights Reserved