本章主要内容:
· 研究输入事件从设备节点开始到窗口处理函数的流程
· 介绍原始输入事件的读取与加工的原理
· 研究事件派发机制
· 讨论事件在输入系统与窗口之间的传递与反馈的过程
· 介绍焦点窗口的选择、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
第4章通过分析WMS详细讨论了Android的窗口管理、布局及动画的工作机制。窗口不仅是内容绘制的载体,同时也是用户输入事件的目标。本章将详细讨论Android输入系统的工作原理,包括输入设备的管理、输入事件的加工方式以及派发流程。因此本章的探讨对象有两个:输入设备、输入事件。
触摸屏与键盘是Android最普遍也是最标准的输入设备。其实Android所支持的输入设备的种类不止这两个,鼠标、游戏手柄均在内建的支持之列。当输入设备可用时,Linux内核会在/dev/input/下创建对应的名为event0~n或其他名称的设备节点。而当输入设备不可用时,则会将对应的节点删除。在用户空间可以通过ioctl的方式从这些设备节点中获取其对应的输入设备的类型、厂商、描述等信息。
当用户操作输入设备时,Linux内核接收到相应的硬件中断,然后将中断加工成原始的输入事件数据并写入其对应的设备节点中,在用户空间可以通过read()函数将事件数据读出。
Android输入系统的工作原理概括来说,就是监控/dev/input/下的所有设备节点,当某个节点有数据可读时,将数据读出并进行一系列的翻译加工,然后在所有的窗口中寻找合适的事件接收者,并派发给它。
以Nexus4为例,其/dev/input/下有evnet0~5六个输入设备的节点。它们都是什么输入设备呢?用户的一次输入操作会产生什么样的事件数据呢?获取答案的最简单的办法就是是用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输入系统的基本原理。
上一节讲述了输入事件的源头是位于/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内核以外的其他参与者的工作原理。
同以往一样,本节通过IMS的启动过程,探讨IMS的构成。上一节提到,IMS分为Java层与Native层两个部分,其启动过程是从Java部分的初始化开始,进而完成Native部分的初始化。
同其他系统服务一样,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()函数完成启动。
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
/* 新建了一个NativeInputManager对象,NativeInputManager,此对象将是Native层组件与
Java层IMS进行通信的桥梁 */
NativeInputManager* im = new NativeInputManager(contextObj, serviceObj,
messageQueue->getLooper());
im->incStrong(serviceObj);
// 返回了NativeInputManager对象的指针给Java层的IMS,IMS将其保存在mPtr成员变量中
returnreinterpret_cast
}
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
mLooper(looper) {
......
// 出现重点了, NativeInputManager创建了EventHub
sp
// 接着创建了Native层的InputManager
mInputManager = new InputManager(eventHub, this, this);
}
在NativeInputManager的构造函数中,创建了两个关键人物,分别是EventHub与InputManager。EventHub复杂的构造函数使其在创建后便拥有了监听设备节点的能力,这一小节中暂不讨论它的构造函数,读者仅需知道EventHub在这里初始化即可。紧接着便是InputManager的创建了,看一下其构造函数:
[InputManager.cpp-->InputManager::InputManager()]
InputManager::InputManager(
const sp
const sp
const sp
// 创建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的结构体系
完成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这台水泵在窗口创建时便已经处于运行状态了。自此,输入系统动力十足地开始运转,设备节点中的输入事件将被源源不断地抽取给事件处理者。本章的主要内容便是讨论这三台水泵的工作原理。
根据对IMS的创建过程的分析,可以得到IMS的成员关系如图5-4所示,这幅图省略了一些非关键的引用与继承关系。
注意IMS内部做了很多的抽象工作,EventHub、nputReader以及InputDispatcher等实际上都继承自相应的名为XXXInterface的接口,并且仅通过接口进行相互之间的引用。鉴于这些接口各自仅有唯一的实现,为了简化叙述我们将不提及这些接口,但是读者在实际学习与研究时需要注意这一点。
图 5-4 IMS的成员关系
在图5-4中,左侧部分为Reader子系统对应于图5-3中的第一台水泵,右侧部分为Dispatcher子系统,对应于图5-3中的第二台水泵。了解了IMS的成员关系后便可以开始我们的IMS深入理解之旅了!
本节将深入探讨第一台水泵——Reader子系统的工作原理。Reader子系统的输入端是设备节点,输出端是Dispatcher子系统的派发队列。从设备节点到派发队列之间的过程发生了什么呢?本章一开始曾经介绍过,一个设备节点对应了一个输入设备,并且其中存储了内核写入的原始事件。因此设备节点拥有两个概念:设备与原始事件。因此Reader子系统需要处理输入设备以及原始事件两种类型的对象。
设备节点的新建与删除表示了输入设备的可用与无效,Reader子系统需要加载或删除对应设备的配置信息;而设备节点中是否有内容可读表示了是否有新的原始事件到来,有新的原始事件到来时Reader子系统需要开始对新事件进行加工并放置到派发队列中。问题是应该如何监控设备节点的新建与删除动作以及如何确定节点中有内容可读呢?最简单的办法是在线程循环中不断地轮询,然而这会导致非常低下的效率,更会导致电量在无谓地轮询中消耗。Android使用由Linux提供的两套机制INotify与Epoll优雅地解决了这两个问题。在正式探讨Reader子系统的工作原理之前,需要首先了解这两套机制的使用方法。
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了。
无论是从设备节点中获取原始输入事件还是从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()等待新事件的发生。
INotify与Epoll这两套由Linux提供的事件监听机制以最小的开销解决了文件系统变化以及文件描述符可读可写状态变化的监听问题。它们是Reader子系统运行的基石,了解了这两个机制的使用方法之后便为对Reader子系统的分析学习铺平了道路。
在了解了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的总体工作流程。而我们接下来将详细讨论这三步的实现。
InputReader在其线程循环中的第一个工作便是从EventHub中读取一批未处理的事件。EventHub是如何工作的呢?
EventHub的直译是事件集线器,顾名思义,它将所有的输入事件通过一个接口getEvents()将从多个输入设备节点中读取的事件交给InputReader,是输入系统最底层的一个组件。它是如何工作呢?没错,正是基于前文所述的INotify与Epoll两套机制。
在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的监听工作分为设备节点和原始输入事件两个方面,接下来将深入探讨这两方面的内容。
正如前文所述,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。
因为输入设备是输入事件的来源,并且决定了输入事件的含义,因此首先讨论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
sp
// 力反馈相关的信息。有些设备如高级的游戏手柄支持力反馈功能,目前暂不考虑
boolffEffectPlaying;
int16_tffEffectId;
};
Device结构体所存储的信息主要包括以下几个方面:
· 设备节点信息:保存了输入设备节点的文件描述符、文件路径等。
· 厂商信息:包括供应商、设备型号、名称等信息,这些信息决定了加载配置文件与键盘映射表的路径。
· 设备特性信息:包括设备的类别,可以上报的事件种类等。这些特性信息直接影响了InputReader对其所产生的事件的加工处理方式。
· 设备的配置信息:包括键盘映射表及其他自定义的信息,从特定位置的配置文件中读取。
另外,Device结构体还存储了力反馈所需的一些数据。在本节中暂不讨论。
EventHub用一个名为mDevices的字典保存当前处于打开状态的设备节点的Device结构体。字典的键为设备Id。
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()将设备加载,稍后再做讨论。
输入设备的卸载由closeDeviceLocked()函数完成。由于此函数的工作内容与openDeviceLocked()函数正好相反,就不列出其代码了。设备的卸载过程为:
· 从Epoll中注销对描述符的监听。
· 关闭设备节点的描述符。
· 从mDevices字典中删除对应的Device对象。
· 将Device对象添加到mClosingDevices链表中,与mOpeningDevices类似,这个链表保存了刚刚被卸载,但尚未通过getEvents()函数向InputReader发送DEVICE_REMOVED事件的设备。
同加载设备一样,在getEvents()函数中有根据需要卸载所有输入设备的操作(比如当EventHub要求重新加载所有设备时,会先将所有设备卸载)。并且当INotify事件告知有设备节点删除时也会调用closeDeviceLocked()将设备卸载。
在分析设备的加载与卸载时发现,新加载的设备与新卸载的设备会被分别放入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事件,标志设备增删事件序列的结束。
通过前文的介绍知道了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;
}
至此,EventHub的设备管理相关的知识便讨论完毕了。在这里进行一下总结:
· EventHub通过Device结构体描述输入设备的各种信息。
· EventHub在getEvents()函数中进行设备的加载与卸载操作。设备的加载与卸载分为按需加载或卸载以及通过INotify动态加载或卸载特定设备两种方式。
· getEvents()函数进行了设备的加载与卸载操作后,会生成DEVICE_ADDED、DEVICE_REMOVED以及FINISHED_DEVICE_SCAN三种设备增删事件,并且设备增删事件拥有高于Epoll事件的优先级。
本节将讨论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()在将一个设备节点中的所有原始输入事件读取完毕之前,不会读取其他设备节点中的事件。
本节针对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下载一个可以录制用户输入操作并可以实时回放的小工具。