# 第5章 深入理解Android輸入系統(節選)
本章主要內容:
+ 研究輸入事件從設備節點開始到窗口處理函數的流程
+ 介紹原始輸入事件的讀取與加工的原理
+ 研究事件派發機制
+ 討論事件在輸入系統與窗口之間的傳遞與反饋的過程
+ 介紹焦點窗口的選擇、ANR的產生以及以軟件方式模擬用戶操作的原理
本章涉及的源代碼文件名及位置:
+ SystemServer.java
frameworks\base\services\java\com\android\server\SystemServer.java
+ InputManagerService.java
frameworks\base\services\java\com\android\server\input/InputManagerService.java
+ WindowManagerService.java
frameworks\base\services\java\com\android\server\wm\WindowManagerService.java
+ WindowState.java
frameworks\base\services\java\com\android\server\wm\WindowState.java
+ InputMonitor.java
frameworks\base\services\java\com\android\server\wm\InputMonitor.java
+ InputEventReceiver.java
frameworks\base\core\java\android\view\InputEventReceiver.java
+ com_android_server_input_InputManagerService.cpp
frameworks\base\services\jni\com_android_server_input_InputManagerService.cpp
+ android_view_InputEventReceiver.cpp
frameworks\base\core\jni\android_view_InputEventReceiver.cpp
+ InputManager.cpp
frameworks\base\services\input\InputManager.cpp
+ EventHub.cpp
frameworks\base\services\input\EventHub.cpp
+ EventHub.h
frameworks\base\services\input\EventHub.h
+ InputDispatcher.cpp
frameworks\base\services\input\InputDispatcher.cpp
+ InputDispatcher.h
frameworks\base\services\input\InputDispatcher.h
+ InputTransport.cpp
frameworks\base\libs\androidfw\InputTransport.cpp
+ InputTransport.h
frameworks\base\include\androidfw\InputTransport.h
## 5.1 ?初識Android輸入系統
第4章通過分析WMS詳細討論了Android的窗口管理、布局及動畫的工作機制。窗口不僅是內容繪制的載體,同時也是用戶輸入事件的目標。本章將詳細討論Android輸入系統的工作原理,包括輸入設備的管理、輸入事件的加工方式以及派發流程。因此本章的探討對象有兩個:輸入設備、輸入事件。
觸摸屏與鍵盤是Android最普遍也是最標準的輸入設備。其實Android所支持的輸入設備的種類不止這兩個,鼠標、游戲手柄均在內建的支持之列。當輸入設備可用時,Linux內核會在/dev/input/下創建對應的名為event0~n或其他名稱的設備節點。而當輸入設備不可用時,則會將對應的節點刪除。在用戶空間可以通過ioctl的方式從這些設備節點中獲取其對應的輸入設備的類型、廠商、描述等信息。
當用戶操作輸入設備時,Linux內核接收到相應的硬件中斷,然后將中斷加工成原始的輸入事件數據并寫入其對應的設備節點中,在用戶空間可以通過read()函數將事件數據讀出。
Android輸入系統的工作原理概括來說,就是監控/dev/input/下的所有設備節點,當某個節點有數據可讀時,將數據讀出并進行一系列的翻譯加工,然后在所有的窗口中尋找合適的事件接收者,并派發給它。
以Nexus4為例,其/dev/input/下有evnet0~5六個輸入設備的節點。它們都是什么輸入設備呢?用戶的一次輸入操作會產生什么樣的事件數據呢?獲取答案的最簡單的辦法就是是用getevent與sendevent工具。
### 5.1.1 ?getevent與sendevent工具
Android系統提供了getevent與sendevent兩個工具供開發者從設備節點中直接讀取輸入事件或寫入輸入事件。
getevent監聽輸入設備節點的內容,當輸入事件被寫入到節點中時,getevent會將其讀出并打印在屏幕上。由于getevent不會對事件數據做任何加工,因此其輸出的內容是由內核提供的最原始的事件。其用法如下:
```
adb shell getevent [-選項] [device_path]
```
其中device_path是可選參數,用以指明需要監聽的設備節點路徑。如果省略此參數,則監聽所有設備節點的事件。
打開模擬器,執行adb shell getevent –t(-t參數表示打印事件的時間戳),并按一下電源鍵(不要松手),可以得到以下一條輸出,輸出的部分數值會因機型的不同而有所差異,但格式一致:
```
[???1262.443489] /dev/input/event0: 0001 0074 00000001
```
松開電源鍵時,又會產生以下一條輸出:
```
[???1262.557130] /dev/input/event0: 0001 0074 00000000
```
這兩條輸出便是按下和抬起電源鍵時由內核生成的原始事件。注意其輸出是十六進制的。每條數據有五項信息:產生事件時的時間戳([?? 1262.443489]),產生事件的設備節點(/dev/input/event0),事件類型(0001),事件代碼(0074)以及事件的值(00000001)。其中**時間戳、類型、代碼、值**便是原始事件的四項基本元素。除時間戳外,其他三項元素的實際意義依照設備類型及廠商的不同而有所區別。在本例中,類型0x01表示此事件為一條按鍵事件,代碼0x74表示電源鍵的掃描碼,值0x01表示按下,0x00則表示抬起。這兩條原始數據被輸入系統包裝成兩個KeyEvent對象,作為兩個按鍵事件派發給Framework中感興趣的模塊或應用程序。
**注意** 一條原始事件所包含的信息量是比較有限的。而在Android API中所使用的某些輸入事件,如觸摸屏點擊/滑動,包含了很多的信息,如XY坐標,觸摸點索引等,其實是輸入系統整合了多個原始事件后的結果。這個過程將在5.2.4節中詳細探討。
為了對原始事件有一個感性的認識,讀者可以在運行getevent的過程中嘗試一下其他的輸入操作,觀察一下每種輸入所對應的設備節點及四項元素的取值。
輸入設備的節點不僅在用戶空間可讀,而且是可寫的,因此可以將將原始事件寫入到節點中,從而實現模擬用戶輸入的功能。sendevent工具的作用正是如此。其用法如下:
```
sendevent <節點路徑> <類型><代碼> <值>
```
可以看出,sendevent的輸入參數與getevent的輸出是對應的,只不過sendevent的參數為十進制。電源鍵的代碼0x74的十進制為116,因此可以通過快速執行如下兩條命令實現點擊電源鍵的效果:
```
adb shell sendevent /dev/input/event0 1 116 1 #按下電源鍵
adb shell sendevent /dev/input/event0 1 116 0 #抬起電源鍵
```
執行完這兩條命令后,可以看到設備進入了休眠或被喚醒,與按下實際的電源鍵的效果一模一樣。另外,執行這兩條命令的時間間隔便是用戶按住電源鍵所保持的時間,所以如果執行第一條命令后遲遲不執行第二條,則會產生長按電源鍵的效果——關機對話框出現了。很有趣不是么?輸入設備節點在用戶空間可讀可寫的特性為自動化測試提供了一條高效的途徑。[1]
現在,讀者對輸入設備節點以及原始事件有了直觀的認識,接下來看一下Android輸入系統的基本原理。
### 5.1.2 ?Android輸入系統簡介
上一節講述了輸入事件的源頭是位于/dev/input/下的設備節點,而輸入系統的終點是由WMS管理的某個窗口。最初的輸入事件為內核生成的原始事件,而最終交付給窗口的則是KeyEvent或MotionEvent對象。因此Android輸入系統的主要工作是讀取設備節點中的原始事件,將其加工封裝,然后派發給一個特定的窗口以及窗口中的控件。這個過程由InputManagerService(以下簡稱IMS)系統服務為核心的多個參與者共同完成。
輸入系統的總體流程和參與者如圖5-1所示。

圖 5-1 輸入系統的總體流程與參與者
圖5-1描述了輸入事件的處理流程以及輸入系統中最基本的參與者。它們是:
+ Linux內核,接受輸入設備的中斷,并將原始事件的數據寫入到設備節點中。
+ 設備節點,作為內核與IMS的橋梁,它將原始事件的數據暴露給用戶空間,以便IMS可以從中讀取事件。
+ InputManagerService,一個Android系統服務,它分為Java層和Native層兩部分。Java層負責與WMS的通信。而Native層則是InputReader和InputDispatcher兩個輸入系統關鍵組件的運行容器。
+ EventHub,直接訪問所有的設備節點。并且正如其名字所描述的,它通過一個名為getEvents()的函數將所有輸入系統相關的待處理的底層事件返回給使用者。這些事件包括原始輸入事件、設備節點的增刪等。
+ InputReader,I是IMS中的關鍵組件之一。它運行于一個獨立的線程中,負責管理輸入設備的列表與配置,以及進行輸入事件的加工處理。它通過其線程循環不斷地通過getEvents()函數從EventHub中將事件取出并進行處理。對于設備節點的增刪事件,它會更新輸入設備列表于配置。對于原始輸入事件,InputReader對其進行翻譯、組裝、封裝為包含了更多信息、更具可讀性的輸入事件,然后交給InputDispatcher進行派發。
+ InputReaderPolicy,它為InputReader的事件加工處理提供一些策略配置,例如鍵盤布局信息等。
+ InputDispatcher,是IMS中的另一個關鍵組件。它也運行于一個獨立的線程中。InputDispatcher中保管了來自WMS的所有窗口的信息,其收到來自InputReader的輸入事件后,會在其保管的窗口中尋找合適的窗口,并將事件派發給此窗口。
+ InputDispatcherPolicy,它為InputDispatcher的派發過程提供策略控制。例如截取某些特定的輸入事件用作特殊用途,或者阻止將某些事件派發給目標窗口。一個典型的例子就是HOME鍵被InputDispatcherPolicy截取到PhoneWindowManager中進行處理,并阻止窗口收到HOME鍵按下的事件。
+ WMS,雖說不是輸入系統中的一員,但是它卻對InputDispatcher的正常工作起到了至關重要的作用。當新建窗口時,WMS為新窗口和IMS創建了事件傳遞所用的通道。另外,WMS還將所有窗口的信息,包括窗口的可點擊區域,焦點窗口等信息,實時地更新到IMS的InputDispatcher中,使得InputDispatcher可以正確地將事件派發到指定的窗口。
+ ViewRootImpl,對于某些窗口,如壁紙窗口、SurfaceView的窗口來說,窗口即是輸入事件派發的終點。而對于其他的如Activity、對話框等使用了Android控件系統的窗口來說,輸入事件的終點是控件(View)。ViewRootImpl將窗口所接收到的輸入事件沿著控件樹將事件派發給感興趣的控件。
簡單來說,內核將原始事件寫入到設備節點中,InputReader不斷地通過EventHub將原始事件取出來并翻譯加工成Android輸入事件,然后交給InputDispatcher。InputDispatcher根據WMS提供的窗口信息將事件交給合適的窗口。窗口的ViewRootImpl對象再沿著控件樹將事件派發給感興趣的控件。控件對其收到的事件作出響應,更新自己的畫面、執行特定的動作。所有這些參與者以IMS為核心,構建了Android龐大而復雜的輸入體系。
Linux內核對硬件中斷的處理超出了本書的討論范圍,因此本章將以IMS為重點,詳細討論除Linux內核以外的其他參與者的工作原理。
### 5.1.3 ?IMS的構成
同以往一樣,本節通過IMS的啟動過程,探討IMS的構成。上一節提到,IMS分為Java層與Native層兩個部分,其啟動過程是從Java部分的初始化開始,進而完成Native部分的初始化。
#### 1. IMS的誕生
同其他系統服務一樣,IMS在SystemServer中的ServerThread線程中啟動。
```
[SystemServer.java-->ServerThread.run()]
public void run() {
??? ......
???InputManagerService inputManager = null;
??? ......
??? // **① 新建IMS對象。**注意第二個參數wmHandler,這說明IMS的一部分功能可能會在WMS的線程中完成
??? inputManager= new InputManagerService(context, wmHandler);
??? // 將IMS發布給ServiceManager,以便其他人可以訪問IMS提供的接口
??? ServiceManager.addService(Context.INPUT_SERVICE,inputManager);
??? // 設置向WMS發起回調的callback對象
??? inputManager.setWindowManagerCallbacks(wm.getInputMonitor());
??? // **② 正式啟動IMS**
???inputManager.start();
??? ......
??? /* 設置IMS給DisplayManagerService。DisplayManagerService將會把屏幕的信息發送給輸入
?????? 系統作為事件加工的依據。在5.2.4節將會討論到這些信息的作用 */
???display.setInputManager(inputManager);
}
```
IMS的誕生分為兩個階段:
+ 創建新的IMS對象。
+ 調用IMS對象的start()函數完成啟動。
##### (1)? IMS的創建
IMS的構造函數如下:
```
[InputManagerService.java-->InputManagerService.InputManagerService()]
public InputManagerService(Context context,Handler handler) {
??? /* 使用wmHandler的Looper新建一個InputManagerHandler。InputManagerHandler將運行在
?????? WMS的主線程中*/
???this.mHandler = new InputManagerHandler(handler.getLooper());
??? ......
??? // 每一個分為Java和Native兩部分的對象在創建時都會有一個nativeInput函數
??? mPtr =nativeInit(this, mContext, mHandler.getLooper().getQueue());
}
```
可以看出,IMS的構造函數非常簡單。看來絕大部分的初始化工作都位于Native層。參考nativeInit()函數的實現。
```
[com_android_server_input_InputManagerService.cpp-->nativeInit()]
static jint nativeInit(JNIEnv* env, jclass clazz,
???????jobject serviceObj, jobject contextObj, jobject messageQueueObj) {
???sp<MessageQueue> messageQueue =android_os_MessageQueue_getMessageQueue(env, messageQueueObj);
??? /* 新建了一個NativeInputManager對象,NativeInputManager,此對象將是Native層組件與
????? Java層IMS進行通信的橋梁 */
???NativeInputManager* im = new NativeInputManager(contextObj, serviceObj,
???????????messageQueue->getLooper());
???im->incStrong(serviceObj);
??? // 返回了NativeInputManager對象的指針給Java層的IMS,IMS將其保存在mPtr成員變量中
??? returnreinterpret_cast<jint>(im);
}
```
nativeInit()函數創建了一個類型為NativeInputManager的對象,它是Java層與Native層互相通信的橋梁。
看下這個類的聲明可以發現,它實現了InputReaderPolicyInterface與InputDispatcherPolicyInterface兩個接口。這說明上一節曾經介紹過的兩個重要的輸入系統參與者InputReaderPolicy和InputDispatcherPolicy是由NativeInputManager實現的,然而它僅僅為兩個策略提供接口實現而已,并不是策略的實際實現者。NativeInputManager通過JNI回調Java層的IMS,由它完成決策。這一小節暫不討論其實現細節,讀者只要先記住兩個策略參與者的接口實現位于NativeInputManager即可。
接下來看一下NativeInputManager的創建:
```
[com_android_server_input_InputManagerService.cpp
-->NativeInputManager::NativeInputManager()]
NativeInputManager::NativeInputManager(jobjectcontextObj,
???????jobject serviceObj, const sp<Looper>& looper) :
???????mLooper(looper) {
??? ......
??? // 出現重點了, NativeInputManager創建了EventHub
???sp<EventHub> eventHub = new EventHub();
??? // 接著創建了Native層的InputManager
???mInputManager = new InputManager(eventHub, this, this);
}
```
在NativeInputManager的構造函數中,創建了兩個關鍵人物,分別是EventHub與InputManager。EventHub復雜的構造函數使其在創建后便擁有了監聽設備節點的能力,這一小節中暫不討論它的構造函數,讀者僅需知道EventHub在這里初始化即可。緊接著便是InputManager的創建了,看一下其構造函數:
```
[InputManager.cpp-->InputManager::InputManager()]
InputManager::InputManager(
???????const sp<EventHubInterface>& eventHub,
???????const sp<InputReaderPolicyInterface>& readerPolicy,
???????const sp<InputDispatcherPolicyInterface>& dispatcherPolicy) {
??? // 創建InputDispatcher
???mDispatcher = new InputDispatcher(dispatcherPolicy);
??? // 創建 InputReader
??? mReader= new InputReader(eventHub, readerPolicy, mDispatcher);
??? // 初始化
???initialize();
}
```
再看initialize()函數:
```
[InputManager.cpp-->InputManager::initialize()]
void InputManager::initialize() {
??? // 創建供InputReader運行的線程InputReaderThread
???mReaderThread = new InputReaderThread(mReader);
??? // 創建供InputDispatcher運行的線程InputDispatcherThread
???mDispatcherThread = new InputDispatcherThread(mDispatcher);
}
```
InputManager的構造函數也比較簡潔,它創建了四個對象,分別為IMS的核心參與者InputReader與InputDispatcher,以及它們所在的線程InputReaderThread與InputDispatcherThread。注意InputManager的構造函數的參數readerPolicy與dispatcherPolicy,它們都是NativeInputManager。
至此,IMS的創建完成了。在這個過程中,輸入系統的重要參與者均完成創建,并得到了如圖5-2所描述的一套體系。

圖 5-2 IMS的結構體系
##### (2)? IMS的啟動與運行
完成IMS的創建之后,ServerThread執行了InputManagerService.start()函數以啟動IMS。InputManager的創建過程分別為InputReader與InputDispatcher創建了承載它們運行的線程,然而并未將這兩個線程啟動,因此IMS的各員大將仍處于待命狀態。此時start()函數的功能就是啟動這兩個線程,使得InputReader于InputDispatcher開始工作。
當兩個線程啟動后,InputReader在其線程循環中不斷地從EventHub中抽取原始輸入事件,進行加工處理后將加工所得的事件放入InputDispatcher的派發發隊列中。InputDispatcher則在其線程循環中將派發隊列中的事件取出,查找合適的窗口,將事件寫入到窗口的事件接收管道中。窗口事件接收線程的Looper從管道中將事件取出,交由事件處理函數進行事件響應。整個過程共有三個線程首尾相接,像三臺水泵似的一層層地將事件交付給事件處理函數。如圖5-3所示。

圖 5-3 三個線程,三臺水泵
InputManagerService.start()函數的作用,就像為Reader線程、Dispatcher線程這兩臺水泵按下開關,而Looper這臺水泵在窗口創建時便已經處于運行狀態了。自此,輸入系統動力十足地開始運轉,設備節點中的輸入事件將被源源不斷地抽取給事件處理者。本章的主要內容便是討論這三臺水泵的工作原理。
#### 2. IMS的成員關系
根據對IMS的創建過程的分析,可以得到IMS的成員關系如圖5-4所示,這幅圖省略了一些非關鍵的引用與繼承關系。
**注意** IMS內部做了很多的抽象工作,EventHub、nputReader以及InputDispatcher等實際上都繼承自相應的名為XXXInterface的接口,并且僅通過接口進行相互之間的引用。鑒于這些接口各自僅有唯一的實現,為了簡化敘述我們將不提及這些接口,但是讀者在實際學習與研究時需要注意這一點。

圖 5-4 IMS的成員關系
在圖5-4中,左側部分為Reader子系統對應于圖5-3中的第一臺水泵,右側部分為Dispatcher子系統,對應于圖5-3中的第二臺水泵。了解了IMS的成員關系后便可以開始我們的IMS深入理解之旅了!
## 5.2 ?原始事件的讀取與加工
本節將深入探討第一臺水泵——Reader子系統的工作原理。Reader子系統的輸入端是設備節點,輸出端是Dispatcher子系統的派發隊列。從設備節點到派發隊列之間的過程發生了什么呢?本章一開始曾經介紹過,一個設備節點對應了一個輸入設備,并且其中存儲了內核寫入的原始事件。因此設備節點擁有兩個概念:設備與原始事件。因此Reader子系統需要處理輸入設備以及原始事件兩種類型的對象。
設備節點的新建與刪除表示了輸入設備的可用與無效,Reader子系統需要加載或刪除對應設備的配置信息;而設備節點中是否有內容可讀表示了是否有新的原始事件到來,有新的原始事件到來時Reader子系統需要開始對新事件進行加工并放置到派發隊列中。問題是應該如何監控設備節點的新建與刪除動作以及如何確定節點中有內容可讀呢?最簡單的辦法是在線程循環中不斷地輪詢,然而這會導致非常低下的效率,更會導致電量在無謂地輪詢中消耗。Android使用由Linux提供的兩套機制INotify與Epoll優雅地解決了這兩個問題。在正式探討Reader子系統的工作原理之前,需要首先了解這兩套機制的使用方法。
### 5.2.1 ?基礎知識:INotify與Epoll
#### 1.INotify介紹與使用
INotify是一個Linux內核所提供的一種文件系統變化通知機制。它可以為應用程序監控文件系統的變化,如文件的新建、刪除、讀寫等。INotify機制有兩個基本對象,分別為inotify對象與watch對象,都使用文件描述符表示。
inotify對象對應了一個隊列,應用程序可以向inotify對象添加多個監聽。當被監聽的事件發生時,可以通過read()函數從inotify對象中將事件信息讀取出來。Inotify對象可以通過以下方式創建:
```
int inotifyFd = inotify_init();
```
而watch對象則用來描述文件系統的變化事件的監聽。它是一個二元組,包括監聽目標和事件掩碼兩個元素。監聽目標是文件系統的一個路徑,可以是文件也可以是文件夾。而事件掩碼則表示了需要需要監聽的事件類型,掩碼中的每一位代表一種事件。可以監聽的事件種類很多,其中就包括文件的創建(IN_CREATE)與刪除(IN_DELETE)。讀者可以參閱相關資料以了解其他可監聽的事件種類。以下代碼即可將一個用于監聽輸入設備節點的創建與刪除的watch對象添加到inotify對象中:
```
int wd = inotify_add_watch (inotifyFd, “/dev/input”,IN_CREATE | IN_DELETE);
```
完成上述watch對象的添加后,當/dev/input/下的設備節點發生創建與刪除操作時,都會將相應的事件信息寫入到inotifyFd所描述的inotify對象中,此時可以通過read()函數從inotifyFd描述符中將事件信息讀取出來。
事件信息使用結構體inotify_event進行描述:
```
struct inotify_event {
???????__s32?????????? wd;???????????? /* 事件對應的Watch對象的描述符 */
???????__u32??????? ???mask;?????????? /* 事件類型,例如文件被刪除,此處值為IN_DELETE */
???????__u32?????????? cookie;
???????__u32?????????? len;??????????? /* name字段的長度 */
???????char??????????? name[0];??????? /* 可變長的字段,用于存儲產生此事件的文件路徑*/
};
```
當沒有監聽事件發生時,可以通過如下方式將一個或多個未讀取的事件信息讀取出來:
```
size_t len = read (inotifyFd, events_buf,BUF_LEN);
```
其中events_buf是inotify_event的數組指針,能夠讀取的事件數量由取決于數組的長度。成功讀取事件信息后,便可根據inotify_event結構體的字段判斷事件類型以及產生事件的文件路徑了。
總結一下INotify機制的使用過程:
+ 通過inotify_init()創建一個inotify對象。
+ 通過inotify_add_watch將一個或多個監聽添加到inotify對象中。
+ 通過read()函數從inotify對象中讀取監聽事件。當沒有新事件發生時,inotify對象中無任何可讀數據。
通過INotify機制避免了輪詢文件系統的麻煩,但是還有一個問題,INotify機制并不是通過回調的方式通知事件,而需要使用者主動從inotify對象中進行事件讀取。那么何時才是讀取的最佳時機呢?這就需要借助Linux的另一個優秀的機制Epoll了。
#### 2.Epoll介紹與使用
無論是從設備節點中獲取原始輸入事件還是從inotify對象中讀取文件系統事件,都面臨一個問題,就是這些事件都是偶發的。也就是說,大部分情況下設備節點、inotify對象這些文件描述符中都是無數據可讀的,同時又希望有事件到來時可以盡快地對事件作出反應。為解決這個問題,我們不希望不斷地輪詢這些描述符,也不希望為每個描述符創建一個單獨的線程進行阻塞時的讀取,因為這都將會導致資源的極大浪費。
此時最佳的辦法是使用Epoll機制。Epoll可以使用一次等待監聽多個描述符的可讀/可寫狀態。等待返回時攜帶了可讀的描述符或自定義的數據,使用者可以據此讀取所需的數據后可以再次進入等待。因此不需要為每個描述符創建獨立的線程進行阻塞讀取,避免了資源浪費的同時又可以獲得較快的響應速度。
Epoll機制的接口只有三個函數,十分簡單。
+ epoll_create(int max_fds):創建一個epoll對象的描述符,之后對epoll的操作均使用這個描述符完成。max_fds參數表示了此epoll對象可以監聽的描述符的最大數量。
+ epoll_ctl (int epfd, int op,int fd, struct epoll_event *event):用于管理注冊事件的函數。這個函數可以增加/刪除/修改事件的注冊。
+ int epoll_wait(int epfd, structepoll_event * events, int maxevents, int timeout):用于等待事件的到來。當此函數返回時,events數組參數中將會包含產生事件的文件描述符。
接下來以監控若干描述符可讀事件為例介紹一下epoll的用法。
(1) 創建epoll對象
首先通過epoll_create()函數創建一個epoll對象:
```
Int epfd = epoll_create(MAX_FDS)
```
(2) 填充epoll_event結構體
接著為每一個需監控的描述符填充epoll_event結構體,以描述監控事件,并通過epoll_ctl()函數將此描述符與epoll_event結構體注冊進epoll對象。epoll_event結構體的定義如下:
```
struct epoll_event {
??? __uint32_tevents; /* 事件掩碼,指明了需要監聽的事件種類*/
???epoll_data_t data; /* 使用者自定義的數據,當此事件發生時該數據將原封不動地返回給使用者 */
};
```
epoll_data_t聯合體的定義如下,當然,同一時間使用者只能使用一個字段:
```
typedef union epoll_data {
??? void*ptr;
??? int fd;
???__uint32_t u32;
???__uint64_t u64;
} epoll_data_t;
```
epoll_event結構中的events字段是一個事件掩碼,用以指明需要監聽的事件種類,同INotify一樣,掩碼的每一位代表了一種事件。常用的事件有EPOLLIN(可讀),EPOLLOUT(可寫),EPOLLERR(描述符發生錯誤),EPOLLHUP(描述符被掛起)等。更多支持的事件讀者可參考相關資料。
data字段是一個聯合體,它讓使用者可以將一些自定義數據加入到事件通知中,當此事件發生時,用戶設置的data字段將會返回給使用者。在實際使用中常設置epoll_event.data.fd為需要監聽的文件描述符,事件發生時便可以根據epoll_event.data.fd得知引發事件的描述符。當然也可以設置epoll_event.data.fd為其他便于識別的數據。
填充epoll_event的方法如下:
```
??? structepoll_event eventItem;
???memset(&eventItem, 0, sizeof(eventItem));
???eventItem.events = EPOLLIN | EPOLLERR | EPOLLHUP; // 監聽描述符可讀以及出錯的事件
??? eventItem.data.fd= listeningFd; // 填寫自定義數據為需要監聽的描述符
```
接下來就可以使用epoll_ctl()將事件注冊進epoll對象了。epoll_ctl()的參數有四個:
+ epfd是由epoll_create()函數所創建的epoll對象的描述符。
+ op表示了何種操作,包括EPOLL_CTL_ADD/DEL/MOD三種,分別表示增加/刪除/修改注冊事件。
+ fd表示了需要監聽的描述符。
+ event參數是描述了監聽事件的詳細信息的epoll_event結構體。
注冊方法如下:
```
??? // 將事件監聽添加到epoll對象中去
??? result =epoll_ctl(epfd, EPOLL_CTL_ADD, listeningFd, &eventItem);
```
重復這個步驟可以將多個文件描述符的多種事件監聽注冊到epoll對象中。完成了監聽的注冊之后,便可以通過epoll_wait()函數等待事件的到來了。
(3) 使用epoll_wait()函數等待事件
epoll_wait()函數將會使調用者陷入等待狀態,直到其注冊的事件之一發生之后才會返回,并且攜帶了剛剛發生的事件的詳細信息。其簽名如下:
```
int epoll_wait(int epfd, struct epoll_event *events, int maxevents, int timeout);
```
+ epfd是由epoll_create()函數所創建的epoll對象描述符。
+ events是一個epoll_event的數組,此函數返回時,事件的信息將被填充至此。
+ maxevents表示此次調用最多可以獲取多少個事件,當然,events參數必須能夠足夠容納這么多事件。
+ timeout表示等待超時的事件。
epoll_wait()函數返回值表示獲取了多少個事件。
4\. 處理事件
epoll_wait返回后,便可以根據events數組中所保存的所有epoll_event結構體的events字段與data字段識別事件的類型與來源。
Epoll的使用步驟總結如下:
+ 通過epoll_create()創建一個epoll對象。
+ 為需要監聽的描述符填充epoll_events結構體,并使用epoll_ctl()注冊到epoll對象中。
+ 使用epoll_wait()等待事件的發生。
+ 根據epoll_wait()返回的epoll_events結構體數組判斷事件的類型與來源并進行處理。
+ 繼續使用epoll_wait()等待新事件的發生。
#### 3.INotify與Epoll的小結
INotify與Epoll這兩套由Linux提供的事件監聽機制以最小的開銷解決了文件系統變化以及文件描述符可讀可寫狀態變化的監聽問題。它們是Reader子系統運行的基石,了解了這兩個機制的使用方法之后便為對Reader子系統的分析學習鋪平了道路。
### 5.2.2 ?InputReader的總體流程
在了解了INotify與Epoll的基礎知識之后便可以正是開始分析Reader子系統的工作原理了。首先要理解InputReader的運行方式。在5.1.3節介紹了InputReader被InputManager創建,并運行于InputReaderThread線程中。InputReader如何在InputReaderThread中運行呢?
InputReaderThread繼承自C++的Thread類,Thread類封裝了pthread線程工具,提供了與Java層Thread類相似的API。C++的Thread類提供了一個名為threadLoop()的純虛函數,當線程開始運行后,將會在內建的線程循環中不斷地調用threadLoop(),直到此函數返回false,則退出線程循環,從而結束線程。
InputReaderThread僅僅重寫了threadLoop()函數:
```
[InputReader.cpp-->InputReaderThread::threadLoop()]
bool InputReaderThread::threadLoop() {
???mReader->loopOnce(); // 執行InputReader的loopOnce()函數
??? returntrue;
}
```
InputReaderThread啟動后,其線程循環將不斷地執行InputReader.loopOnce()函數。因此這個loopOnce()函數作為線程循環的循環體包含了InputReader的所有工作。
**注意** C++層的Thread類與Java層的Thread類有著一個顯著的不同。C++層Thread類內建了線程循環,threadLoop()就是一次循環而已,只要返回值為true,threadLoop()將會不斷地被內建的循環調用。這也是InputReader.loopOnce()函數名稱的由來。而Java層Thread類的run()函數則是整個線程的全部,一旦其退出,線程也便完結。
接下來看一下InputReader.loopOnce()的代碼,分析一下InputReader在一次線程循環中做了什么。
```
[InputReader.cpp-->InputReader::loopOnce()]
void InputReader::loopOnce() {
??? ......
??? /* **① 通過EventHub抽取事件列表**。讀取的結果存儲在參數mEventBuffer中,返回值表示事件的個數
?????? 當EventHub中無事件可以抽取時,此函數的調用將會阻塞直到事件到來或者超時 */
??? size_tcount = mEventHub->getEvents(timeoutMillis
??????????????????????????????????????????? ,mEventBuffer, EVENT_BUFFER_SIZE);
??? {
???????AutoMutex _l(mLock);
??????? ......
??????? if(count) {
???????????// **② 如果有抽得事件,則調用processEventsLocked()函數對事件進行加工處理**
???????????processEventsLocked(mEventBuffer, count);
??????? }
??????? ......
??? }
??? ......
??? /* **③ 發布事件。** processEventsLocked()函數在對事件進行加工處理之后,便將處理后的事件存儲在
??????mQueuedListener中。在循環的最后,通過調用flush()函數將所有事件交付給InputDispatcher */
???mQueuedListener->flush();
}
```
InputReader的一次線程循環的工作思路非常清晰,一共三步:
+ 首先從EventHub中抽取未處理的事件列表。這些事件分為兩類,一類是從設備節點中讀取的原始輸入事件,另一類則是輸入設備可用性變化事件,簡稱為設備事件。
+ 通過processEventsLocked()對事件進行處理。對于設備事件,此函數對根據設備的可用性加載或移除設備對應的配置信息。對于原始輸入事件,則在進行轉譯、封裝與加工后將結果暫存到mQueuedListener中。
+ 所有事件處理完畢后,調用mQueuedListener.flush()將所有暫存的輸入事件一次性地交付給InputDispatcher。
這便是InputReader的總體工作流程。而我們接下來將詳細討論這三步的實現。
### 5.2.3 ?深入理解EventHub
InputReader在其線程循環中的第一個工作便是從EventHub中讀取一批未處理的事件。EventHub是如何工作的呢?
EventHub的直譯是事件集線器,顧名思義,它將所有的輸入事件通過一個接口getEvents()將從多個輸入設備節點中讀取的事件交給InputReader,是輸入系統最底層的一個組件。它是如何工作呢?沒錯,正是基于前文所述的INotify與Epoll兩套機制。
#### 1.設備節點監聽的建立
在EventHub的構造函數中,它通過INotify與Epoll機制建立起了對設備節點增刪事件以及可讀狀態的監聽。在繼續之前,請讀者先回憶一下INotify與Epoll的使用方法。
EventHub的構造函數如下:
```
[EventHub.cpp-->EventHub::EventHub()]
EventHub::EventHub(void) :
???????mBuiltInKeyboardId(NO_BUILT_IN_KEYBOARD), mNextDeviceId(1),
???????mOpeningDevices(0), mClosingDevices(0),
???????mNeedToSendFinishedDeviceScan(false),
???????mNeedToReopenDevices(false), mNeedToScanDevices(true),
???????mPendingEventCount(0), mPendingEventIndex(0), mPendingINotify(false) {
??? /* **① 首先使用epoll_create()函數創建一個epoll對象**。EPOLL_SIZE_HINT指定最大監聽個數為8
?????? 這個epoll對象將用來監聽設備節點是否有數據可讀(有無事件) */
??? mEpollFd= epoll_create(EPOLL_SIZE_HINT);
??? // **② 創建一個inotify對象**。這個inotify對象將被用來監聽設備節點的增刪事件
???mINotifyFd = inotify_init();
??? /* 將存儲設備節點的路徑/dev/input作為監聽對象添加到inotify對象中。當此文件夾下的設備節點
?????? 發生創建與刪除事件時,都可以通過mINotifyFd讀取事件的詳細信息 */
??? intresult = inotify_add_watch(mINotifyFd, DEVICE_PATH, IN_DELETE | IN_CREATE);
??? /* **③ 接下來將mINotifyFd作為epoll的一個監控對象**。當inotify事件到來時,epoll_wait()將
?????? 立刻返回,EventHub便可從mINotifyFd中讀取設備節點的增刪信息,并作相應處理 */
??? structepoll_event eventItem;
???memset(&eventItem, 0, sizeof(eventItem));
???eventItem.events = EPOLLIN; // 監聽mINotifyFd可讀
??? // 注意這里并沒有使用fd字段,而使用了自定義的值EPOLL_ID_INOTIFY
???eventItem.data.u32 = EPOLL_ID_INOTIFY;
??? // 將對mINotifyFd的監聽注冊到epoll對象中
??? result =epoll_ctl(mEpollFd, EPOLL_CTL_ADD, mINotifyFd, &eventItem);
??? /* 在構造函數剩余的代碼中,EventHub創建了一個名為wakeFds的匿名管道,并將管道讀取端的描述符
?????? 的可讀事件注冊到epoll對象中。因為InputReader在執行getEvents()時會因無事件而導致其線程
?????? 阻塞在epoll_wait()的調用里,然而有時希望能夠立刻喚醒InputReader線程使其處理一些請求。此
?????? 時只需向wakeFds管道的寫入端寫入任意數據,此時讀取端有數據可讀,使得epoll_wait()得以返回,
?????? 從而達到喚醒InputReader線程的目的*/
??? ......
}
```
EventHub的構造函數初識化了Epoll對象和INotify對象,分別監聽原始輸入事件與設備節點增刪事件。同時將INotify對象的可讀性事件也注冊到Epoll中,因此EventHub可以像處理原始輸入事件一樣監聽設備節點增刪事件了。
構造函數同時也揭示了EventHub的監聽工作分為設備節點和原始輸入事件兩個方面,接下來將深入探討這兩方面的內容。
#### 2.getEvents()函數的工作方式
正如前文所述,InputReaderThread的線程循環為Reader子系統提供了運轉的動力,EventHub的工作也是由它驅動的。InputReader::loopOnce()函數調用EventHub::getEvents()函數獲取事件列表,所以這個getEvents()是EventHub運行的動力所在,幾乎包含了EventHub的所有工作內容,因此首先要將getEvents()函數的工作方式搞清楚。
getEvents()函數的簽名如下:
size_t EventHub::getEvents(int timeoutMillis,RawEvent* buffer, size_t bufferSize)
此函數將盡可能多地讀取設備增刪事件與原始輸入事件,將它們封裝為RawEvent結構體,并放入buffer中供InputReader進行處理。RawEvent結構體的定義如下:
```
[EventHub.cpp-->RawEvent]
struct RawEvent {
??? nsecs_twhen;???????????? /* 發生事件時的時間戳 */
??? int32_tdeviceId;??????? /* 產生事件的設備Id,它是由EventHub自行分配的,InputReader
??????????????????????????????????? 以根據它從EventHub中獲取此設備的詳細信息 */
??? int32_ttype;???????????? /* 事件的類型 */
??? int32_tcode;???????????? /* 事件代碼 */
??? int32_tvalue;??????????? /* 事件值 */
};
```
可以看出,RawEvent結構體與getevent工具的輸出十分一致,包含了原始輸入事件的四個基本元素,因此用RawEvent結構體表示原始輸入事件是非常直觀的。RawEvent同時也用來表示設備增刪事件,為此,EventHub定義了三個特殊的事件類型DEVICE_ADD、DEVICE_REMOVED以及FINISHED_DEVICE_SCAN,用以與原始輸入事件進行區別。
由于getEvents()函數較為復雜,為了給后續分析鋪平道路,本節不討論其細節,先通過偽代碼理解此函數的結構與工作方式,在后續深入分析時思路才會比較清晰。
getEvents()函數的本質就是讀取并處理Epoll事件與INotify事件。參考以下代碼:
```
[EventHub.cpp-->EventHub::getEvents()]
size_t EventHub::getEvents(int timeoutMillis,RawEvent* buffer, size_t bufferSize) {
??? /* event指針指向了在buffer下一個可用于存儲事件的RawEvent結構體。每存儲一個事件,
?????? event指針都回向后偏移一個元素 */
???RawEvent* event = buffer;
??? /*capacity記錄了buffer中剩余的元素數量。當capacity為0時,表示buffer已滿,此時需要停
?????? 繼續處理新事件,并將已處理的事件返回給調用者 */
??? size_tcapacity = bufferSize;
??? /* 接下來的循環是getEvents()函數的主體。在這個循環中,會先將可用事件放入到buffer中并返回。
?????? 如果沒有可用事件,則進入epoll_wait()等待事件的到來,epoll_wait()返回后會重新循環將可用
?????? 將新事件放入buffer */
??? for (;;){
???????nsecs_t now = systemTime(SYSTEM_TIME_MONOTONIC);
??????? /* **① 首先進行與設備相關的工作。**某些情況下,如EventHub創建后第一次執行getEvents()函數
?????????? 時,需要掃描/dev/input文件夾下的所有設備節點并將這些設備打開。另外,當設備節點的發生增
?????????? 動作生時,會將設備事件存入到buffer中 */
??????? ......
??????? /* **② 處理未被InputReader取走的輸入事件與設備事件。**epoll_wait()所取出的epoll_event
?????????? 存儲在mPendingEventItems中,mPendingEventCount指定了mPendingEventItems數組
?????????? 所存儲的事件個數。而mPendingEventIndex指定尚未處理的epoll_event的索引 */
???????while (mPendingEventIndex < mPendingEventCount) {
???????????const struct epoll_event& eventItem =
???????????????????????????????? mPendingEventItems[mPendingEventIndex++];
???????????/* 在這里分析每一個epoll_event,如果是表示設備節點可讀,則讀取原始事件并放置到buffer
??????????????中。如果是表示mINotifyFd可讀,則設置mPendingINotify為true,當InputReader
??????????????將現有的輸入事件都取出后讀取mINotifyFd中的事件,并進行相應的設備加載與卸載操作。
?????????????? 另外,如果此epoll_event表示wakeFds的讀取端有數據可讀,則設置awake標志為true,
??????????????無論此次getEvents()調用有無取到事件,都不會再次進行epoll_wait()進行事件等待 */
???????????......
??????? }
??????? // ③ 如果mINotifyFd有數據可讀,說明設備節點發生了增刪操作
??????? if(mPendingINotify && mPendingEventIndex >= mPendingEventCount) {
???????????/* 讀取mINotifyFd中的事件,同時對輸入設備進行相應的加載與卸載操作。這個操作必須當
??????????????InputReader將現有輸入事件讀取并處理完畢后才能進行,因為現有的輸入事件可能來自需要
??????????????被卸載的輸入設備,InputReader處理這些事件依賴于對應的設備信息 */
??????????? ......
??????????? deviceChanged= true;
??????? }
??????? // 設備節點增刪操作發生時,則重新執行循環體,以便將設備變化的事件放入buffer中
??????? if(deviceChanged) {
???????????continue;
??????? }
??????? // 如果此次getEvents()調用成功獲取了一些事件,或者要求喚醒InputReader,則退出循環并
??????? // 結束getEvents()的調用,使InputReader可以立刻對事件進行處理
??????? if(event != buffer || awoken) {
???????????break;
??????? }
??????? /* ④ 如果此次getEvents()調用沒能獲取事件,說明mPendingEventItems中沒有事件可用,
?????????? 于是執行epoll_wait()函數等待新的事件到來,將結果存儲到mPendingEventItems里,并重
????????? ?置mPendingEventIndex為0 */
???????mPendingEventIndex = 0;
???????......
??????? intpollResult = epoll_wait(mEpollFd, mPendingEventItems, EPOLL_MAX_EVENTS,timeoutMillis);
???????......
??????? mPendingEventCount= size_t(pollResult);
??????? // 從epoll_wait()中得到新的事件后,重新循環,對新事件進行處理
??? }
??? // 返回本次getEvents()調用所讀取的事件數量
??? returnevent - buffer;
}
```
getEvents()函數使用Epoll的核心是mPendingEventItems數組,它是一個事件池。getEvents()函數會優先從這個事件池獲取epoll事件進行處理,并將讀取相應的原始輸入事件返回給調用者。當因為事件池枯竭而導致調用者無法獲得任何事件時,會調用epoll_wait()函數等待新事件的到來,將事件池重新注滿,然后再重新處理事件池中的Epoll事件。從這個意義來說,getEvents()函數的調用過程,就是消費epoll_wait()所產生的Epoll事件的過程。因此可以將從epoll_wait()的調用開始,到將Epoll事件消費完畢的過程稱為EventHub的一個監聽周期。依據每次epoll_wait()產生的Epoll事件的數量以及設備節點中原始輸入事件的數量,一個監聽周期包含一次或多次getEvents()調用。周期中的第一次調用會因為事件池枯竭而直接進入epoll_wait(),而周期中的最后一次調用一定會將最后的事件取走。
**注意** getEvents()采用事件池機制的根本原因是buffer的容量限制。由于一次epoll_wait()可能返回多個設備節點的可讀事件,每個設備節點又有可能讀取多條原始輸入事件,一段時間內原始輸入事件的數量可能大于buffer的容量。因此需要一個事件池以緩存因buffer容量不夠而無法處理的epoll事件,以便在下次調用時可以將這些事件優先處理。這是緩沖區操作的一個常用技巧。
當有INotify事件可以從mINotifyFd中讀取時,會產生一個epoll事件,EventHub便得知設備節點發生了增刪操作。在getEvents()將事件池中的所有事件處理完畢后,便會從mINotifyFd中讀取INotify事件,進行輸入設備的加載/卸載操作,然后生成對應的RawEvent結構體并返回給調用者。
通過上述分析可以看到,getEvents()包含了原始輸入事件讀取、輸入設備加載/卸載等操作。這幾乎是EventHub的全部工作了。如果沒有geEvents()的調用,EventHub將對輸入事件、設備節點增刪事件置若罔聞,因此可以將一次getEvents()調用理解為一次心跳,EventHub的核心功能都會在這次心跳中完成。
getEvents()的代碼還揭示了另外一個信息:在一個監聽周期內的設備增刪事件與Epoll事件的優先級。設備事件的生成邏輯位于Epoll事件的處理之前,因此getEvents()將優先生成設備增刪事件,完成所有設備增刪事件的生成之前不會處理Epoll事件,也就是不會生成原始輸入事件。
接下來我們將從設備管理與原始輸入事件處理兩個方面深入探討EventHub。
#### 3.輸入設備管理
因為輸入設備是輸入事件的來源,并且決定了輸入事件的含義,因此首先討論EventHub的輸入設備管理機制。
輸入設備是一個可以為接收用戶操作的硬件,內核會為每一個輸入設備在/dev/input/下創建一個設備節點,而當輸入設備不可用時(例如被拔出),將其設備節點刪除。這個設備節點包含了輸入設備的所有信息,包括名稱、廠商、設備類型,設備的功能等。除了設備節點,某些輸入設備還包含一些自定義配置,這些配置以鍵值對的形式存儲在某個文件中。這些信息決定了Reader子系統如何加工原始輸入事件。EventHub負責在設備節點可用時加載并維護這些信息,并在設備節點被刪除時將其移除。
EventHub通過一個定義在其內部的名為Device的私有結構體來描述一個輸入設備。其定義如下:
```
[EventHub.h-->EventHub::Device]
struct Device {
??? Device*next;? /* Device結構體實際上是一個單鏈表 */
??? int fd;???????? /* fd表示此設備的設備節點的描述符,可以從此描述符中讀取原始輸入事件 */
??? constint32_t id;??? ?/* id在輸入系統中唯一標識這個設備,由EventHub在加載設備時進行分配 */
??? constString8 path; /* path存儲了設備節點在文件系統中的路徑 */
??? constInputDeviceIdentifier identifier; /* 廠商信息,存儲了設備的供應商、型號等信息
???????????????????????????????????????????????????????這些信息從設備節點中獲得 */
??? uint32_tclasses;? /* classes表示了設備的類別,鍵盤設備,觸控設備等。一個設備可以同時屬于
????????????????????????????? 多個設備類別。類別決定了InputReader如何加工其原始輸入事件 */
??? /* 接下來是一系列的事件位掩碼,它們詳細地描述了設備能夠產生的事件類型。設備能夠產生的事件類型
?????? 決定了此設備所屬的類型*/
??? uint8_tkeyBitmask[(KEY_MAX + 1) / 8];
??? ......
??? /* 配置信息。以鍵值對的形式存儲在一個文件中,其路徑取決于identfier字段中的廠商信息,這些
?????? 配置信息將會影響InputReader對此設備的事件的加工行為 */
??? String8configurationFile;
???PropertyMap* configuration;
??? /* 鍵盤映射表。對于鍵盤類型的設備,這些鍵盤映射表將原始事件中的鍵盤掃描碼轉換為Android定義的
????? 的按鍵值。這個映射表也是從一個文件中加載的,文件路徑取決于dentifier字段中的廠商信息 */
???VirtualKeyMap* virtualKeyMap;
??? KeyMapkeyMap;
???sp<KeyCharacterMap> overlayKeyMap;
???sp<KeyCharacterMap> combinedKeyMap;
??? // 力反饋相關的信息。有些設備如高級的游戲手柄支持力反饋功能,目前暫不考慮
??? boolffEffectPlaying;
??? int16_tffEffectId;
};
```
Device結構體所存儲的信息主要包括以下幾個方面:
+ 設備節點信息:保存了輸入設備節點的文件描述符、文件路徑等。
+ 廠商信息:包括供應商、設備型號、名稱等信息,這些信息決定了加載配置文件與鍵盤映射表的路徑。
+ 設備特性信息:包括設備的類別,可以上報的事件種類等。這些特性信息直接影響了InputReader對其所產生的事件的加工處理方式。
+ 設備的配置信息:包括鍵盤映射表及其他自定義的信息,從特定位置的配置文件中讀取。
另外,Device結構體還存儲了力反饋所需的一些數據。在本節中暫不討論。
EventHub用一個名為mDevices的字典保存當前處于打開狀態的設備節點的Device結構體。字典的鍵為設備Id。
##### (1)輸入設備的加載
EventHub在創建后在第一次調用getEvents()函數時完成對系統中現有輸入設備的加載。
再看一下getEvents()函數中相關內容的實現:
```
[EventHub.cpp-->EventHub::getEvents()]
size_t EventHub::getEvents(int timeoutMillis,RawEvent* buffer, size_t bufferSize) {
??? for (;;){
??????? // 處理輸入設備卸載操作
???????......
??????? /* 在EventHub的構造函數中,mNeedToScanDevices被設置為true,因此創建后第一次調用
??????????getEvents()函數會執行scanDevicesLocked(),加載所有輸入設備 */
??????? if(mNeedToScanDevices) {
???????????mNeedToScanDevices = false;
???? ???????/***scanDevicesLocked()將會把/dev/input下所有可用的輸入設備打開并存儲到Device**
**?????????????? 結構體中** */
???????????scanDevicesLocked();
???????????mNeedToSendFinishedDeviceScan = true;
??????? }
???????......
??? }
??? returnevent – buffer;
}
```
加載所有輸入設備由scanDevicesLocked()函數完成。看一下其實現:
```
[EventHub.cpp-->EventHub::scanDevicesLocked()]
void EventHub::scanDevicesLocked() {
??? // 調用scanDirLocked()函數遍歷/dev/input文件夾下的所有設備節點并打開
??? status_tres = scanDirLocked(DEVICE_PATH);
??? ......// 錯誤處理
??? // 打開一個名為VIRTUAL_KEYBOARD的輸入設備。這個設備時刻是打開著的。它是一個虛擬的輸入設
?????? 備,沒有對應的輸入節點。讀者先記住有這么一個輸入設備存在于輸入系統中 */
??? if(mDevices.indexOfKey(VIRTUAL_KEYBOARD_ID) < 0) {
???????createVirtualKeyboardLocked();
??? }
}
```
scanDirLocked()遍歷指定文件夾下的所有設備節點,分別對其執行openDeviceLocked()完成設備的打開操作。在這個函數中將為設備節點創建并加載Device結構體。參考其代碼:
```
[EventHub.cpp-->EventHub::openDeviceLocked()]
status_t EventHub::openDeviceLocked(const char*devicePath) {
??? // 打開設備節點的文件描述符,用于獲取設備信息以及讀取原始輸入事件
??? int fd =open(devicePath, O_RDWR | O_CLOEXEC);
??? // 接下來的代碼通過ioctl()函數從設備節點中獲取輸入設備的廠商信息
???InputDeviceIdentifier identifier;
??? ......
??? // 分配一個設備Id并創建Device結構體
??? int32_tdeviceId = mNextDeviceId++;
??? Device*device = new Device(fd, deviceId, String8(devicePath), identifier);
??? // 為此設備加載配置信息。、
???loadConfigurationLocked(device);
??? // **① 通過ioctl函數獲取設備的事件位掩碼。**事件位掩碼指定了輸入設備可以產生何種類型的輸入事件
???ioctl(fd, EVIOCGBIT(EV_KEY, sizeof(device->keyBitmask)),device->keyBitmask);
??? ......
???ioctl(fd, EVIOCGPROP(sizeof(device->propBitmask)),device->propBitmask);
??? // 接下來的一大段內容是根據事件位掩碼為設備分配類別,即設置classes字段。、
??? ......
??? /* **② 將設備節點的描述符的可讀事件注冊到Epoll中。**當此設備的輸入事件到來時,Epoll會在
??????getEvents()函數的調用中產生一條epoll事件 */
??? structepoll_event eventItem;
???memset(&eventItem, 0, sizeof(eventItem));
???eventItem.events = EPOLLIN;
???eventItem.data.u32 = deviceId; /* 注意,epoll_event的自定義信息是設備的Id
??? if(epoll_ctl(mEpollFd, EPOLL_CTL_ADD, fd, &eventItem)) {
?????? ?......
??? }
??? ......
??? // **③ 調用addDeviceLocked()將Device添加到mDevices字典中**
???addDeviceLocked(device);
??? return0;
}
```
openDeviceLocked()函數打開指定路徑的設備節點,為其創建并填充Device結構體,然后將設備節點的可讀事件注冊到Epoll中,最后將新建的Device結構體添加到mDevices字典中以供檢索之需。整個過程比較清晰,但仍有以下幾點需要注意:
+ openDeviceLocked()函數從設備節點中獲取了設備可能上報的事件類型,并據此為設備分配了類別。整個分配過程非常繁瑣,由于它和InputReader的事件加工過程關系緊密,因此這部分內容將在5.2.4節再做詳細討論。
+ 向Epoll注冊設備節點的可讀事件時,epoll_event的自定義數據被設置為設備的Id而不是fd。
+ addDeviceLocked()將新建的Device對象添加到mDevices字典中的同時也會將其添加到一個名為mOpeningDevices的鏈表中。這個鏈表保存了剛剛被加載,但尚未通過getEvents()函數向InputReader發送DEVICE_ADD事件的設備。
完成輸入設備的加載之后,通過getEvents()函數便可以讀取到此設備所產生的輸入事件了。除了在getEvents()函數中使用scanDevicesLockd()一次性加載所有輸入設備,當INotify事件告知有新的輸入設備節點被創建時,也會通過opendDeviceLocked()將設備加載,稍后再做討論。
##### (2)輸入設備的卸載
輸入設備的卸載由closeDeviceLocked()函數完成。由于此函數的工作內容與openDeviceLocked()函數正好相反,就不列出其代碼了。設備的卸載過程為:
+ 從Epoll中注銷對描述符的監聽。
+ 關閉設備節點的描述符。
+ 從mDevices字典中刪除對應的Device對象。
+ 將Device對象添加到mClosingDevices鏈表中,與mOpeningDevices類似,這個鏈表保存了剛剛被卸載,但尚未通過getEvents()函數向InputReader發送DEVICE_REMOVED事件的設備。
同加載設備一樣,在getEvents()函數中有根據需要卸載所有輸入設備的操作(比如當EventHub要求重新加載所有設備時,會先將所有設備卸載)。并且當INotify事件告知有設備節點刪除時也會調用closeDeviceLocked()將設備卸載。
##### (3)設備增刪事件
在分析設備的加載與卸載時發現,新加載的設備與新卸載的設備會被分別放入mOpeningDevices與mClosingDevices鏈表之中。這兩個鏈表將是在getEvents()函數中向InputReader發送設備增刪事件的依據。
參考getEvents()函數的相關代碼,以設備卸載事件為例看一下設備增刪事件是如何產生的:
```
[EventHub.cpp-->EventHub::getEvents()]
size_t EventHub::getEvents(int timeoutMillis,RawEvent* buffer, size_t bufferSize) {
??? for (;;){
??????? // 遍歷mClosingDevices鏈表,為每一個已卸載的設備生成DEVICE_REMOVED事件
???????while (mClosingDevices) {
???????????Device* device = mClosingDevices;
???????????mClosingDevices = device->next;
???????????/* 分析getEvents()函數的工作方式時介紹過,event指針指向buffer中下一個可用于填充
??????????????事件的RawEvent對象 */
???????????event->when = now; // 設置產生事件的事件戳
???????????event->deviceId =
?????????????????? device->id ==mBuiltInKeyboardId ? BUILT_IN_KEYBOARD_ID : device->id;
???????????event->type = DEVICE_REMOVED; // 設置事件的類型為DEVICE_REMOVED
???????????event += 1; // 將event指針移動到下一個可用于填充事件的RawEvent對象
???????????delete device; // 生成DEVICE_REMOVED事件之后,被卸載的Device對象就不再需要了
???????????mNeedToSendFinishedDeviceScan = true; // 隨后發送FINISHED_DEVICE_SCAN事件
???????? ???/* 當buffer已滿則停止繼續生成事件,將已生成的事件返回給調用者。尚未生成的事件
??????????????將在下次getEvents()調用時生成并返回給調用者 */
???????????if (--capacity == 0) {
???????????????break;
???????????}
??????? }
??????? // 接下來進行DEVICE_ADDED事件的生成,此過程與 DEVICE_REMOVED事件的生成一致
???????......
??? }
??? returnevent – buffer;
}
```
可以看到,在一次getEvents()調用中會嘗試為所有尚未發送增刪事件的輸入設備生成對應的事件返回給調用者。表示設備增刪事件的RawEvent對象包含三個信息:產生事件的事件戳、產生事件的設備Id,以及事件類型(DEVICE_ADDED或DEVICE_REMOVED)。
當生成設備增刪事件時,會設置mNeedToSendFinishedDeviceSan為true,這個動作的意思是完成所有DEVICE_ADDED/REMOVED事件的生成之后,需要向getEvents()的調用者發送一個FINISHED_DEVICE_SCAN事件,表示設備增刪事件的上報結束。這個事件僅包括時間戳與事件類型兩個信息。
經過以上分析可知,EventHub可以產生的設備增刪事件一共有三種,而且這三種事件擁有固定的優先級,DEVICE_REMOVED事件的優先級最高,DEVICE_ADDED事件次之,FINISHED_DEVICE_SCAN事件最低。而且,getEvents()完成當前高優先級事件的生成之前,不會進行低優先級事件的生成。因此,當發生設備的加載與卸載時,EventHub所生成的完整的設備增刪事件序列如圖5-5所示,其中R表示DEVICE_REMOVED,A表示DEVICE_ADDED,F表示FINISHED_DEVICE_SCAN。

圖 5-5 設備增刪事件的完整序列
由于參數buffer的容量限制,這個事件序列可能需要通過多次getEvents()調用才能完整地返回給調用者。另外,根據5.2.2節的討論,設備增刪事件相對于Epoll事件擁有較高的優先級,因此從R1事件開始生成到F事件生成之前,getEvents()不會處理Epoll事件,也就是說不會生成原始輸入事件。
總結一下設備增刪事件的生成原理:
+ 當發生設備增刪時,addDeviceLocked()函數與closeDeviceLocked()函數會將相應的設備放入mOpeningDevices和mClosingDevices鏈表中。
+ getEvents()函數會根據mOpeningDevices和mClosingDevices兩個鏈表生成對應DEVICE_ADDED和DEVICE_REMOVED事件,其中后者的生成擁有高優先級。
+ DEVICE_ADDED和DEVICE_REMOVED事件都生成完畢后,getEvents()會生成FINISHED_DEVICE_SCAN事件,標志設備增刪事件序列的結束。
##### (4)通過INotify動態地加載與卸載設備
通過前文的介紹知道了openDeviceLocked()和closeDeviceLocked()可以加載與卸載輸入設備。接下來分析EventHub如何通過INotify進行設備的動態加載與卸載。在EventHub的構造函數中創建了一個名為mINotifyFd的INotify對象的描述符,用以監控/dev/input下設備節點的增刪。之后將mINotifyFd的可讀事件加入到Epoll中。于是可以確定動態加載與卸載設備的工作方式為:首先篩選epoll_wait()函數所取得的Epoll事件,如果Epoll事件表示了mINotifyFd可讀,便從mINotifyFd中讀取設備節點的增刪事件,然后通過執行openDeviceLocked()或closeDeviceLocked()進行設備的加載與卸載。
看一下getEvents()中與INotify相關的代碼:
```
[EventHub.cpp-->EventHub::getEvents()]
size_t EventHub::getEvents(int timeoutMillis,RawEvent* buffer, size_t bufferSize) {
??? for (;;){
???????...... // 設備增刪事件處理
??????? while(mPendingEventIndex < mPendingEventCount) {
???????????const struct epoll_event& eventItem =
???????????????????????????????? mPendingEventItems[mPendingEventIndex++];
???????????/* **① 通過Epoll事件的data字段確定此事件表示了mINotifyFd可讀**
??????????????注意EPOLL_ID_INOTIFY在EventHub的構造函數中作為data字段向
??????????????Epoll注冊mINotifyFd的可讀事件 */
???????????if (eventItem.data.u32 == EPOLL_ID_INOTIFY) {
???????????????if (eventItem.events & EPOLLIN) {
???????????????????mPendingINotify = true; // 標記INotify事件待處理
???????????????} else { ...... }
???????????????continue; // 繼續處理下一條Epoll事件
???????????}
???????????...... // 其他Epoll事件的處理
??????? }
??????? // 如果INotify事件待處理
??????? if(mPendingINotify && mPendingEventIndex >= mPendingEventCount) {
???????????mPendingINotify = false;
???????????/* **② 調用readNotifyLocked()函數讀取并處理存儲在mINotifyFd中的INotify事件**
??????????????這個函數將完成設備的加載與卸載 */
???????????readNotifyLocked();
???????????deviceChanged = true;
??????? }
??????? //**③ 如果處理了INotify事件,則返回到循環開始處,生成設備增刪事件**
??????? if(deviceChanged) {
???????????continue;
??????? }
??? }
}
```
getEvents()函數中與INotify相關的代碼共有三處:
+ 識別表示mINotifyFd可讀的Epoll事件,并通過設置mPendingINotify為true以標記有INotify事件待處理。getEvents()并沒有立刻處理INotify事件,因為此時進行設備的加載與卸載是不安全的。其他Epoll事件可能包含了來自即將被卸載的設備的輸入事件,因此需要將所有Epoll事件都處理完畢后再進行加載與卸載操作。
+ 當epoll_wait()所返回的Epoll事件都處理完畢后,調用readNotifyLocked()函數讀取mINotifyFd中的事件,并進行設備的加載與卸載操作。
+ 完成設備的動態加載與卸載后,需要返回到循環最開始處,以便設備增刪事件處理代碼生成設備的增刪事件。
其中第一部分與第三部分比較容易理解。接下來看一下readNotifyLocked()是如何工作的。
```
[EventHub.cpp-->EventHub::readNotifyLocked()]
status_t EventHub::readNotifyLocked() {
??? ......
??? // 從mINotifyFd中讀取INotify事件列表
??? res =read(mINotifyFd, event_buf, sizeof(event_buf));
??? ......
??? // 逐個處理列表中的事件
???while(res >= (int)sizeof(*event)) {
???????strcpy(filename, event->name); // 從事件中獲取設備節點路徑
???????if(event->mask & IN_CREATE) {
???????????openDeviceLocked(devname); // 如果事件類型為IN_CREATE,則加載對應設備
??????? }else {
???????????closeDeviceByPathLocked(devname); // 否則卸載對應設備
??????? }
??????? ......// 移動到列表中的下一個事件
??? }
??? return0;
}
```
##### (5) EventHub設備管理總結
至此,EventHub的設備管理相關的知識便討論完畢了。在這里進行一下總結:
+ EventHub通過Device結構體描述輸入設備的各種信息。
+ EventHub在getEvents()函數中進行設備的加載與卸載操作。設備的加載與卸載分為按需加載或卸載以及通過INotify動態加載或卸載特定設備兩種方式。
+ getEvents()函數進行了設備的加載與卸載操作后,會生成DEVICE_ADDED、DEVICE_REMOVED以及FINISHED_DEVICE_SCAN三種設備增刪事件,并且設備增刪事件擁有高于Epoll事件的優先級。
#### 4.原始輸入事件的監聽與讀取
本節將討論EventHub另一個核心的功能,監聽與讀取原始輸入事件。
回憶一下輸入設備的加載過程,當設備加載時,openDeviceLocked()會打開設備節點的文件描述符,并將其可讀事件注冊進Epoll中。于是當設備的原始輸入事件到來時,getEvents()函數將會獲得一條Epoll事件,然后根據此Epoll事件讀取文件描述符中的原始輸入事件,將其填充到RawEvents結構體并放入buffer中被調用者取走。openDeviceLocked()注冊了設備節點的EPOLLIN和EPOLLHUP兩個事件,分別表示可讀與被掛起(不可用),因此getEvents()需要分別處理這兩種事件。
看一下getEvents()函數中的相關代碼:
```
[EventHub.cpp-->EventHub::getEvents()]
size_t EventHub::getEvents(int timeoutMillis,RawEvent* buffer, size_t bufferSize) {
??? for (;;){
???????...... // 設備增刪事件處理
??????? while(mPendingEventIndex < mPendingEventCount) {
???????????const struct epoll_event& eventItem =
???????????????????????????????? mPendingEventItems[mPendingEventIndex++];
???????????...... // INotify與wakeFd的Epoll事件處理
???????????/* **① 通過Epoll的data.u32字段獲取設備Id,進而獲取對應的Device對象。**如果無法找到
??????????????對應的Device對象,說明此Epoll事件并不表示原始輸入事件的到來,忽略之 */
???????????ssize_t deviceIndex = mDevices.indexOfKey(eventItem.data.u32);
???????????Device* device = mDevices.valueAt(deviceIndex);
???????????......
???????????if (eventItem.events & EPOLLIN) {
???????????????/* **② 如果Epoll事件為EPOLLIN,表示設備節點有原始輸入事件可讀**。此時可以從描述符
??????????????????中讀取。讀取結果作為input_event結構體并存儲在readBuffer中,注意事件的個數
?????????????????? 受到capacity的限制*/
???????????????int32_t readSize = read(device->fd, readBuffer,
??????????????????????? sizeof(structinput_event) * capacity);
???????????????if (......) {??????????????????? ......// 一些錯誤處理 }
????????? ??????else {
???????????????????size_t count = size_t(readSize) / sizeof(struct input_event);
???????????????????/* **② 將讀取到的每一個input_event結構體中的數據轉換為一個RawEvent對象,**
?????????????????????? 并存儲在buffer參數中以返回給調用者 */
???????????????????for (size_t i = 0; i < count; i++) {
??????????????????????? const structinput_event& iev = readBuffer[i];
??????????????????????? ......
?????????? ?????????????event->when = now;
??????????????????????? event->deviceId =deviceId;
??????????????????????? event->type =iev.type;
??????????????????????? event->code =iev.code;
??????????????????????? event->value =iev.value;
???????????????????????event += 1; // 移動到buffer的下一個可用元素
???????????????????}
???????????????????/* 接下來的一個細節需要注意,因為buffer的容量限制,可能無法完全讀取設備節點
?????????????????????? 中存儲的原始事件。一旦buffer滿了則需要立刻返回給調用者。設備節點中剩余的
?????????????????????? 輸入事件將在下次getEvents()調用時繼續讀取,也就是說,當前的Epoll事件
?????????????????????? 并未處理完畢。mPendingEventIndex -= 1的目的就是使下次getEvents()調用
?????????????????????? 能夠繼續處理這個Epoll事件 */
???????????????????capacity -= count;
???????????????????if (capacity == 0) {
??????????????????????? mPendingEventIndex -=1;
????????? ??????????????break;
???????????????????}
???????????????}
???????????} else if (eventItem.events & EPOLLHUP) {
???????????????deviceChanged = true; // 如果設備節點的文件描述符被掛起則卸載此設備
???????????????closeDeviceLocked(device);
???????????} else { ...... }
??????? }
???????...... // 讀取并處理INotify事件
??????? ......// 等待新的Epoll事件
??? }
??? returnevent – buffer;
}
```
getEvents()通過Epoll事件的data.u32字段在mDevices列表中查找已加載的設備,并從設備的文件描述符中讀取原始輸入事件列表。從文件描述符中讀取的原始輸入事件存儲在input_event結構體中,這個結構體的四個字段存儲了事件的事件戳、類型、代碼與值四個元素。然后逐一將input_event的數據轉存到RawEvent中并保存至buffer以返回給調用者。
**注意** 為了敘述簡單,上述代碼使用了調用getEvents()的時間作為輸入事件的時間戳。由于調用getEvents()函數的時機與用戶操作的時間差的存在,會使得此時間戳與事件的真實時間有所偏差。從設備節點中讀取的input_event中也包含了一個時間戳,這個時間戳消除了getEvents()調用所帶來的時間差,因此可以獲得更精確的時間控制。可以通過打開HAVE_POSIX_CLOCKS宏以使用input_event中的時間而不是將getEvents()調用的時間作為輸入事件的時間戳。
需要注意的是,由于Epoll事件的處理優先級低于設備增刪事件,因此當發生設備加載與卸載動作時,不會產生設備輸入事件。另外還需注意,在一個監聽周期中,getEvents()在將一個設備節點中的所有原始輸入事件讀取完畢之前,不會讀取其他設備節點中的事件。
#### 5.EventHub總結
本節針對EventHub的設備管理與原始輸入事件的監聽讀取兩個核心內容介紹了EventHub的工作原理。EventHub作為直接操作設備節點的輸入系統組件,隱藏了INotify與Epoll以及設備節點讀取等底層操作,通過一個簡單的接口getEvents()向使用者提供抽取設備事件與原始輸入事件的功能。EventHub的核心功能都在getEvents()函數中完成,因此深入理解getEvents()的工作原理對于深入理解EventHub至關重要。
getEvents()函數的本質是通過epoll_wait()獲取Epoll事件到事件池,并對事件池中的事件進行消費的過程。從epoll_wait()的調用開始到事件池中最后一個事件被消費完畢的過程稱之為EventHub的一個監聽周期。由于buffer參數的尺寸限制,一個監聽周期可能包含多個getEvents()調用。周期中的第一個getEvents()調用一定會因事件池的枯竭而直接進行epoll_wait(),而周期中的最后一個getEvents()一定會將事件池中的最后一條事件消費完畢并將事件返回給調用者。前文所討論的事件優先級都是在同一個監聽周期內而言的。
在本節中出現了很多種事件,有原始輸入事件、設備增刪事件、Epoll事件、INotify事件等,存儲事件的結構體有RawEvent、epoll_event、inotify_event、input_event等。圖5-6可以幫助讀者理清這些事件之間的關系。

圖 5-6 EventHub的事件關聯
另外,getEvents()函數返回的事件列表依照事件的優先級擁有特定的順序。并且在一個監聽周期中,同一輸入設備的輸入事件在列表中是相鄰的。
至此,相信讀者對EventHub的工作原理,以及EventHub的事件監聽與讀取機制有了深入的了解。接下來的內容將討論EventHub所提供的原始輸入事件如何被加工為Android輸入事件,這個加工者就是Reader子系統中的另一員大將:InputReader。
> [1] 感興趣的讀者可以通過`gitclone git://github.com/barzooka/robert.git`下載一個可以錄制用戶輸入操作并可以實時回放的小工具。