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`下載一個可以錄制用戶輸入操作并可以實時回放的小工具。
- 前言
- 推薦序
- 第1章 開發環境部署
- 1.1獲取Android源代碼
- 1.2Android的編譯
- 1.3在IDE中導入Android源代碼
- 1.3.1將Android源代碼導入Eclipse
- 1.3.2將Android源代碼導入SourceInsight
- 1.4調試Android源代碼
- 1.4.1使用Eclipse調試Android Java源代碼
- 1.4.2使用gdb調試Android C/C 源代碼
- 1.5本章小結
- 第2章 深入理解Java Binder和MessageQueue
- 2.1概述
- 2.2Java層中的Binder分析
- 2.2.1Binder架構總覽
- 2.2.2初始化Java層Binder框架
- 2.2.3窺一斑,可見全豹乎
- 2.2.4理解AIDL
- 2.2.5Java層Binder架構總結
- 2.3心系兩界的MessageQueue
- 2.3.1MessageQueue的創建
- 2.3.2提取消息
- 2.3.3nativePollOnce函數分析
- 2.3.4MessageQueue總結
- 2.4本章小結
- 第3章 深入理解AudioService
- 3.1概述
- 3.2音量管理
- 3.2.1音量鍵的處理流程
- 3.2.2通用的音量設置函數setStreamVolume()
- 3.2.3靜音控制
- 3.2.4音量控制小結
- 3.3音頻外設的管理
- 3.3.1 WiredAccessoryObserver 設備狀態的監控
- 3.3.2AudioService的外設狀態管理
- 3.3.3音頻外設管理小結
- 3.4AudioFocus機制的實現
- 3.4.1AudioFocus簡單的例子
- 3.4.2AudioFocus實現原理簡介
- 3.4.3申請AudioFocus
- 3.4.4釋放AudioFocus
- 3.4.5AudioFocus小結
- 3.5AudioService的其他功能
- 3.6本章小結
- 第4章 深入理解WindowManager-Service
- 4.1初識WindowManagerService
- 4.1.1一個從命令行啟動的動畫窗口
- 4.1.2WMS的構成
- 4.1.3初識WMS的小結
- 4.2WMS的窗口管理結構
- 4.2.1理解WindowToken
- 4.2.2理解WindowState
- 4.2.3理解DisplayContent
- 4.3理解窗口的顯示次序
- 4.3.1主序、子序和窗口類型
- 4.3.2通過主序與子序確定窗口的次序
- 4.3.3更新顯示次序到Surface
- 4.3.4關于顯示次序的小結
- 4.4窗口的布局
- 4.4.1從relayoutWindow()開始
- 4.4.2布局操作的外圍代碼分析
- 4.4.3初探performLayoutAndPlaceSurfacesLockedInner()
- 4.4.4布局的前期處理
- 4.4.5布局DisplayContent
- 4.4.6布局的階段
- 4.5WMS的動畫系統
- 4.5.1Android動畫原理簡介
- 4.5.2WMS的動畫系統框架
- 4.5.3WindowAnimator分析
- 4.5.4深入理解窗口動畫
- 4.5.5交替運行的布局系統與動畫系統
- 4.5.6動畫系統總結
- 4.6本章小結
- 第5章 深入理解Android輸入系統
- 5.1初識Android輸入系統
- 5.1.1getevent與sendevent工具
- 5.1.2Android輸入系統簡介
- 5.1.3IMS的構成
- 5.2原始事件的讀取與加工
- 5.2.1基礎知識:INotify與Epoll
- 5.2.2 InputReader的總體流程
- 5.2.3 深入理解EventHub
- 5.2.4 深入理解InputReader
- 5.2.5原始事件的讀取與加工總結
- 5.3輸入事件的派發
- 5.3.1通用事件派發流程
- 5.3.2按鍵事件的派發
- 5.3.3DispatcherPolicy與InputFilter
- 5.3.4輸入事件的派發總結
- 5.4輸入事件的發送、接收與反饋
- 5.4.1深入理解InputChannel
- 5.4.2連接InputDispatcher和窗口
- 5.4.3事件的發送
- 5.4.4事件的接收
- 5.4.5事件的反饋與發送循環
- 5.4.6輸入事件的發送、接收與反饋總結
- 5.5關于輸入系統的其他重要話題
- 5.5.1輸入事件ANR的產生
- 5.5.2 焦點窗口的確定
- 5.5.3以軟件方式模擬用戶操作
- 5.6本章小結
- 第6章 深入理解控件系統
- 6.1 初識Android的控件系統
- 6.1.1 另一種創建窗口的方法
- 6.1.2 控件系統的組成
- 6.2 深入理解WindowManager
- 6.2.1 WindowManager的創建與體系結構
- 6.2.2 通過WindowManagerGlobal添加窗口
- 6.2.3 更新窗口的布局
- 6.2.4 刪除窗口
- 6.2.5 WindowManager的總結
- 6.3 深入理解ViewRootImpl
- 6.3.1 ViewRootImpl的創建及其重要的成員
- 6.3.2 控件系統的心跳:performTraversals()
- 6.3.3 ViewRootImpl總結
- 6.4 深入理解控件樹的繪制
- 6.4.1 理解Canvas
- 6.4.2 View.invalidate()與臟區域
- 6.4.3 開始繪制
- 6.4.4 軟件繪制的原理
- 6.4.5 硬件加速繪制的原理
- 6.4.6 使用繪圖緩存
- 6.4.7 控件動畫
- 6.4.8 繪制控件樹的總結
- 6.5 深入理解輸入事件的派發
- 6.5.1 觸摸模式
- 6.5.2 控件焦點
- 6.5.3 輸入事件派發的綜述
- 6.5.4 按鍵事件的派發
- 6.5.5 觸摸事件的派發
- 6.5.6 輸入事件派發的總結
- 6.6 Activity與控件系統
- 6.6.1 理解PhoneWindow
- 6.6.2 Activity窗口的創建與顯示
- 6.7 本章小結
- 第7章 深入理解SystemUI
- 7.1 初識SystemUI
- 7.1.1 SystemUIService的啟動
- 7.1.2 狀態欄與導航欄的創建
- 7.1.3 理解IStatusBarService
- 7.1.4 SystemUI的體系結構
- 7.2 深入理解狀態欄
- 7.2.1 狀態欄窗口的創建與控件樹結構
- 7.2.2 通知信息的管理與顯示
- 7.2.3 系統狀態圖標區的管理與顯示
- 7.2.4 狀態欄總結
- 7.3 深入理解導航欄
- 7.3.1 導航欄的創建
- 7.3.2 虛擬按鍵的工作原理
- 7.3.3 SearchPanel
- 7.3.4 關于導航欄的其他話題
- 7.3.5 導航欄總結
- 7.4 禁用狀態欄與導航欄的功能
- 7.4.1 如何禁用狀態欄與導航欄的功能
- 7.4.2 StatusBarManagerService對禁用標記的維護
- 7.4.3 狀態欄與導航欄對禁用標記的響應
- 7.5 理解SystemUIVisibility
- 7.5.1 SystemUIVisibility在系統中的漫游過程
- 7.5.2 SystemUIVisibility發揮作用
- 7.5.3 SystemUIVisibility總結
- 7.6 本章小結
- 第8章 深入理解Android壁紙
- 8.1 初識Android壁紙
- 8.2深入理解動態壁紙
- 8.2.1啟動動態壁紙的方法
- 8.2.2壁紙服務的啟動原理
- 8.2.3 理解UpdateSurface()方法
- 8.2.4 壁紙的銷毀
- 8.2.5 理解Engine的回調
- 8.3 深入理解靜態壁紙-ImageWallpaper
- 8.3.1 獲取用作靜態壁紙的位圖
- 8.3.2 靜態壁紙位圖的設置
- 8.3.3 連接靜態壁紙的設置與獲取-WallpaperObserver
- 8.4 WMS對壁紙窗口的特殊處理
- 8.4.1 壁紙窗口Z序的確定
- 8.4.2 壁紙窗口的可見性
- 8.4.3 壁紙窗口的動畫
- 8.4.4 壁紙窗口總結
- 8.5 本章小結