Input子系统框架之深入理解EventHub

1、概述

InputReaderThread继承自C++的Thread类,Thread类封装了pthread线程工具,提供了与Java层Thread类相似的API。C++的Thread类提供了一个名为threadLoop()的纯虚函数,当线程开始运行后,将会在内建的线程循环中不断地调用threadLoop(),直到此函数返回false,则退出线程循环,从而结束线程。 InputReaderThread启动后,其线程循环将不断地执行InputReader.loopOnce()函数。因此这个loopOnce()函数作为线程循环的循环体包含了InputReader的所有工作。接下来详细说明EventHub。
    首先从EventHub中抽取未处理的事件列表。这些事件分为两类,一类是从设备节点中读取的原始输入事件,另一类则是输入设备可用性变化事件,简称为设备事件。

 [->frameworks/native/services/inputflinger/InputReader.cpp]

 void InputReader::loopOnce() {
	......
	/* 通过EventHub抽取事件列表。读取的结果存储在参数mEventBuffer中,返回值表示事件的个数
	当EventHub中无事件可以抽取时,此函数的调用将会阻塞直到事件到来或者超时 */
	size_tcount = mEventHub->getEvents(timeoutMillis,mEventBuffer, EVENT_BUFFER_SIZE);
	......
}

以下是一张EventHub->getEvents()工作时序图,后面将跟着时序图一步步介绍。
在这里插入图片描述

2、设备节点监听的建立

frameworks/native/services/inputflinger/EventHub.cpp
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读取事件的详细信息
	//static const char *DEVICE_PATH = "/dev/input";
	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));
	eventItem.events = EPOLLIN;// 监听mINotifyFd可读
	eventItem.data.u32 = EPOLL_ID_INOTIFY; // 注意这里并没有使用fd字段,而使用了自定义的值EPOLL_ID_INOTIFY
	result = epoll_ctl(mEpollFd, EPOLL_CTL_ADD, mINotifyFd, &eventItem);// 将对mINotifyFd的监听注册到epoll对象中
	LOG_ALWAYS_FATAL_IF(result != 0, "Could not add INotify to epoll instance.  errno=%d", errno);
	
	//在构造函数剩余的代码中,EventHub创建了一个名为wakeFds的匿名管道,并将管道读取端的描述符
	//的可读事件注册到epoll对象中。因为InputReader在执行getEvents()时会因无事件而导致其线程
	//阻塞在epoll_wait()的调用里,然而有时希望能够立刻唤醒InputReader线程使其处理一些请求。此
	//时只需向wakeFds管道的写入端写入任意数据,此时读取端有数据可读,使得epoll_wait()得以返回
	//从而达到唤醒InputReader线程的目的
	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;
	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的监听工作分为设备节点和原始输入事件两个方面,接下来将深入探讨这两方面的内容。

3、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.h–>RawEvent]
struct RawEvent {
    nsecs_t when;             /* 发生事件时的时间戳 */
    int32_t deviceId;        //产生事件的设备Id,它是由EventHub自行分配的,InputReader
                            //以根据它从EventHub中获取此设备的详细信息
    int32_t type;             /* 事件的类型 */
    int32_t code;             /* 事件代码 */
    int32_t value;            /* 事件值 */
    };

可以看出,RawEvent结构体与getevent工具的输出十分一致,包含了原始输入事件的四个基本元素,因此用RawEvent结构体表示原始输入事件是非常直观的。RawEvent同时也用来表示设备增删事件,为此,EventHub定义了三个特殊的事件类型DEVICE_ADD、DEVICE_REMOVED以及FINISHED_DEVICE_SCAN,用以与原始输入事件进行区别。

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。

4、输入设备管理

    因为输入设备是输入事件的来源,并且决定了输入事件的含义,因此首先讨论EventHub的输入设备管理机制。

    输入设备是一个可以为接收用户操作的硬件,内核会为每一个输入设备在/dev/input/下创建一个设备节点,而当输入设备不可用时(例如被拔出),将其设备节点删除。这个设备节点包含了输入设备的所有信息,包括名称、厂商、设备类型,设备的功能等。除了设备节点,某些输入设备还包含一些自定义配置,这些配置以键值对的形式存储在某个文件中。这些信息决定了Reader子系统如何加工原始输入事件。EventHub负责在设备节点可用时加载并维护这些信息,并在设备节点被删除时将其移除。

    EventHub通过一个定义在其内部的名为Device的私有结构体来描述一个输入设备。其定义如下:

[EventHub.h–>EventHub::Device]

struct Device {
	Device* next;  /* Device结构体实际上是一个单链表 */
	int fd;         /* fd表示此设备的设备节点的描述符,可以从此描述符中读取原始输入事件 */
	const int32_t id;     /* id在输入系统中唯一标识这个设备,由EventHub在加载设备时进行分配 */
	const String8 path; /* path存储了设备节点在文件系统中的路径 */
	const InputDeviceIdentifier identifier; /* 厂商信息,存储了设备的供应商、型号等信息
													这些信息从设备节点中获得 */
	uint32_t classes;  /* classes表示了设备的类别,键盘设备,触控设备等。一个设备可以同时属于
							多个设备类别。类别决定了InputReader如何加工其原始输入事件 */
	/* 接下来是一系列的事件位掩码,它们详细地描述了设备能够产生的事件类型。设备能够产生的事件类型
	决定了此设备所属的类型*/
	uint8_t keyBitmask[(KEY_MAX + 1) / 8];
	......
	/* 配置信息。以键值对的形式存储在一个文件中,其路径取决于identfier字段中的厂商信息,这些
	配置信息将会影响InputReader对此设备的事件的加工行为 */
	String8 configurationFile;
	PropertyMap* configuration;
	/* 键盘映射表。对于键盘类型的设备,这些键盘映射表将原始事件中的键盘扫描码转换为Android定义的
	的按键值。这个映射表也是从一个文件中加载的,文件路径取决于dentifier字段中的厂商信息 */
	VirtualKeyMap* virtualKeyMap;
	KeyMap keyMap;
	sp<KeyCharacterMap> overlayKeyMap;
	sp<KeyCharacterMap> combinedKeyMap;
	// 力反馈相关的信息。有些设备如高级的游戏手柄支持力反馈功能,目前暂不考虑
	bool ffEffectPlaying;
	int16_t ffEffectId;
};

Device结构体所存储的信息主要包括以下几个方面:

  • 设备节点信息:保存了输入设备节点的文件描述符、文件路径等。
  • 厂商信息:包括供应商、设备型号、名称等信息,这些信息决定了加载配置文件与键盘映射表的路径。
  • 设备特性信息:包括设备的类别,可以上报的事件种类等。这些特性信息直接影响了InputReader对其所产生的事件的加工处理方式。
  • 设备的配置信息:包括键盘映射表及其他自定义的信息,从特定位置的配置文件中读取。

EventHub用一个名为mDevices的字典保存当前处于打开状态的设备节点的Device结构体。字典的键为设备Id。

4.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事件 */
	struct epoll_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);
	return 0;
}

openDeviceLocked()函数打开指定路径的设备节点,为其创建并填充Device结构体,然后将设备节点的可读事件注册到Epoll中,最后将新建的Device结构体添加到mDevices字典中以供检索之需。整个过程比较清晰,但仍有以下几点需要注意:

  • openDeviceLocked()函数从设备节点中获取了设备可能上报的事件类型,并据此为设备分配了类别。整个分配过程非常繁琐。
  • 向Epoll注册设备节点的可读事件时,epoll_event的自定义数据被设置为设备的Id而不是fd。
  • addDeviceLocked()将新建的Device对象添加到mDevices字典中的同时也会将其添加到一个名为mOpeningDevices的链表中。这个链表保存了刚刚被加载,但尚未通过getEvents()函数向InputReader发送DEVICE_ADD事件的设备。

完成输入设备的加载之后,通过getEvents()函数便可以读取到此设备所产生的输入事件了。除了在getEvents()函数中使用scanDevicesLockd()一次性加载所有输入设备,当INotify事件告知有新的输入设备节点被创建时,也会通过opendDeviceLocked()将设备加载,稍后再做讨论。

4.2、输入设备的卸载

输入设备的卸载由closeDeviceLocked()函数完成。由于此函数的工作内容与openDeviceLocked()函数正好相反,就不列出其代码了。设备的卸载过程为:

  • 从Epoll中注销对描述符的监听。
  • 关闭设备节点的描述符。
  • 从mDevices字典中删除对应的Device对象。
  • 将Device对象添加到mClosingDevices链表中,与mOpeningDevices类似,这个链表保存了刚刚被卸载,但尚未通过getEvents()函数向InputReader发送DEVICE_REMOVED事件的设备。

同加载设备一样,在getEvents()函数中有根据需要卸载所有输入设备的操作(比如当EventHub要求重新加载所有设备时,会先将所有设备卸载)。并且当INotify事件告知有设备节点删除时也会调用closeDeviceLocked()将设备卸载。

4.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。
图:设备增删事件的完整序列
由于参数buffer的容量限制,这个事件序列可能需要通过多次getEvents()调用才能完整地返回给调用者。设备增删事件相对于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.4、通过INotify动态地加载与卸载设备

通过前文的介绍知道了openDeviceLocked()和closeDeviceLocked()可以加载与卸载输入设备。接下来分析EventHub如何通过INotify进行设备的动态加载与卸载。在EventHub的构造函数中创建了一个名为mINotifyFd的INotify对象的描述符,用以监控/dev/input下设备节点的增删。之后将mINotifyFd的可读事件加入到Epoll中。于是可以确定动态加载与卸载设备的工作方式为:首先筛选epoll_wait()函数所取得的Epoll事件,如果Epoll事件表示了mINotifyFd可读,便从mINotifyFd中读取设备节点的增删事件,然后通过执行openDeviceLocked()或closeDeviceLocked()进行设备的加载与卸载。

看一下getEvents()中与INotify相关的代码:

[EventHub.cpp–>EventHub::getEvents()]

size_t EventHub::getEvents(int timeoutMillis,RawEvent* buffer, size_t bufferSize) {
for (;;){
   ...... // 设备增删事件处理
    while(mPendingEventIndex < mPendingEventCount) {
       const struct epoll_event& eventItem =
                             mPendingEventItems[mPendingEventIndex++];
       /* ① 通过Epoll事件的data字段确定此事件表示了mINotifyFd可读
          注意EPOLL_ID_INOTIFY在EventHub的构造函数中作为data字段向
          Epoll注册mINotifyFd的可读事件 */
       if (eventItem.data.u32 == EPOLL_ID_INOTIFY) {
           if (eventItem.events & EPOLLIN) {
               mPendingINotify = true; // 标记INotify事件待处理
           } else { ...... }
           continue; // 继续处理下一条Epoll事件
       }
       ...... // 其他Epoll事件的处理
    }
    // 如果INotify事件待处理
    if(mPendingINotify && mPendingEventIndex >= mPendingEventCount) {
       mPendingINotify = false;
       /* ② 调用readNotifyLocked()函数读取并处理存储在mINotifyFd中的INotify事件
          这个函数将完成设备的加载与卸载 */
       readNotifyLocked();
       deviceChanged = true;
    }
    //③ 如果处理了INotify事件,则返回到循环开始处,生成设备增删事件
    if(deviceChanged) {
       continue;
    }
}
}

getEvents()函数中与INotify相关的代码共有三处:

  • 识别表示mINotifyFd可读的Epoll事件,并通过设置mPendingINotify为true以标记有INotify事件待处理。getEvents()并没有立刻处理INotify事件,因为此时进行设备的加载与卸载是不安全的。其他Epoll事件可能包含了来自即将被卸载的设备的输入事件,因此需要将所有Epoll事件都处理完毕后再进行加载与卸载操作。
  • 当epoll_wait()所返回的Epoll事件都处理完毕后,调用readNotifyLocked()函数读取mINotifyFd中的事件,并进行设备的加载与卸载操作。
  • 完成设备的动态加载与卸载后,需要返回到循环最开始处,以便设备增删事件处理代码生成设备的增删事件。

其中第一部分与第三部分比较容易理解。接下来看一下readNotifyLocked()是如何工作的。

[EventHub.cpp–>EventHub::readNotifyLocked()]

status_t EventHub::readNotifyLocked() {

    ......
    // 从mINotifyFd中读取INotify事件列表
    res =read(mINotifyFd, event_buf, sizeof(event_buf));
    ......
    // 逐个处理列表中的事件
       while(res >= (int)sizeof(*event)) {
       strcpy(filename, event->name); // 从事件中获取设备节点路径
       if(event->mask & IN_CREATE) {
           openDeviceLocked(devname); // 如果事件类型为IN_CREATE,则加载对应设备
        }else {
           closeDeviceByPathLocked(devname); // 否则卸载对应设备
        }
        ......// 移动到列表中的下一个事件
    }
    return0;
    }

4.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事件
}
return event – buffer;
}

getEvents()通过Epoll事件的data.u32字段在mDevices列表中查找已加载的设备,并从设备的文件描述符中读取原始输入事件列表。从文件描述符中读取的原始输入事件存储在input_event结构体中,这个结构体的四个字段存储了事件的事件戳、类型、代码与值四个元素。然后逐一将input_event的数据转存到RawEvent中并保存至buffer以返回给调用者。

注意为了叙述简单,上述代码使用了调用getEvents()的时间作为输入事件的时间戳。由于调用getEvents()函数的时机与用户操作的时间差的存在,会使得此时间戳与事件的真实时间有所偏差。从设备节点中读取的input_event中也包含了一个时间戳,这个时间戳消除了getEvents()调用所带来的时间差,因此可以获得更精确的时间控制。可以通过打开HAVE_POSIX_CLOCKS宏以使用input_event中的时间而不是将getEvents()调用的时间作为输入事件的时间戳。

需要注意的是,由于Epoll事件的处理优先级低于设备增删事件,因此当发生设备加载与卸载动作时,不会产生设备输入事件。另外还需注意,在一个监听周期中,getEvents()在将一个设备节点中的所有原始输入事件读取完毕之前,不会读取其他设备节点中的事件。

5、EventHub总结

本节针对EventHub的设备管理与原始输入事件的监听读取两个核心内容介绍了EventHub的工作原理。EventHub作为直接操作设备节点的输入系统组件,隐藏了INotify与Epoll以及设备节点读取等底层操作,通过一个简单的接口getEvents()向使用者提供抽取设备事件与原始输入事件的功能。EventHub的核心功能都在getEvents()函数中完成,因此深入理解getEvents()的工作原理对于深入理解EventHub至关重要。

getEvents()函数的本质是通过epoll_wait()获取Epoll事件到事件池,并对事件池中的事件进行消费的过程。从epoll_wait()的调用开始到事件池中最后一个事件被消费完毕的过程称之为EventHub的一个监听周期。由于buffer参数的尺寸限制,一个监听周期可能包含多个getEvents()调用。周期中的第一个getEvents()调用一定会因事件池的枯竭而直接进行epoll_wait(),而周期中的最后一个getEvents()一定会将事件池中的最后一条事件消费完毕并将事件返回给调用者。前文所讨论的事件优先级都是在同一个监听周期内而言的。

在本节中出现了很多种事件,有原始输入事件、设备增删事件、Epoll事件、INotify事件等,存储事件的结构体有RawEvent、epoll_event、inotify_event、input_event等。图5-6可以帮助读者理清这些事件之间的关系。
图 5-6 EventHub的事件关联
另外,getEvents()函数返回的事件列表依照事件的优先级拥有特定的顺序。并且在一个监听周期中,同一输入设备的输入事件在列表中是相邻的。

至此,相信读者对EventHub的工作原理,以及EventHub的事件监听与读取机制有了深入的了解。接下来的内容将讨论EventHub所提供的原始输入事件如何被加工为Android输入事件,这个加工者就是Reader子系统中的另一员大将:InputReader。
在这里插入图片描述

  • 6
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值