Android输入事件原理总结

输入事件系统的相关组件

Linux内核

接受输入设备的中断,并将原始事件的输入写入设备节点中;

设备节点

作为内核和IMS的桥梁,将原始事件的数据暴露给用户空间,以便IMS可以从中读取事件;

InputManagerService

Android系统服务,它分为java层和native层两部分;java层负责与WMS通信,native层则是InputReader和InputDispatcher两个输入系统关键组件的运行容器;

EventHub

使用inotify监听输入设备的添加和移除。

使用epoll机制监听输入设备的数据变化。

读取设备文件的数据。

将原始数据(生事件)返回给InputReader。

InputReader

IMS中的关键组件之一,它运行于一个独立的线程中,负责管理输入设备的列表与配置,以及进行输入事件的加工处理。它通过其线程循环不断地通过getEvents()函数从EventHub中将事件取出并进行处理。对于设备节点的增删事件,它会更新输入设备列表与配置。对于原始输入事件,InputReader对其进行翻译、组装、封装为包含更多信息、更具可读性的输入事件,然后交给InputDispatcher进行派发;

InputDispatcher

IMS中的另一个关键组件,它也运行于一个独立的线程中。InputDispatcher中保管了来自WMS的所有窗口的信息,其收到来自InputReader的输入事件后,会在其保管的窗口中寻找合适的窗口,并将事件派发给此窗口;

InputChannel

InputChannel支持跨进程传输。

保存socketpair的FD,App进程持有一端,WMS进程持有一端。

InputChannel负责事件最终的读写。

InputEventReceiver

包装了InputChannel,负责将InputChannel的FD加入到main looper并负责读写InputChannel。

将事件封装成Java层的事件对象向上派发给ViewRootImpl。

WMS

不是输入系统的一员,但它对InputDispatcher的正常工作起到重要作用。当新建窗口时,WMS为新窗口和IMS创建了事件传递所用的通道。另外,WMS还将所有窗口的信息,包括窗口的可点击区域,焦点窗口等信息,实时的更新到IMS的InputDispatcher中,使得InputDispatcher可以正确地将事件派发到指定的窗口;

ViewRootImpl

对某些窗口,如壁纸窗口、SurfaceView的窗口来说,窗口就是输入事件派发的终点。而对其他的activity、对话框等使用了Android控件系统的窗口来说,输入事件的终点是控件View。ViewRootImpl将窗口所接收的输入事件沿着控件树将事件派发给感兴趣的控件;

inotify机制 与epoll机制

inotify机制

INotify是一个Linux内核所提供的一种文件系统变化通知机制。它可以为应用程序监控文件系统的变化,如文件的新建、删除、读写等。INotify机制有两个基本对象,分别为inotify对象与watch对象,都使用文件描述符表示。

inotify对象对应了一个队列,应用程序可以向inotify对象添加多个监听。当被监听的事件发生时,可以通过read()函数从inotify对象中将事件信息读取出来。Inotify对象可以通过以下方式创建:

int inotifyFd = inotify_init();

而watch对象则用来描述文件系统的变化事件的监听。它是一个二元组,包括监听目标和事件掩码两个元素。

int wd = inotify_add_watch (inotifyFd, “/dev/input”,IN_CREATE | IN_DELETE);

当没有监听事件发生时,可以通过如下方式将一个或多个未读取的事件信息读取出来:

size_t len = read (inotifyFd, events_buf,BUF_LEN);

总结一下INotify机制的使用过程:

通过inotify_init()创建一个inotify对象。

通过inotify_add_watch将一个或多个监听添加到inotify对象中。

通过read()函数从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的使用步骤总结如下:

通过epoll_create()创建一个epoll对象。

为需要监听的描述符填充epoll_events结构体,并使用epoll_ctl()注册到epoll对象中。
使用epoll_wait()等待事件的发生。

根据epoll_wait()返回的epoll_events结构体数组判断事件的类型与来源并进行处理。

继续使用epoll_wait()等待新事件的发生。

IMS的构成

IMS在SystemServer中的ServerThread线程中启动,在InputManagerService的构造函数中调用nativeInit方法,nativeInit方法创建了一个类型为NativeInputManager的对象,它是Java层与Native层互相通信的桥梁。NativeInputManager位于IMS的jni层,负责native层的组件与java层的IMS的相互通信,同时它为主要工作是为InputReader和InputDispatcher提供策略请求接口InputReaderPolicyInterface和InputDispatcherPolicyInterface,策略请求被它转发为Java层的IMS,由IMS最终确定。在NativeInputManager构造函数中创建了EventHub和InputManager。在InputManager中创建了四个对象,分别为InputDispatcher,InputReader,InputReaderThread和InputDispatcherThread。

####### InputReader总体流程

1、首先从EventHub中抽取未处理的事件列表,这些事件分为两类,一类是从设备节点读取的原始输入事件,另一类是设备事件。

2、对原始输入事件进行封装与加工将结果暂存到mQueuedListener中。

3.所有事件处理完毕之后,调用mQueuedListener.flush()将所有暂存的输入事件一次性的交付给InputDispatcher.

EventHub中抽取未处理的事件列表主要是调用getEvents函数,getEvents函数的本质是通过epoll_wait()获取Epoll事件到事件池,并对事件池中的事件进行消费的过程。从epoll_wait()的调用开始到事件池的最后一个时间被消费完毕的过程称为EventHub的一个监听周期。由于buffer参数的额尺寸限制,一个监听周期可能包含多个getEvents调用。

InputReader经过加工之后,输出的事件分为三种基本类型,分别为:按键类型,手势类型和开关类型。三种类型分别由NotifyKeyArgs,NotifyMotionArgs,NotifySwitchArgs三个结构体描述。可以说EventHub的EawEvent是InputReader的输入,而上述三个结构体是InputReader的输出。InputDispatcher继承了InputListenerInterface,实现了notifyKey和notifyMotion和notifySwitch等方法。创建InputReader时将InputDispatcher传给了InputReader。InputReader以InputListenerInterface类型持有InputDispatcher。然后调用mQueuedListener.flush()将三种类型事件传给InputDispatcher。

在这有个问题,为什么不直接使用InputDispatcher作为事件的接受者,而是用QueuedInputListener这个中间人?QueuedInputListener是使用mArgsQueue队列将信息保存起来,当InputReader处理完自EventHub的所有原始输入事件之后,调用flush()函数将缓存的事件信息取出,这样做的目的是,减少InputDispatcher的休眠与唤醒次数,因为InputDispatcher派发的速度快于InputReader加工一个原始输入事件的速度,就会导致InputDispatcher多次休眠与唤醒。

InputDispatcher总体流程

InputReader将处理好的事件提交给InputDispatcher之后,会将输入事件放进派发队列,但是在放进派发队列之前,需要先过滤。过滤之后将事件封装成EventEntry的子类,然后调用enqueueInboundEventLocked()将事件注入mInboundQueue的队尾,并且根据mInboundQueue是否为空来是否唤醒派发线程。

真正的派发是调用dispatchOnceInnerLocked()函数,如果派发队列为空,则会使派发线程陷入无限期休眠状态,即将被派发的事件从派发队列中取出,事件也有可能某些原因被丢弃,被丢弃的原因保存在dropReason中,然后去寻找合适的窗口,目标窗口分为两种:普通窗口和监听窗口。普通窗口通过按点查找与按焦点查找两种方式获得,而监听窗口则无条件监听所有输入事件。

Motion事件派发与按键事件派发的区别:

按键事件在正式派发给窗口之前,进行一次额外的派发策略查询,这个查询结果决定此事件是正常派发、稍后派发还是丢弃。

按键事件的派发目标仅通过焦点方式进行查找。

派发找到对应的窗口之后,然后根据window找到Connection,然后将事件加到Connection的outboundQueue, 然后从outboundQueue队头取一个消息,调用Connection的InputPublisher发送事件,InputPublisher最终会调用InputChannel,InputChannel用自己保存的FD调用socketpair的senMsg函数将事件发出。
一个window对应一个InputChannel对应一个Connection。

发完事件后,将这个消息记录到Connection的waitQueue的队尾。InputDispatcherThread再次等待在Looper上,等App窗口消费完事件并发送finish事件后,InputDispatcherThread就会被唤醒,然后根据发生消息的FD(一个窗口对应一个FD)找到Connection,再根据事件的序列号(seq)找到事件然后将事件从waitQueue移除,并继续派发属于这个Connction的消息。

微信图片_20211103195352.jpg
InputChannel

InputChannel本质是一对SocketPair(非网络套接字)。SocketPair用来实现在本机内进行进程间的通信。一对SocketPair通过socketpair()函数创建,其使用者可以因此而得到两个相互连接的文件描述符。这两个描述符可以通过套接字接口send()和recv()进行写入和读取,并且向其中一个文件描述符写入的数据,可以从另一个描述符中读取。同pipe()所创建的管道不同,SocketPair的两个文件描述符是双通的,因此非常适合用来进行进程间的交互式通信;

1.事件发送主要是通过InputChannel来完成;

2.在wms 执行addView()时,调用openInputChannel来从native层获取inputchannels数组,一个通过ims
registerInputChannel来连接InputDispatcher,另外一个通过InputEventReceiver来连接窗口;

3.InputDispatcher经过Connection最终通过InputPublisher将事件发送到目标窗口;

4.NativeInputEventListener监听到事件到来时通过InputConsumer处理InputMessage后回调Java层接口;

Connection

Connection包含两个队列,分别为outboundQueue和waitQueue。还持有InputPublisher对象。

InputPublisher封装InputChannel并直接对齐进行写入和读取,也负责InputMessage结构体的封装和解析。

outboundQueue保存等待Connection进行发送事件的队列。

waitQueue已发送等待反馈的队列,得到反馈后则从队列中删除。

App进程获取到InputChannel后将之内部的socketpair的FD加入到main looper的FD监听列表中去,后续如果收到事件,事件的处理会直接发生在主线程,main looper监听到FD上有数据后回调FD绑定的回调函数,回调函数将事件读出来封装成对应的Event对象,然后层层传递到ViewRootImpl。ViewRootImpl通过一个责任链决定事件的处理顺序和方式,某些事件可能会先派发给输入法窗口进行消费,如果输入法窗口不消费就继续派发给view tree消费,派发给view tree是直接派发的,因为这时已经在主线程了,流程大致是:
ViewRootImpl -> DecorView -> Activity -> View(DecorView) -> DecorView的子View

如果App进程没有消费事件,也就是Activity、View等都没有处理这个事件,App进程发送给InputDispather的finish事件会标志这个事件的handled为false。
InputDispatcher收到handled为false的事件后会询问IMS是否备选(fallback)事件,IMS最终会经过WMS到PhoneWindowManager询问是否有备选事件,如果有就将PhoneWindowManager返回的备选事件加入到窗口对应的connection的outboundQueue的队头,在下一次窗口派发循环(注意InputDispather的mInboundQueue队列对应的大循环和connection的outboundQueue对应的窗口事件小循环)中将这个事件发给窗口。

微信图片_20211103195405.jpg

派发循环和事件发送循环

派发循环是InputDiapatcher不断的从派发队列取出事件,寻找合适的窗口进行发送的过程,主要是InputDispatcherThread线程主要的工作。

事件发送循环是InputDispatcher通过Connection对象将事件发送到窗口,并接受反馈的过程

你可能感兴趣的:(Android输入事件原理总结)