《深入理解Android 卷III》第五章 深入理解Android输入系统
《深入理解Android 卷III》即将公布。作者是张大伟。此书填补了深入理解Android Framework卷中的一个主要空白。即Android Framework中和UI相关的部分。在一个特别讲究颜值的时代,本书分析了Android 4.2中WindowManagerService、ViewRoot、Input系统、StatusBar、Wallpaper等重要“颜值绘制/处理”模块
第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的构造函数非常easy。看来绝大部分的初始化工作都位于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所看到的。
watermark/2/text/aHR0cDovL2Jsb2cuY3Nkbi5uZXQv/font/5a6L5L2T/fontsize/400/fill/I0JBQkFCMA==/dissolve/70/gravity/Center" alt="" />
图 5-3 三个线程,三台水泵
InputManagerService.start()函数的作用,就像为Reader线程、Dispatcher线程这两台水泵按下开关。而Looper这台水泵在窗体创建时便已经处于运行状态了。自此,输入系统动力十足地開始运转,设备节点中的输入事件将被源源不断地抽取给事件处理者。本章的主要内容便是讨论这三台水泵的工作原理。
2. IMS的成员关系
依据对IMS的创建过程的分析。能够得到IMS的成员关系如图5-4所看到的,这幅图省略了一些非关键的引用与继承关系。
注意IMS内部做了非常多的抽象工作。EventHub、nputReader以及InputDispatcher等实际上都继承自相应的名为XXXInterface的接口,而且仅通过接口进行相互之间的引用。鉴于这些接口各自仅有唯一的实现,为了简化叙述我们将不提及这些接口,可是读者在实际学习与研究时须要注意这一点。
watermark/2/text/aHR0cDovL2Jsb2cuY3Nkbi5uZXQv/font/5a6L5L2T/fontsize/400/fill/I0JBQkFCMA==/dissolve/70/gravity/Center" alt="" />
图 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。
watermark/2/text/aHR0cDovL2Jsb2cuY3Nkbi5uZXQv/font/5a6L5L2T/fontsize/400/fill/I0JBQkFCMA==/dissolve/70/gravity/Center" alt="" />
图 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中的事件,并进行设备的载入与卸载操作。
· 完毕设备的动态载入与卸载后,须要返回到循环最開始处,以便设备增删事件处理代码生成设备的增删事件。
当中第一部分与第三部分比較easy理解。
接下来看一下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能够帮助读者理清这些事件之间的关系。
watermark/2/text/aHR0cDovL2Jsb2cuY3Nkbi5uZXQv/font/5a6L5L2T/fontsize/400/fill/I0JBQkFCMA==/dissolve/70/gravity/Center" alt="" />
图 5-6 EventHub的事件关联
另外,getEvents()函数返回的事件列表按照事件的优先级拥有特定的顺序。
而且在一个监听周期中。同一输入设备的输入事件在列表中是相邻的。
至此。相信读者对EventHub的工作原理,以及EventHub的事件监听与读取机制有了深入的了解。接下来的内容将讨论EventHub所提供的原始输入事件怎样被加工为Android输入事件,这个加工者就是Reader子系统中的还有一员大将:InputReader。
[1] 感兴趣的读者能够通过gitclone git://github.com/barzooka/robert.git下载一个能够录制用户输入操作并能够实时回放的小工具。
《深入理解Android 卷III》第五章 深入理解Android输入系统的更多相关文章
- 《深入理解Android 卷III》第二章 深入理解Java Binder和MessageQueue
<深入理解Android 卷III>即将公布.作者是张大伟.此书填补了深入理解Android Framework卷中的一个主要空白,即Android Framework中和UI相关的部分. ...
- 《深入理解Android 卷III》第八章深入理解Android壁纸
<深入理解Android 卷III>即将公布,作者是张大伟. 此书填补了深入理解Android Framework卷中的一个主要空白,即Android Framework中和UI相关的部分 ...
- 《深入理解Android 卷III》第七章 深入理解SystemUI
<深入理解Android 卷III>即将公布,作者是张大伟.此书填补了深入理解Android Framework卷中的一个主要空白,即Android Framework中和UI相关的部分. ...
- 《深入理解Android 卷III》第六章 深入理解控件(ViewRoot)系统
<深入理解Android 卷III>即将公布,作者是张大伟.此书填补了深入理解Android Framework卷中的一个主要空白,即Android Framework中和UI相关的部分. ...
- 《深入理解Android 卷III》第四章 深入理解WindowManagerService
<深入理解Android 卷III>即将公布,作者是张大伟.此书填补了深入理解Android Framework卷中的一个主要空白.即Android Framework中和UI相关的部分. ...
- [深入理解Android卷一全文-第三章]深入理解init
因为<深入理解Android 卷一>和<深入理解Android卷二>不再出版,而知识的传播不应该因为纸质媒介的问题而中断,所以我将在CSDN博客中全文转发这两本书的全部内容. ...
- [深入理解Android卷一全文-第四章]深入理解zygote
由于<深入理解Android 卷一>和<深入理解Android卷二>不再出版,而知识的传播不应该由于纸质媒介的问题而中断,所以我将在CSDN博客中全文转发这两本书的所有内容. ...
- [深入理解Android卷一全文-第七章]深入理解Audio系统
由于<深入理解Android 卷一>和<深入理解Android卷二>不再出版,而知识的传播不应该由于纸质媒介的问题而中断,所以我将在CSDN博客中全文转发这两本书的全部内容. ...
- Android深度探索-卷1第五章心得体会
S3C6410是由三星公司推出的一款低功耗.高性价比的RISC处理器,开发是,首先安装minicom串口调试工具: 第一步:检测当前系统是否支持USB转串口. Lsmod | grep usseria ...
随机推荐
- Java&Xml教程(十一)JAXB实现XML与Java对象转换
JAXB是Java Architecture for XML Binding的缩写,用于在Java类与XML之间建立映射,可以帮助开发人员非常方便的將XML和Java对象进行相互转换. 本文以一个简单 ...
- Codeforces Round #FF (Div. 2):B. DZY Loves Strings
B. DZY Loves Strings time limit per test 1 second memory limit per test 256 megabytes input standard ...
- UESTC--1269--ZhangYu Speech(模拟)
ZhangYu Speech Time Limit: 1000MS Memory Limit: 65535KB 64bit IO Format: %lld & %llu Submit ...
- 一个Python项目的创建架构
要进行Python项目的编写,很多人刚开始一筹莫展,不知道该如何去构建一个项目,现在粗略的描述一下一个项目的创建过程,供大家参考了解一下: 大家可以先忽略其中创建的函数 ,每个包的含义都有定义,大家可 ...
- Qt-信号和槽-1对1
前言:信号和槽是Qt的核心机制,窗体和控件对象之间的沟通一般都使用信号和槽. 对于部件有哪些信号和槽,可以查看help文档. 一.使用自定义槽 1.1 新建工程 新建工程,新建Widget类(基于QW ...
- JQuery事件绑定,bind与on区别
jquery事件绑定bind:向匹配元素添加一个或多个事件处理器 $(selector).bind("click",data,function); live:向当前或未来的匹配元素 ...
- 五年磨一剑:Java 开源博客 Solo 1.0.0 发布了!
从 Solo 第一个版本发布至今,已经过去 5 年了.今天我们非常自豪地宣布,Solo 1.0.0 正式发布,感谢一直以来关注 B3log 开源的朋友! 目前 B3log 开源有三款产品: GitHu ...
- Android 对ScrollView滚动监听,实现美团、大众点评的购买悬浮效果
转帖请注明本文出自xiaanming的博客(http://blog.csdn.net/xiaanming),请尊重他人的辛勤劳动成果,谢谢! 随着移动互联网的快速发展,它已经和我们的生活息息相关了,在 ...
- 01《UML大战需求分析》阅读笔记之一
在大二的时候就已经在课堂上对UML也就是统一建模语言有了初步的了解,但是却不怎么明白,虽然可以画图可以完成任务,但是有些糊里糊涂.所以特地把这门书作为精读书籍,想要更加深度地学习UML.很多内容只用语 ...
- Unity 动画系统(Mecanim) 术语及翻译 表格
原文 翻译 Animation Clip 视频片段 Avatar 阿凡达 Retargeting 重定向 Rigging 绑定 skinning 蒙皮 Animator Component 动画组件 ...