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对象将事件发送到窗口,并接受反馈的过程

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 206,013评论 6 481
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 88,205评论 2 382
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 152,370评论 0 342
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 55,168评论 1 278
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 64,153评论 5 371
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,954评论 1 283
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,271评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,916评论 0 259
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,382评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,877评论 2 323
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,989评论 1 333
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,624评论 4 322
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,209评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,199评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,418评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,401评论 2 352
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,700评论 2 345

推荐阅读更多精彩内容