![2b903735f3f82299c3acba5456b92652.png](https://i-blog.csdnimg.cn/blog_migrate/7c4a24fb7a75996c88788f96ecafa698.jpeg)
本篇是基于学习深入理解Android系列,以及自己工作中触及到的领域的一些学习及总结,仅供自己学习,不涉及任何版权纠纷。
深入理解Android输入系统
本章主要内容:
- 1. 研究输入事件从设备节点开始到窗口处理函数的流程
- 2. 介绍原始输入事件的读取与加工
- 3. 研究事件派发机制
- 4. 讨论事件在输入系统与窗口之间传递与反馈的过程
- 5. 介绍焦点窗口的选择、ANR的产生以及以软件方式模拟用户操作的原理
本章涉及的源代码文件名及位置:
NA
5.1 初识Android输入系统
5.1.1 getevent与sendevent工具
getevent写入节点:
执行adb shell getevent -t(-t参数表示打印事件的时间戳),按一下电源键(不要松手),可以得到以下输出:
[ 1336.934001] /dev/input/event0: 0001 0074 00000001
[ 1336.934001] /dev/input/event0: 0000 0000 00000000
松开电源键,又产生以下输出
[ 1341.160479] /dev/input/event0: 0001 0074 00000000
[ 1341.160479] /dev/input/event0: 0000 0000 00000000
这两条输出便是按下和抬起电源键时由内核生成的原始事件。注意其输出是十六进制的。每条数据有5项信息:产生事件时的时间戳()、产生事件的设备节点()、事件类型()、事件代码()以及事件的值()。其中时间戳、类型、代码、值便是原始事件的4项基本元素。本例0x01表示此事件为一条按键事件,代码0x74表示电源键的扫描码,值0x01表示按下,0x00表示抬起。这两个原始数据被输入系统包装成两个KeyEvent对象,作为两个按键事件派发给framework中感兴趣的模块。
sendevent写入节点:
sendevent的输入参数与getevent的输出是对应的,只不过sendevent的参数为十进制。电源键的代码0x74的十进制为116,快速执行如下命令实现点击电源键的效果:
adb shell sendevent /dev/input/event0 1 116 1 #按下电源键
adb shell sendevent /dev/input/event0 1 116 0 #抬起电源键
5.1.2 Android输入系统简介
输入事件的源头是位于、dev/input/下的设备节点,而输入系统的终点是由WMS管理的某个窗口。最初的输入事件为内核生成的原始事件,而最终交付给窗口的则是KeyEvent或MotionEvent对象。因此Android输入系统的主要工作是读取设备节点中的原始事件,将其加工封装,然后派发给一个指定的窗口以及窗口中的控件。这个过程由InputManagerService系统服务为核心的多个参与者共同完成。输入系统的总体流程和参与者如图5-1所示。
![85a57cc4924bb90875555fddaa0713da.png](https://i-blog.csdnimg.cn/blog_migrate/fcbe03dc3bd1604892dbdf64f110e552.jpeg)
图5-1描述了输入事件的处理流程以及输入系统中最基本的参与者。它们是:
1 Linux内核,接受输入设备的中断,并将原始事件的输入写入设备节点中
2 设备节点,作为内核和IMS的桥梁,他将原始事件的数据暴露给用户空间,以便IMS可以从中读取事件
3 InputManagerService,一个Android系统服务,它分为java层和native层两部分。java层负责与WMS通信。而native层则是InputReader和InputDispatcher两个输入系统关键组件的运行容器
4 EventHub,直接访问所有的设备节点。它通过一个名为getEvent()的函数将所有输入系统相关的待处理的底层事件返回给使用者。这些事件包括原始输入事件、设备节点的增删等。
5 InputReader,是IMS中的关键组件之一。它运行于一个独立的线程中,负责管理输入设备的列表与配置,以及进行输入事件的加工处理。它通过其线程循环不断地通过getEvents()函数从EventHub中将事件取出并进行处理。对于设备节点的增删事件,它会更新输入设备列表与配置。对于原始输入事件,InputReader对其进行翻译、组装、封装为包含更多信息、更具可读性的输入事件,然后交给InputDispatcher进行派发。
6 InputReaderPolicy,它为InputReader的事件加工处理提供一些策略配置,例如键盘布局信息等
7 InputDispatcher,是IMS中的另一个关键组件。它也运行于一个独立的线程中。InputDispatcher中保管了来自WMS的所有窗口的信息,其收到来自InputReader的输入事件后,会在其保管的窗口中寻找合适的窗口,并将事件派发给此窗口。
8 InputDispatcherPolicy,它为InputDispatcher的派发过程提供策略控制。例如截取某些特定的输入事件用作特殊用途,或者阻止将某些事件派发给目标窗口。一个典型的例子就是HOME键被InputDispatcherPolicy截取到PhoneWindowManager中进行处理,并阻止窗口收到HOME键按下的事件。
9 WMS,不是输入系统的一员,但它对InputDispatcher的正常工作起到重要作用。当新建窗口时,wms为新窗口和ims创建了事件传递所用的通道。另外,WMS还将所有窗口的息,包括窗口的可点击区域,焦点窗口等信息,实时的更新到IMS的InputDispatcher中,使得InputDispatcher可以正确地将事件派发到指定的窗口。
10 ViewRootImpl,对某些窗口,如壁纸窗口、SurfaceView的窗口来说,窗口就是输入事件派发的终点。而对其他的activity、对话框等使用了Android控件系统的窗口来说,输入事件的终点是控件View。ViewRootImpl将窗口所接收的输入事件沿着控件树将事件派发给感兴趣的控件。
简单来说,内核将原始事件写入设备节点中,InputReader不断地通过EventHub将原始事件取出来并翻译加工成Android输入事件,然后交给InputDispatcher。InputDispatcher根据WMS提供的窗口信息将事件交给合适的窗口。窗口的ViewRootImpl对象再沿着控件树将事件派发给感兴趣的控件。控件对其收到的事件做出响应,更新自己的画面、执行特定的动作。所有这些参与者以IMS为核心,构建Android输入体系。
5.1.3 IMS的构成
本节通过IMS的启动过程探讨IMS的构成。IMS分为java和native两部分,其启动过程是从java部分的初始化开始,进而完成native部分的初始化。
(1) IMS的诞生
同其他系统服务一样,IMS在SystemServer的ServerThread线程中启动。
[
IMS的构造方法很简单,大部分初始化工作在native层。
frameworks
在NativeInputManager构造函数中,创建InputManager,在InputManager中创建InputDispatcher和InputReader,在InputReader中创建EventHub。EventHub在创建后就拥有了监听设备节点的能力。
再看 initialize()
void
InputManager的构造函数创建了4个对象,分别是IMS的核心参与者InputReader与InputDispatcher,以及它们所在的线程InputReaderThread与InputDispatcherThread。注意InputManager的构造函数的参数readerPolicy与dispatcherPolicy,它们都是NativeInputManager。输入系统参与者创建完成,得到了一套体系如图5-2
(2)IMS的启动与运行
完成IMS的创建后,ServerThread执行InputManagerService.start()函数启动IMS。InputManager的创建过程分为InputReader与InputDispacher创建了承载它们运行的线程,然而并未启动这两个线程,因此IMS仍处于待命状态。此时start()函数的功能就是启动这两个线程,使得InputReader与InputDispacher开始工作。
![b4550b6e58d35910b1e868147bb9c79d.png](https://i-blog.csdnimg.cn/blog_migrate/e02dbded0cf3d8a8485d6bf811fad7f5.jpeg)
当两个线程启动后。InputReader在其线程循环中不断地从EventHub中抽取原始输入事件,进行加工处理后将加工所得的事件放入InputDispatcher的派发队列中。InputDispatcher则在其线程循环中将派发队列中的事件取出,查找合适的窗口,将事件写入窗口的事件接收管道中。窗口事件接收线程的Looper从管道中将事件取出,交由事件处理函数进行事件响应。整个过程共有三个线程首尾相接,像三个水泵一层层将事件交给事件处理函数。如图5-3所示
![bf082cf910d7d5dd0f1a0eb51af02a0b.png](https://i-blog.csdnimg.cn/blog_migrate/6f0b83d603ad2b8fa6b83c4f9a91b467.jpeg)
InputManagerService.start()函数的作用,就像为Reader线程和Dispatcher线程这两天水泵按下开关,而Looper这台水泵在窗口创建时便已经处于运行状态。
2 IMS的成员关系
根据对IMS创建过程分析,IMS成员关系如图5-4所示,这幅图省略了一些非关键的引用与继承关系。
IMS内部做了很多抽象工作,EventHub、InputReader以及InputDispatcher等实际上都继承自相应的名为XXXInterface的接口,并且仅通过接口进行相互之间的引用。鉴于这些接口各自仅有唯一的实现,为了简化叙述我们将不提及这些接口,但是读者在实际学习与研究时需要注意这一点。
![2b83e413064975a03fe871b6b969645f.png](https://i-blog.csdnimg.cn/blog_migrate/f9187254dcfd1d5a81ca00b8227b6feb.jpeg)
在图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介绍与使用 2 Epoll介绍与使用
5.2.2 InputReader的总体流程
首先要理解InputReader的运行方式。在5.1.3节介绍了InputReader被InputManager创建,并运行于InputReaderThread线程中。InputReader如何在InputReaderThread运行呢?
InputReaderThread继承自C的Thread类,Thread类封装了pthread线程工具,提供了与java层Thread类相似的API。C的Thread类提供了一个名为threadLoop()的纯虚函数,当线程开始运行后,将会在内建的线程循环中不断地调用threadLoop(),直到此函数返回false,则退出线程循环,从而结束线程。
[InputReader.cpp -> InputReaderThread::threadLoop()]
bool InputReaderThread::threadLoop() {
// 执行InputReader的loopOnce()函数
mReader->loopOnce();
return true;
}
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() {
...
// 1 通过EventHub抽取事件列表。读取的结果存储在参数mEventBuffer中,返回值表示事件的个数当EventHub中无事件可抽取时,此函数的调用将会阻塞直到事件到来或者超时
size_t count = mEventHub->getEvents(timeoutMillis, mEventBuffer, EVENT_BUFFER_SIZE);
{ // acquire lock
AutoMutex _l(mLock);
mReaderIsAliveCondition.broadcast();
if (count) {
// 2 如果有抽取事件,则调用processEventsLocked()对事件进行加工处理
processEventsLocked(mEventBuffer, count);
}
...
} // release lock
// Send out a message that the describes the changed input devices.
if (inputDevicesChanged) {
mPolicy->notifyInputDevicesChanged(inputDevices);
}
//3 发布事件,在processEventsLocked对事件加工处理后,便将处理后的事件存储在mQueuedListener中。在循环的最后,调用flush()将所有事件交给InputDispatcher
mQueuedListener->flush();
}
InputReader一次线程循环的思路,一共3步:
1 从EventHub中抽取未处理的事件列表。这些事件分为两类,一类是从设备节点中读取的原始输入事件,另一类是输入设备可用性变化事件,简称设备事件。
2 通过processEventsLocked()对事件进行处理。对于设备事件,此函数对根据设备的可用性加载或移除设备对应的配置信息。对于原始输入事件,则在进行转译、封装与加工后将结果暂存到mQueuedListener中。
3 所有事件处理完毕后,调用mQueuedListener.flush()将所有暂存的输入事件一次性地交付给InputDispatcher
5.2.3 深入理解EventHub
InputReader在其线程循环中的第一个工作便是从EventHub中读取一批未处理的事件。EventHub是如何工作的呢?
EventHub的直译是事件集线器,它将所有的输入事件通过一个接口getEvents()把从多个输入设备节点中读取的事件交给InputReader,它是输入系统最底层的一个组件。它是如何工作的?正是基于前文所述的INotify与Epoll两套机制。
1 设备节点监听的建立 在EventHub的构造函数中,它通过INotify与Epoll机制建立起对设备节点增删事件以及可读状态的监听。回忆下INotify与Epoll的使用方法。
frameworks/native/services/inputflinger/EventHub.cpp [EventHub.cpp->EventHub::EventHub()]
EventHub::EventHub(void) :
mBuiltInKeyboardId(NO_BUILT_IN_KEYBOARD), mNextDeviceId(1), mControllerNumbers(),
mOpeningDevices(0), mClosingDevices(0),
mNeedToSendFinishedDeviceScan(false),
mNeedToReopenDevices(false), mNeedToScanDevices(true),
mPendingEventCount(0), mPendingEventIndex(0), mPendingINotify(false) {
acquire_wake_lock(PARTIAL_WAKE_LOCK, WAKE_LOCK_ID);
//1 首先使用epoll_create()函数创建一个epoll对象。EPOLL_SIZE_HINT指定最大监听个数为8,这个epoll对象将用来监听设备节点是否有数据可读(有无事件)
mEpollFd = epoll_create(EPOLL_SIZE_HINT);
LOG_ALWAYS_FATAL_IF(mEpollFd < 0, "Could not create epoll instance. errno=%d", errno);
//2 创建一个inotify对象。这个inotify对象将被用来监听设备节点的增删事件
mINotifyFd = inotify_init();
//将存储设备节点的路径/dev/input作为监听对象添加到inotify对象中。当此文件夹下的设备节点发生创建与删除事件时,都可以通过mINotifyFd读取事件的详细信息
int result = inotify_add_watch(mINotifyFd, DEVICE_PATH, IN_DELETE | IN_CREATE);
LOG_ALWAYS_FATAL_IF(result < 0, "Could not register INotify for %s. errno=%d",
DEVICE_PATH, errno);
//3 接下来将mINotifyFd作为epoll的一个监控对象。当inotify事件到来时,epoll_wait()将立即返回,EventHub便可以从mINotifyFd中读取设备节点的增删信息,并进行相应处理
struct epoll_event eventItem;
memset(&eventItem, 0, sizeof(eventItem));
//监听mINotifyFd可读
eventItem.events = EPOLLIN;
//注意这里并没有使用fd字段,而使用了自定义的值EPOLL_ID_INOTIFY
eventItem.data.u32 = EPOLL_ID_INOTIFY;
// 将对mINotifyFd的监听注册到epoll对象中
result = epoll_ctl(mEpollFd, EPOLL_CTL_ADD, mINotifyFd, &eventItem);
LOG_ALWAYS_FATAL_IF(result != 0, "Could not add INotify to epoll instance. errno=%d", errno);
//创建一个名为wakeFds的匿名管道
int wakeFds[2];
result = pipe(wakeFds);
LOG_ALWAYS_FATAL_IF(result != 0, "Could not create wake pipe. errno=%d", errno);
mWakeReadPipeFd = wakeFds[0];
mWakeWritePipeFd = wakeFds[1];
result = fcntl(mWakeReadPipeFd, F_SETFL, O_NONBLOCK);
LOG_ALWAYS_FATAL_IF(result != 0, "Could not make wake read pipe non-blocking. errno=%d",
errno);
result = fcntl(mWakeWritePipeFd, F_SETFL, O_NONBLOCK);
LOG_ALWAYS_FATAL_IF(result != 0, "Could not make wake write pipe non-blocking. errno=%d",
errno);
eventItem.data.u32 = EPOLL_ID_WAKE;
// 将管道读取端的描述符的可读事件注册到epoll对象中。因为InputReader在执行getEvents()时会因无事件而导致其线程阻塞在epoll_wait()的调用里,然而有时希望能够立刻唤醒InputReader线程使其处理一些请求。此时只需向wakeFds管道的写入端写入任意数据,此时读取端有数据可读,使得epoll_wait()得以返回,从而达到唤醒InputReader线程的目的
result = epoll_ctl(mEpollFd, EPOLL_CTL_ADD, mWakeReadPipeFd, &eventItem);
LOG_ALWAYS_FATAL_IF(result != 0, "Could not add wake read pipe to epoll instance. errno=%d",
errno);
int major, minor;
getLinuxRelease(&major, &minor);
// EPOLLWAKEUP was introduced in kernel 3.5
mUsingEpollWakeup = major > 3 || (major == 3 && minor >= 5);
}
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_t when; //发生事件时的时间戳
int32_t deviceId; //产生事件的设备Id,它是由EventHub自行分配的,InputReader可以根据它从EventHub中获取此设备的详细信息
int32_t type; //事件的类型
int32_t code; //事件代码
int32_t value; //事件值
};
可以看出,RawEvent结构体与getevent工具的输出十分一致,包含了原始输入事件的4个基本元素,因此用RawEvent结构体表示原始输入事件是非常直观的。RawEvent同时也用来表示设备增删事件,为此EventHub定义了三个特殊的事件类型DEVICE_ADD DEVICE_REMOVED FINISHED_DEVICE_SCAN 用以与原始输入事件进行区别。
getEvents()函数的本质就是读取并处理Epoll事件与INotify事件。
size_t EventHub::getEvents(int timeoutMillis, RawEvent* buffer, size_t bufferSize) {
ALOG_ASSERT(bufferSize >= 1);
AutoMutex _l(mLock);
struct input_event readBuffer[bufferSize];
// event指针指向在buffer中下一个可用于存储事件的RawEvent结构体。每存储一个事件,event指针都会向后偏移一个元素
RawEvent* event = buffer;
// capacity记录了buffer中剩余的元素数量。当capacity为0时,表示buffer已满,此时需要停止继续处理新事件,并将已处理的事件返回给调用者
size_t capacity = bufferSize;
bool awoken = false;
// 接下来的循环是getEvents()函数的主体。在这个循环中,会先将可用事件放入buffer中并返回。如果没有可用事件,则进入epoll_wait()等待事件到来,epoll_wait()返回后会重新执行循环体,将新事件放入buffer
for (;;) {
nsecs_t now = systemTime(SYSTEM_TIME_MONOTONIC);
//1 首先进行与设备相关的工作。某些情况下,如果EventHub创建后第一次执行getEvents()函数时,需要扫描/dev/input文件夹下的所有设备节点并将这些设备打开。另外,当设备节点的增删事件发生时,会将这些事件存入buffer中
// Reopen input devices if needed.
if (mNeedToReopenDevices) {
mNeedToReopenDevices = false;
ALOGI("Reopening all input devices due to a configuration change.");
closeAllDevicesLocked();
mNeedToScanDevices = true;
break; // return to the caller before we actually rescan
}
...
//2 处理未被InputReader取走的输入事件与设备事件。epoll_wait()所取出的epoll_event存储在mPendingEventItems中,mPendingEventCount指定mPendingEventItems数组所存储的事件个数。而mPendingEventIndex指定尚未处理的epoll_event的索引
// Grab the next input event.
bool deviceChanged = false;
while (mPendingEventIndex < mPendingEventCount) {
const struct epoll_event& eventItem = mPendingEventItems[mPendingEventIndex++];
// 在这里分析每一个epoll_event 如果表示设备节点可读,则读取原始事件并放置到buffer中。如果表示mINotifyEd可读,则设置mPendingINotify为true,当InputReader将现有的输入事件都取出后读取mINotifyEd中的事件,并加装与卸载相应的设备。另外,如果此epoll_event表示wakeFds的读取端有数据可读,则设置awake标志为true,此时无论此次getEvents()调用是否取到事件,都不会调用epoll_wait()进行事件等待。
if (eventItem.data.u32 == EPOLL_ID_INOTIFY) {
if (eventItem.events & EPOLLIN) {
mPendingINotify = true;
} else {
ALOGW("Received unexpected epoll event 0x%08x for INotify.", eventItem.events);
}
continue;
}
...
//3 如果mINotifyFd有数据可读,说明设备节点发生了增删操作
if (mPendingINotify && mPendingEventIndex >= mPendingEventCount) {
mPendingINotify = false;
readNotifyLocked();
// 读取mINotifyFd中的事件,同时对输入设备进行相应的加载与卸载操作。这个操作必须当InputReader将现有输入事件读取并处理完毕后才能进行,因为现有的输入事件可能来自需要被卸载的输入设备,InputReader处理这些事件依赖于对应的设备信息
deviceChanged = true;
}
// 设备节点增删操作发生时,则重新执行循环体,以便将设备变化的事件放入buffer中
// Report added or removed devices immediately.
if (deviceChanged) {
continue;
}
// 如果此次getEvents()调用成功获取了一些事件,或者要求唤醒InputReader,则退出循环并结束getEvents()的调用,使InputReader可以立刻处理事件
// Return now if we have collected any events or if we were explicitly awoken.
if (event != buffer || awoken) {
break;
}
...
//4 如果此次getEvents()调用没能获取事件,说明mPendingEventItems中没有事件可用。于是执行epoll_wait()函数等待新的事件到来,将结果存储到mPendingEventItems里,并重置mPendingEventIndex为0
mPendingEventIndex = 0;
mLock.unlock(); // release lock before poll, must be before release_wake_lock
release_wake_lock(WAKE_LOCK_ID);
int pollResult = epoll_wait(mEpollFd, mPendingEventItems, EPOLL_MAX_EVENTS, timeoutMillis);
acquire_wake_lock(PARTIAL_WAKE_LOCK, WAKE_LOCK_ID);
mLock.lock(); // reacquire lock after poll, must be after acquire_wake_lock
if (pollResult == 0) {
// Timed out.
mPendingEventCount = 0;
break;
}
if (pollResult < 0) {
// An error occurred.
mPendingEventCount = 0;
// Sleep after errors to avoid locking up the system.
// Hopefully the error is transient.
if (errno != EINTR) {
ALOGW("poll failed (errno=%d)n", errno);
usleep(100000);
}
} else {
// 从epoll_wait()中得到新的事件后,重新循环,对新事件进行处理
// Some events occurred.
mPendingEventCount = size_t(pollResult);
}
}
// 返回本次getEvents()调用所读取的事件数量
// All done, return the number of events we read.
return event - buffer;
}
5.2.4 深入理解InputReader
前文讨论了,InputReader运行在InputReaderThread中,其线程循环像水泵一样,从EventHub中抽取事件,进行加工处理后将处理完成的事件注入InputDispatcher的派发队列中。
5.2.2介绍了InputReader的loopOnce()函数,此函数的工作有3步:
1 调用mEventHub.getEvents()获取事件列表
2 调用processEventsLocked()函数处理列表中的所有事件
3 调用mQueuedListener.flush()将事件注入InputDispatcher的派发队列中。
5.2.3 探讨了第一步getEvents()函数的工作原理。接下来探讨剩余两个步骤。
1 初识原始输入事件的加工
原始事件加工原理的入口是processEventsLocked()函数。
frameworks/native/services/inputflinger/reader/InputReader.cpp
[InputReader.cpp->InputReader::processEventsLocked()]
void InputReader::processEventsLocked(const RawEvent* rawEvents, size_t count) {
//遍历所有事件
for (const RawEvent* rawEvent = rawEvents; count;) {
int32_t type = rawEvent->type;
size_t batchSize = 1;
// 根据事件的类型区分原始输入事件或设备增删事件
if (type < EventHubInterface::FIRST_SYNTHETIC_EVENT) {
int32_t deviceId = rawEvent->deviceId;
while (batchSize < count) {
if (rawEvent[batchSize].type >= EventHubInterface::FIRST_SYNTHETIC_EVENT || rawEvent[batchSize].deviceId != deviceId) {
break;
}
batchSize += 1;
}
#if DEBUG_RAW_EVENTS
ALOGD("BatchSize: %zu Count: %zu", batchSize, count);
#endif
//执行processEventsForDeviceLocked()处理某一设备的一批事件
//batchSize表示属于此设备的输入事件的个数
processEventsForDeviceLocked(deviceId, rawEvent, batchSize);
} else {
//处理设备增删事件,先不讨论部分内容
switch (rawEvent->type) {
case EventHubInterface::DEVICE_ADDED:
addDeviceLocked(rawEvent->when, rawEvent->deviceId);
break;
case EventHubInterface::DEVICE_REMOVED:
removeDeviceLocked(rawEvent->when, rawEvent->deviceId);
break;
case EventHubInterface::FINISHED_DEVICE_SCAN:
handleConfigurationChangedLocked(rawEvent->when);
break;
default:
ALOG_ASSERT(false); // can't happen
break;
}
}
count -= batchSize;
rawEvent += batchSize;
}
}
processEventsLocked()会分别处理原始输入事件与设备增删事件。这里先不讨论设备增删事件的处理。对于原始输入事件,由于EventHub会将属于同一输入设备的原始输入事件放在一起,因此processEventsLocked()可以使processEventsForDeviceLocked()同时处理来自同一输入设备的一批事件。
[InputReader.cpp->InputReader::processEventsForDeviceLocked()]
void InputReader::processEventsForDeviceLocked(int32_t deviceId, const RawEvent* rawEvents,size_t count) {
//InputReader中也保存了一个mDevices字典,这个mDevices字典以设备Id为键存储了一系列的InputDevice对象。InputDevice是什么?
ssize_t deviceIndex = mDevices.indexOfKey(deviceId);
if (deviceIndex < 0) {
ALOGW("Discarding event for unknown deviceId %d.", deviceId);
return;
}
InputDevice* device = mDevices.valueAt(deviceIndex);
if (device->isIgnored()) {
// ALOGD("Discarding event for ignored deviceId %d.", deviceId);
return;
}
// 调用InputDevice的process()函数对这批事件进行处理
device->process(rawEvents, count);
}
出现了InputDevice类,先看下InputDevice::process()函数的实现
frameworks/native/services/inputflinger/reader/InputDevice.cpp
[InputDevice.cpp->InputDevice::process()]
void InputDevice::process(const RawEvent* rawEvents, size_t count) {
// Process all of the events in order for each mapper.
// We cannot simply ask each mapper to process them in bulk because mappers may
// have side-effects that must be interleaved. For example, joystick movement events and
// gamepad button presses are handled by different mappers but they should be dispatched
// in the order received.
for (const RawEvent* rawEvent = rawEvents; count != 0; rawEvent++) {
#if DEBUG_RAW_EVENTS
ALOGD("Input event: device=%d type=0x%04x code=0x%04x value=0x%08x when=%" PRId64,
rawEvent->deviceId, rawEvent->type, rawEvent->code, rawEvent->value, rawEvent->when);
#endif
if (mDropUntilNextSync) {
//处理事件同步错误
if (rawEvent->type == EV_SYN && rawEvent->code == SYN_REPORT) {
mDropUntilNextSync = false;
#if DEBUG_RAW_EVENTS
ALOGD("Recovered from input event buffer overrun.");
#endif
} else {
#if DEBUG_RAW_EVENTS
ALOGD("Dropped input event while waiting for next input sync.");
#endif
}
} else if (rawEvent->type == EV_SYN && rawEvent->code == SYN_DROPPED) {
ALOGI("Detected input event buffer overrun for device %s.", getName().c_str());
mDropUntilNextSync = true;
reset(rawEvent->when);
} else {
for (InputMapper* mapper : mMappers) {
//InputDevice中有一个InputMapper对象的列表,可以看出实际的输入事件处理位于InputMapper的process()函数中,这个mapper是什么?
mapper->process(rawEvent);
}
}
--count;
}
}
processEventsLocked()函数处理原始输入事件的逻辑比较简单,首先将属于同一设备的输入事件列表交由processEventsForDeviceLocked()处理,processEventsForDeviceLocked()再将事件列表交给InputDevice::process()处理,InputDevice::process()将事件逐个交给每一个InputMapper的process()事件处理。
InputDevice和InputMapper是什么?
原来,InputReader中也有一个类存储输入设备的信息,它就是InputDevice。与EventHub一样,InputDevice描述了一个输入设备,并且以设备Id为键保存在mDevices字典中。InputDevice类与EventHub的Device结构体类似,也保存了设备的Id、厂商信息以及设备所属的类别。它们之间的一个重要差别是,InputDevice相对于Device结构体多了一个InputMapper列表。
从上面的代码可以看到,InputMapper是InputReader中实际进行原始输入事件加工的场所,它有一系列的子类,分别用于加工不同类型的原始输入事件。而InputDevice的process()函数使用InputMapper的方式是一个简化了的职责链(chain of responsibility)设计模式。InputDevice不需要知道哪个InputMapper可以处理一个原始输入事件,只须将一个事件逐个交给每一个InputMapper尝试处理,如果InputMapper可以接受这个事件则处理它,否则什么都不做。
我们了解了InputReader处理原始输入事件的大致流程,而且知道了InputDevice和InputMapper二者在处理过程中起关键作用。因此在分析原始输入事件处理之前,要先清楚InputDevice和InputMapper从何而来?
2 InputDevice和InputMapper
InputDevice既然用来表示一个输入设备,因此InputDevice的创建与销毁操作与EventHub的设备增删事件有关。processEventsLocked()函数会根据DEVICE_ADDED事件调用addDeviceLocked()函数创建InputDevice,并会根据DEVICE_REMOVED事件调用removeDeviceLocked()将InputDevice删除。对于FINISHED_DEVICE_SCAN事件,InputReader会产生一个ConfigurationChanged事件并将其发给InputDispatcher。
1 InputDevice的创建
由于InputDevice中所保存的设备信息与EventHub的Device结构体差不多,因此InputDevice的创建过程的主体就是从EventHub中读取InputReader所感兴趣的设备信息,然后根据这些信息创建InputDevice对象。
看下addDevicesLocked()函数的实现
void InputReader::addDeviceLocked(nsecs_t when, int32_t deviceId) {
ssize_t deviceIndex = mDevices.indexOfKey(deviceId);
if (deviceIndex >= 0) {
ALOGW("Ignoring spurious device added event for deviceId %d.", deviceId);
return;
}
//1 从EventHub中获取厂商信息与设备类别
InputDeviceIdentifier identifier = mEventHub->getDeviceIdentifier(deviceId);
uint32_t classes = mEventHub->getDeviceClasses(deviceId);
int32_t controllerNumber = mEventHub->getDeviceControllerNumber(deviceId);
//2 通过createDeviceLocked()函数创建一个InputDevice对象
InputDevice* device = createDeviceLocked(deviceId, controllerNumber, identifier, classes);
//3 使用InputReader中保存的策略配置信息对新建的InputDevice进行策略配置,并通过reset()进行设备重置
device->configure(when, &mConfig, 0);
device->reset(when);
if (device->isIgnored()) {
ALOGI("Device added: id=%d, name='%s' (ignored non-input device)", deviceId,
identifier.name.string());
} else {
ALOGI("Device added: id=%d, name='%s', sources=0x%08x", deviceId,
identifier.name.string(), device->getSources());
}
//4 将设备放入mDevices字典中
mDevices.add(deviceId, device);
bumpGenerationLocked();
if (device->getClasses() & INPUT_DEVICE_CLASS_EXTERNAL_STYLUS) {
notifyExternalStylusPresenceChanged();
}
}
这个函数好理解,在代码3处,通过createDeviceLocked()创建InputDevice对象之后需要使用mConfig变量对其进行策略配置,注意是策略配置。mConfig的类型是InputReaderConfiguration,这一来自InputReaderPolicy的配置信息使得IMS以及应用程序得以在一定程度上影响输入事件的处理过程。在随后对原始输入事件加工过程的详细探讨中将会对这一策略配置所产生的影响进行介绍。
接下来看通过createDeviceLocked()的实现:
[InputReader.cpp->InputReader::createDeviceLocked()]
InputDevice* InputReader::createDeviceLocked(int32_t deviceId, int32_t controllerNumber,
const InputDeviceIdentifier& identifier, uint32_t classes) {
//1 根据设备Id、厂商信息以及设备类型创建一个InputDevice对象。InputDevice的构造函数保存了这些信息,并没有进行其他操作
InputDevice* device = new InputDevice(&mContext, deviceId, bumpGenerationLocked(),
controllerNumber, identifier, classes);
// External devices.
if (classes & INPUT_DEVICE_CLASS_EXTERNAL) {
device->setExternal(true);
}
// Devices with mics.
if (classes & INPUT_DEVICE_CLASS_MIC) {
device->setMic(true);
}
//2 后续代码将按照设备类型为InputDevice添加特定类型的InputMapper
// Switch-like devices.
if (classes & INPUT_DEVICE_CLASS_SWITCH) {
device->addMapper(new SwitchInputMapper(device));
}
...
return device;
}
InputDevice的创建过程就是如此。新建的InputDevice保存了设备Id 厂商信息,以及设备类型,并根据设备类型向InputDevice中添加了一系列各种类型的InputMapper。InputReader还会使用其当前的策略配置信息对InputDevice进行配置,使其事件处理过程能够根据IMS或应用程序的需求进行调整。
(2)InputMapper的分配
如前文所述,InputMapper完成了原始输入事件的加工处理,因此了解InputMapper的分配依据十分重要。
从createDeviceLocked()函数的实现可知,InputMapper的分配依据是设备类型,这个设备类型来自EventHub的Device结构体。在5.2.3节分析EventHub的设备管理相关内容时提到过Device结构体中所保存的设备类别的设置依据来自从设备节点中读取的事件位掩码。
...
3. Keyboard类型事件的加工处理
(4)按键事件的加工处理
根据扫描码获取虚拟键值以及功能后,KeyboardInputMapper::process()调用了processKey()函数对按键事件做进一步处理。
frameworks/native/services/inputflinger/reader/mapper/KeyboardInputMapper.cpp
[InputReader.cpp->KeyboardInputMapper::processKey()]
void KeyboardInputMapper::processKey(nsecs_t when, bool down, int32_t scanCode, int32_t usageCode) {
int32_t keyCode;
int32_t keyMetaState;
uint32_t policyFlags;
if (getEventHub()->mapKey(getDeviceId(), scanCode, usageCode, mMetaState, &keyCode,
&keyMetaState, &policyFlags)) {
keyCode = AKEYCODE_UNKNOWN;
keyMetaState = mMetaState;
policyFlags = 0;
}
// 当按下时,首先需要根据屏幕的方向对按键的虚拟键值进行旋转变换
if (down) {
// Rotate key codes according to orientation if needed.
if (mParameters.orientationAware) {
keyCode = rotateKeyCode(keyCode, getOrientation());
}
//2 从接下来的代码中可以看出,KeyboardInputMapper维护了一个KeyDown结构体的集合。当按键按下时会生成一个保存了扫描码与键值的KeyDown对象并添加到集合中。通过扫描码对集合的查找结果表明了按键是否是重复按下
// Add key down.
ssize_t keyDownIndex = findKeyDown(scanCode);
if (keyDownIndex >= 0) {
//对于重复按下的按键,需要确保后续的处理过程中的虚拟键值与第一次按下时一致,以免重复的过程中屏幕方向的变化导致虚拟键值的变化,使得后续InputDispatcher无法正常识别重复按键的动作
// key repeat, be sure to use same keycode as before in case of rotation
keyCode = mKeyDowns[keyDownIndex].keyCode;
} else {
// key down
if ((policyFlags & POLICY_FLAG_VIRTUAL) &&
mContext->shouldDropVirtualKey(when, getDevice(), keyCode, scanCode)) {
return;
}
if (policyFlags & POLICY_FLAG_GESTURE) {
mDevice->cancelTouch(when);
}
// 生成KeyDown结构体并添加到集合中
KeyDown keyDown;
keyDown.keyCode = keyCode;
keyDown.scanCode = scanCode;
mKeyDowns.push_back(keyDown);
}
mDownTime = when;
} else {
// Remove key down.
ssize_t keyDownIndex = findKeyDown(scanCode);
//3 对于抬起的按键,将对应的KeyDown对象从集合中移除
if (keyDownIndex >= 0) {
// key up, be sure to use same keycode as before in case of rotation
keyCode = mKeyDowns[keyDownIndex].keyCode;
mKeyDowns.erase(mKeyDowns.begin() + (size_t)keyDownIndex);
} else {
// key was not actually down
ALOGI("Dropping key up from device %s because the key was not down. "
"keyCode=%d, scanCode=%d",getDeviceName().c_str(), keyCode, scanCode);
//如果设备节点上报了一个并未按下的按键的抬起事件,这个事件将被忽略
return;
}
}
//4 接下来设置metaState 所谓metaState是指控制键的按下状态。控制键有左右shift、alt、ctrl、fn、capsLock、numlock、scrolllock等 当这些按键被按下或抬起时,mMetaState会将响应的位置1或置0
//updateMetaStateIfNeeded会检查此次按键是否为控制键并返回新的mMetaState
if (updateMetaStateIfNeeded(keyCode, down)) {
// If global meta state changed send it along with the key.
// If it has not changed then we'll use what keymap gave us,
// since key replacement logic might temporarily reset a few
// meta bits for given key.
keyMetaState = mMetaState;//保存新的mMetaState到keyMetaState
}
nsecs_t downTime = mDownTime;
// Key down on external an keyboard should wake the device.
// We don't do this for internal keyboards to prevent them from waking up in your pocket.
// For internal keyboards, the key layout file should specify the policy flags for
// each wake key individually.
// TODO: Use the input device configuration to control this behavior more finely.
if (down && getDevice()->isExternal() && !isMediaKey(keyCode)) {
policyFlags |= POLICY_FLAG_WAKE;
}
if (mParameters.handlesKeyRepeat) {
policyFlags |= POLICY_FLAG_DISABLE_KEY_REPEAT;
}
//5 将所有的按键事件信息封装为NotifyKeyArgs对象,并将此对象通知给listener
NotifyKeyArgs args(mContext->getNextSequenceNum(), when, getDeviceId(), mSource, getDisplayId(), policyFlags, down ? AKEY_EVENT_ACTION_DOWN : AKEY_EVENT_ACTION_UP,
AKEY_EVENT_FLAG_FROM_SYSTEM, keyCode, scanCode, keyMetaState, downTime);
getListener()->notifyKey(&args);
}
按键事件的加工很简单,主要动作就是根据屏幕旋转方向进行虚拟键值的变换,设置metaState以及容错处理。
进行变换的目的是使得按键动作能够体现其在某个屏幕方向下的实际意义(如方向键)这个变换过程是通过定义在nputReader.cpp中的一个名为keyCodeRotationMap的二维数组完成。
metaState描述了功能键的按下状态。事件的接受者往往需要根据ctrl、alt、shift等按键的按下状态对输入事件做特殊处理(例如C键的metaState为ctrl按下时进行复制操作),因此,metaState是输入事件的一个重要信息。另外不止是按键事件,其他类型事件也要携带metaState信息。因此updateMetaStateIfNeeded中会调用getContext()->updateGlobalMetaState()将最新产生的metaState更新至InputReader的Context中,当其他的InputMapper产生事件时可以从context里获取最新的metaState并添加到自己的事件信息里。
bool KeyboardInputMapper::updateMetaStateIfNeeded(int32_t keyCode, bool down) {
int32_t oldMetaState = mMetaState;
int32_t newMetaState = android::updateMetaState(keyCode, down, oldMetaState);
bool metaStateChanged = oldMetaState != newMetaState;
if (metaStateChanged) {
mMetaState = newMetaState;
updateLedState(false);
getContext()->updateGlobalMetaState();
}
return metaStateChanged;
}
容错处理主要是为了处理重复的按下与抬起事件。
在代码最后调用getListener()->notifyKey(&args)函数。到达这个调用时,按键事件已经在InputReader中加工完毕,准备提交给InputDispatcher进行派发。这个调用就是提交给InputDispatcher的第一步。在本节5.2.4的第5小节将介绍输入事件从InputReader提交到nputDispatcher的过程。
(5)keyboard类型事件的处理小结
本节主要介绍KeyboardInputMapper的加工过程。keyboard类型事件比较简单,一条原始输入事件即可完整地描述一次按键动作,因此KeyboardInputMapper的加工过程也比较简单。
本节关键点:
1 扫描码与虚拟键值的概念与区别
2 扫描码到虚拟键值的映射过程
3 虚拟键值根据屏幕方向进行变换的过程与意义
4 对于重复按下与重复抬起事件的容错处理
4 Touch类型事件的加工处理
虽然Touch类型事件与Keyboard类型事件的处理共用一套InputMapper框架,但是由于Touch类型事件所包含的信息量多很多,因此其处理过程也复杂很多。这一节探讨Touch类型原始输入事件的处理原理,以及Android处理复杂输入事件的方式。
(1)Touch类型事件的信息与原始事件的组织方式
Touch类型事件描述了用户的一次点击操作,对应于EV_ABS类型的原始输入事件。点击的对象可以是触摸屏、触控板等,我们可以统称为传感器,而发生点击动作的可以是手指,触控笔等工具。
android输入系统对一次点击操作的描述信息的支持非常多,对一次点击操作的完整描述信息包括以下内容:
1 点击坐标,描述点击操作的坐标位置。和点击坐标有关的传感器属性包括坐标精度(传感器的密集程度)、坐标范围(传感器总面积)以及噪音漂移(传感器的抗噪能力)等。
2点击区域,无论是手指还是触控笔,落在传感器上不可能是一个点,而是一个区域。如果支持点击区域的识别,输入设备使用一个长轴(Major Axis)及短轴(Minor Axis)表示的椭圆来近似地描述触摸工具落在传感器上的区域。
3 触摸工具到传感器的距离,有些高级的传感器可以识别悬停在其上的触摸工具,因此对于这种设备,触摸工具到传感器的距离是一项必要的信息。
4 压力,高级的传感器可以识别触摸工具施加在传感器上的压力大小
5 触摸工具的类型,有些传感器可以对手指及触控笔作出区分。
由于一次点击动作包含如此多信息,因此一条原始输入事件是无法完整描述的。为了解决这个问题。Linux的输入子系统使用多个原始输入事件对一次复杂的输入事件进行描述,其中每一个事件描述一项信息,并在完整描述设备所产生的所有信息之后,使用特殊类型的事件标识一次输入事件上报的完成。
举例,在android设备上完成一次移动,可以通过getevent工具得到以下三条原始事件的输出:
[278.791198] /dev/input/event0: 0003 0000 0000 00b6
[278.791271] /dev/input/event0: 0003 0001 0000 0229
[278.791313] /dev/input/event0: 0000 0000 0000 0000
前两条的事件类型为0x03,即EV_ABS,表示它描述点击事件的一条信息。其Code字段的值为ABS_X(0x00)与ABS_Y(0x01),表示他们携带X坐标和Y坐标的信息。而第三条的事件类型为EV_SYN(0x00),并且其Code为SYN_REPORT,即所谓的表示事件上报完成可以进行派发的特殊类型的事件。
因此,Touch类型事件的加工处理其实就是收集所有描述点击动作的原始输入事件所包含的各种新信息,并在EV_SYN类型事件到来时,将这些信息进行整合,在进行一些处理之后,交付给InputDispatcher进行分发。
(2)TouchInputMapper的体系
负责处理点击事件的InputMapper是TouchInputMapper。由于点击事件的信息量大,并且还分为单点与多点两种类型,TouchInputMapper的组成与逻辑相比KeyboardInputMapper要复杂多。首先要了解TouchInputMapper的体系结构与参与者的作用,如图5-10
nputDispatcher。
2 MultiTouchInputMapper,主要是为了实现syncTouch()函数,目的是将MultiTouchMotionAccumlator收集到的点击事件信息存储在mCurrentRawPointerData中。另外MultiTouchInputMapper也负责从EventHub中获取与多点触控相关的设备配置信息,如所支持的触控点数量以及各项信息的取值范围、精度、噪音偏移量等指标。
3 MultiTouchMotionAccumlator,多点触控信息的累加器。它负责接收处理每条EV_ABS事件,并将事件中携带的点击信息保存下来,并在EV_SYN事件到来时提供给MultiTouchInputMapper。
4 SingleTouchInputMapper与SingleTouchMotionAccumulator,负责单点触控事件的收集与整合工作。其工作方式与多点触控类似。
本节将会以多点触控的事件解析与整合为例,介绍TouchInputMapper的工作原理。
(3)MultiTouchInputMapper的配置
MultiTouchInputMapper及其累加器的构造函数没什么内容。事件信息收集与整合所需要的信息的获取位于其配置过程中,也就是其基类TouchInputMapper的configure()函数中。
TouchInputMapper的configure()函数的调用时机同KeyboardInputMapper一样。在TouchInputMapper被创建之后对configure()的第一次调用中,TouchInputMapper会调用由子类实现的configureRawPointerAxes()函数用以获取由设备节点提供的技术信息。这些技术信息包括输入设备所支持的触控点数量以及各项信息的取值范围、精度、干扰偏移量等指标。它们是通过EventHub,使用ioctl的方式获取的。一项信息的技术指标由RawAbsoluteAxisInfo结构体描述。其定义如下:
frameworks/native/services/inputflinger/reader/include/EventHub.h
[EventHub.h->RawAbsoluteAxisInfo]
/* Describes an absolute axis. */
struct RawAbsoluteAxisInfo {
//此项信息是否受到输入设备的支持
bool valid; // true if the information is valid, false otherwise
//此项信息的最小值
int32_t minValue; // minimum value
//此项信息的最大值
int32_t maxValue; // maximum value
//以下三个字段为精度信息
int32_t flat; // center flat position, eg. flat == 8 means center is between -8 and 8
//容错范围。表示因干扰所导致的最大偏移量
int32_t fuzz; // error tolerance, eg. fuzz == 4 means value is +/- 4 due to noise
//精度,表示在1毫米的范围中的解析点的数量
int32_t resolution; // resolution in units per mm or radians per mm
inline void clear() {
valid = false;
minValue = 0;
maxValue = 0;
flat = 0;
fuzz = 0;
resolution = 0;
}
};
例如,对设备的X坐标这项信息而言,RawAbsoluteAxisInfo的minValue和maxValue表示了事件上报的x坐标范围,resolution表示传感器在每毫米距离中可以产生的x坐标点的数量(ppm)。x坐标和y坐标的这些技术指标构建了传感器的物理坐标系。而对压力值这项信息而言,如果其RawAbsoluteAxisInfo的valid值为false,则表示此设备不支持识别压力值。当然,这些字段并不是对所有类型的信息都有效,例如对于传感器所支持的触控点数量这项信息,仅有valid和maxValue两个字段有效,valid为true表示设备支持通过slot协议进行触控点索引的识别,而maxValue则表示最大触控点的数量为maxValue+1。
所有信息的技术指标被ultiTouchInputMapper保存在它的一个名为mRawPointerAxes的RawPointerAxes结构体中。
另外,TouchInputMapper的configure()调用的configureSurface函数会通过DisplayViewPort获取屏幕的方向以及屏幕坐标系的信息。屏幕方向的不同会导致触控位置的x和y两个方向的颠倒或交换。通过计算屏幕坐标系与传感器物理坐标系之间的差异,使得在事件到来时可以将点击位置从传感器的物理坐标系转换到屏幕坐标系中。
读者可以从键盘的扫描码与虚拟键值的关系中类比地理解物理坐标系与屏幕坐标系的概念。EV_ABS类型的原始输入事件上报的坐标系是传感器的物理坐标系,其范围与设备的硬件实现有关,并且与屏幕的实际坐标系有较大出入。举个极端例子,一个做得很粗糙的触摸屏在x方向上的分辨率仅为10,当点击发生在屏幕右边缘时,其原始输入事件上报的x坐标为10,这是输入设备的物理坐标。然而手机所配备的屏幕在x方向上的分辨率为1920,这是屏幕坐标。因此必须将物理坐标值10根据两个坐标系的比例与位置关系,换为屏幕坐标系下的1920才能使用。
总结下MultiTouchInputMapper的配置内容:
1 MultiTouchInputMapper的configureRawPointerAxes()函数获取来自设备节点的各项触控信息的技术指标。同时,这些指标构建了传感器的物理坐标系。
2 TouchInputMapper的configureSurface()函数获取来自DisplayViewPort的屏幕方向以及屏幕坐标系的信息,并计算物理坐标系到屏幕坐标系的差异信息。
(4)点击事件信息的收集
看下MultiTouchInputMapper的process()函数的实现:
frameworks/native/services/inputflinger/reader/mapper/MultiTouchInputMapper.cpp
[MultiTouchInputMapper.cpp->MultiTouchInputMapper::process()]
void MultiTouchInputMapper::process(const RawEvent* rawEvent) {
//调用基类的process()函数。基类的process()函数主要处理EV_SYN事件并进行事件信息整合
TouchInputMapper::process(rawEvent);
//转而调用mMultiTouchMotionAccumulator::process()进行事件信息收集
mMultiTouchMotionAccumulator.process(rawEvent);
}
这个函数首先调用基类的process()函数,用以处理EV_SYN事件,下一节再详细讨论这个处理。由于事件信息的收集工作交由mMultiTouchMotionAccumulator类的process()函数完成。在多点触控事件的信息收集工作中,需要识别信息所属的触控点索引。如何识别触控点的索引有两种方式:基于slot协议的显示识别方式与基于事件顺序的隐式识别方式。我们先分析显式识别下的信息收集方式,然后再探讨隐式识别与显式识别的区别。
[MultiTouchInputMapper.cpp->MultiTouchInputMapper::process()]
void MultiTouchMotionAccumulator::process(const RawEvent* rawEvent) {
//1 点击原始输入事件的类型为EV_ABS
if (rawEvent->type == EV_ABS) {
bool newSlot = false;
if (mUsingSlotsProtocol) {
if (rawEvent->code == ABS_MT_SLOT) {
//2 代码为ABS_MT_SLOT的事件指明后续事件对应的触控点的索引
mCurrentSlot = rawEvent->value;
newSlot = true;
}
} else if (mCurrentSlot < 0) {
mCurrentSlot = 0;
}
if (mCurrentSlot < 0 || size_t(mCurrentSlot) >= mSlotCount) {
#if DEBUG_POINTERS
//越界
if (newSlot) {
ALOGW("MultiTouch device emitted invalid slot index %d but it "
"should be between 0 and %zd; ignoring this slot.",
mCurrentSlot, mSlotCount - 1);
}
#endif
} else {
//3 根据触控点的索引获取一个slot对象。随后所收集的信息将放置在这个slot对象中
Slot* slot = &mSlots[mCurrentSlot];
//4 提取事件中携带的信息,并存储在触控点所对应的slot对象中
switch (rawEvent->code) {
case ABS_MT_POSITION_X:
//设置mInUse为true表示这个slot中包含有效信息
slot->mInUse = true;
//将信息保存在slot相应的字段里
slot->mAbsMTPositionX = rawEvent->value;
break;
case ABS_MT_POSITION_Y:
slot->mInUse = true;
slot->mAbsMTPositionY = rawEvent->value;
break;
case ABS_MT_TOUCH_MAJOR:
slot->mInUse = true;
slot->mAbsMTTouchMajor = rawEvent->value;
break;
case ABS_MT_TOUCH_MINOR:
slot->mInUse = true;
slot->mAbsMTTouchMinor = rawEvent->value;
slot->mHaveAbsMTTouchMinor = true;
break;
case ABS_MT_WIDTH_MAJOR:
slot->mInUse = true;
slot->mAbsMTWidthMajor = rawEvent->value;
break;
case ABS_MT_WIDTH_MINOR:
slot->mInUse = true;
slot->mAbsMTWidthMinor = rawEvent->value;
slot->mHaveAbsMTWidthMinor = true;
break;
case ABS_MT_ORIENTATION:
slot->mInUse = true;
slot->mAbsMTOrientation = rawEvent->value;
break;
case ABS_MT_TRACKING_ID:
if (mUsingSlotsProtocol && rawEvent->value < 0) {
// The slot is no longer in use but it retains its previous contents,
// which may be reused for subsequent touches.
slot->mInUse = false;
} else {
slot->mInUse = true;
slot->mAbsMTTrackingId = rawEvent->value;
}
break;
case ABS_MT_PRESSURE:
slot->mInUse = true;
slot->mAbsMTPressure = rawEvent->value;
break;
case ABS_MT_DISTANCE:
slot->mInUse = true;
slot->mAbsMTDistance = rawEvent->value;
break;
case ABS_MT_TOOL_TYPE:
slot->mInUse = true;
slot->mAbsMTToolType = rawEvent->value;
slot->mHaveAbsMTToolType = true;
break;
}
}
} else if (rawEvent->type == EV_SYN && rawEvent->code == SYN_MT_REPORT) {
// MultiTouch Sync: The driver has returned all data for *one* of the pointers.
mCurrentSlot += 1;
} else if (rawEvent->type == EV_MSC && rawEvent->code == MSC_TIMESTAMP) {
mDeviceTimestamp = rawEvent->value;
}
}
MultiTouchMotionAccumulator保存了一个名为mSlots的Slot对象数组。Slot对象是收集特定触控点的点击信息的场所,而其在数组中的索引就是触控点的Id。从这段代码中看到,当输入设备开始上报某一个触控点的信息时,首先会发送一条类型为EV_ABS,代码为ABS_MT_SLOT的事件(后面以类型+代码的方式称呼这类事件),这个事件的值表明随后携带点击信息的事件属于哪个触控点。携带信息的事件到来时,其事件代码指明了它所携带的信息类型,MultiTouchMotionAccumulator从mSlots数组中以触控点的id为索引将对应的Slot对象取出,并将信息值保存在Slot的对应字段中。随着携带信息的事件不断到来,slot对象中的信息也丰满起来。当设备将这个触控点的点击信息全部上报完毕后,会发送一个EV_SYN+SYN_REPORT事件启动TouchInputMapper的信息整合与加工工作,或者发送另一个事件代码为ABS_MT_SLOT的事件,开始上报另一个触控点的信息。
由此可见,携带点击信息的原始输入事件的格式如下:
1 Type 取值为EV_ABS 表明属于点击事件
2 Code取值为ABS_MT_XXX 其中的XXX表明了信息的类型
3 Value 信息的值
而在一个点击信息收集过程中所收到的原始输入事件序列如下所示:
EV_ABS ABS_MT_SLOT 触控点1
EV_ABS 信息1 信息1的值
EV_ABS 信息2 信息2的值
EV_ABS ABS_MT_SLOT 触控点2
......// 触控点2的信息以及更多的触控点
EV_SYN SYN_REPORT
因此整个点击事件信息的收集过程从指定触控点的事件开始,以一条EV_SYN SYN_REPORT事件结束。在这个过程后MultiTouchMotionAccumlator便在mSlots数组中保存了一个或多个触控点的点击信息。
这种通过ABS_MT_SLOT 事件代码由设备显示地指明触控点的方式称为显式识别方式,也叫slot协议方式。这种方式的好处是灵活多变,而坏处是,当只有0号触控点处于活动状态时(也就是单点操作)仍需要一个ABS_MT_SLOT 事件,这是一种浪费。
另外一种识别方式为隐式识别。在隐式识别方式下,代表触控点信息的事件组由EV_SYN SYN_MT_REPORT的事件分隔(注意不是标志开始进行信息整合的代码SYN_REPORT)。分割的第一批事件携带的信息被认为属于0号索引点的触控点,随后事件组所属的触控点索引依次增加1.最后,使用EV_SYN SYN_REPORT事件通知开始进行信息的整合工作。因此,隐式识别下的原始输入事件序列如下:
EV_ABS 信息1 信息1的值 //0号触控点的信息
EV_ABS 信息2 信息2的值
EV_SYN SYN_MT_REPORT
...... //1号触控点的信息
EV_SYN SYN_MT_REPORT
EV_ABS 信息1 信息1的值 //2号触控点的信息
EV_SYN SYN_REPORT
隐式识别的好处在于,当单点操作时,不需要额外的ABS_MT_SLOT事件对触控点索引进行指明,从而减少单点操作情况下的原始输入事件数量与处理时间。而缺点也很明显,在多点操作过程中,如果低索引值的触控点陆续抬起,仅剩下一个高索引值的触控点处于活动状态时,在上报其信息之前,仍需相应数量EV_SYN SYN_MT_REPORT事件进行补齐,这也是一种浪费。
因此两种方式各有利弊,一个多点触控设备采用哪种方式应当根据其应用场景进行适合选择。
(5)点击事件信息的整合、变换与高级事件的生成
从前面所述,事件信息的整合加工工作由EV_SYN SYN_REPORT触发。看一下TouchInputMapper的process()函数:
frameworks/native/services/inputflinger/InputReader.cpp [InputReader.cpp->TouchInputMapper::process()]
void TouchInputMapper::process(const RawEvent* rawEvent) {
mCursorButtonAccumulator.process(rawEvent);
mCursorScrollAccumulator.process(rawEvent);
mTouchButtonAccumulator.process(rawEvent);
if (rawEvent->type == EV_SYN && rawEvent->code == SYN_REPORT) {
//通过sync()函数开始整合
sync(rawEvent->when);
}
}
再看下sync()函数的实现,这个函数比较长而且复杂,因为它需要整合加工触摸、笔触(stylus)以及悬停的多种事件。为了简单起见,只保留了与触摸事件相关的代码:
void