Android输入系统-IMS
前言
上次系统学习了下WMS计算位置和大小的流程,在调整系统window窗口位置后,输入系统和UI显示对不上,最近有时间,再把Android输入系统完整分析下,如果只是解决一个问题,不用这么费劲,但是完整流程学习一遍,还是收益很多,特别是对事件处理的设计模式和多个模块协调完美配合。
学习Andrid输入系统,网上资料也基本都是参考《深入理解Android 卷III》,它写的特别好,代码/原理都写的很清楚,自己花了三天时间完整把这一章节看完,本书把输入系统核心写的很清楚,其他一些输入模块,比如鼠标绘制,View里面事件派发,Policy策略处理,以及更底层kernel输入框架(扯的有点远)等 并没有写,后面有机会补充这些子模块的分析。
看完总想留下点什么,就把部分流程图自己画下,算是学习笔记了,如果想学习Android输入系统,强烈推荐看下这本书。网上都是部分节选,完整版下载电子或者购买吧。
深入理解Android 卷III
《深入理解Android 卷III》第五章 深入理解Android输入系统
Android输入系统简介
对应用层而言,输入事件的源头是位于/dev/input下的设备节点,而输入系统的终点是WMS管理的某个窗口。最初的输入事件位内核生成的原始事件,而最终交付给窗口的则是KeyEvent或MotionEvent对象。因此Android输入系统的主要工作是读取设备节点中的原始事件,将其加工封装,然后派发给一个特点的窗口以及窗口中的控件。这个过程由InputManagerService(以下简称IMS)系统服务为核心的多个参与者共同完成。
Android输入系统的总体流程.png
输入系统中最基本的参与者和模块
-
Linux内核,接受输入设备的中断,并将原始事件的数据写入设备节点中
设备接电,作为内核与IMS的桥梁,将原始事件的数据暴露给用户空间,以便IMS可以从中读取事件 - InputManagerService,一个android系统服务,分为Java层和Native层两部分,java层负责与WMS通信,而Native层则是InputReader和InputDispatcher两个输入系统关键组件的运行容器
- EventHub,直接访问所有的设备节点,通过一个名为getEvents()的函数将所有输入系统相关的待处理的底层事件返回给使用者,包括原始输入事件,设备节点的增删等
- InputReader,是IMS中的关键组件之一,它运行一个独立的线程中,负责管理输入设备的列表和配置,以及进行输入事件的加工处理,它通过其线程循环不断地通过getEvents()函数从EventHub中将事件取出并进行处理,对于设备节点的增删事件,它会更新输入设备列表与配置,对于原始输入事件,InputReader对其进行翻译,组装,封装为包含更多信息,更多可读性的输入事件,然后交给InputDispatcher进行派发
- InputReaderPolicy,为InputReader的事件加工处理提供一些策略配置
- InputDispatcher,是IMS中的另一个关键组件,运行于一个独立的线程中, InputDispatcher中保管来自WMS的所有窗口的信息,收到InputReader的输入事件后,会在其保管的窗口中寻找合适的窗口,并将事件派发给此窗口
- InputDispatcherPolicy,为InputDispatcher的派发过程提供策略控制,例如HOME键被InputDispatcherPolicy截取到PhoneWindowManager中处理,并阻止窗口收到HOME键按下的事件
-
WMS,并不是输入系统的一员,新建窗口时,WMS为新窗口和IMS创建了事件传递所用的通道,会将窗口的可点击区域,焦点窗口等信息实时更新到IMS的InputDispatcher中,使得InputDispatcher可以正确将事件派发到指定窗口
ViewRootImpl,对某些窗口,如壁纸窗口,SurfaceView的窗口来说,窗口就是输入事件派发的终点,而对其他的如Activity,对话框等使用了Android控件系统的窗口来说,输入事件的终点是控件
IMS体系结构
IMS服务的大部分工作是在Native层完成的,Java层基本是Native层的封装,并负责和WMS服务对接。
看下面框架图,IMS的启动主要是创建InputReader线程和InputDispather线程,后面主要的读取输入事件和分发事件都是在这两个线程中完成的。
IMS框架图
三个线程、三台水泵
InputReader在其线程循环中不断地从EventHub中抽取原始输入事件,进行加工处理后将加工所得的事件放入InputDispatcher的派发队列中。InputDispatcher则在其线程循环中将派发队列的事件取出,查找合适的窗口,将事件写入创建的事件接受管道中。窗口事件接受线程的Looper从管道中将事件取出,交由事件处理函数进行事件的响应。整个过程共有三个线程收尾相连,像三台水泵似的一层一层将事件交付给事件处理函数。
IMS三个线程,三台水泵
IMS成员关系
IMS内部做了很多的抽象工作,EventHub、InputReader以及InputDispatcher等实际上都是继承自相应的名为XXXInterface接口,并且仅通过接口进行相互之间的引用。下图左侧部分为Reader子系统作为第一台水泵,右侧为Dispatcher子系统,作为第二台水泵。
IMS成员关系
EventHub事件生成流程
EventHub的直译是事件的集线器,顾名思义,它将所有的输入事件通过一个接口getEvents()把从多个输入设备节点中读取的事件交给InputReader,它是输入系统最底层的一个组件。
EventHub的事件生成流程
InputMapper分配
Device结构体的事件位掩码描述了4种类型的输入事件:
- EV_KEY, 按键类型的事件。能够上报这类事件的设备有键盘,鼠标,手柄,手写板等一切拥有按钮的设备(也包括手机上实体按键)。在Device结构体中,对应的事件位掩码keyBitMask描述了设备可以产生的按键事件的集合。按键事件的全集包括字符按键,方向按键,控制键,鼠标键,游戏按键等。
- EV_ABS, 绝对坐标类型的事件。这类事件描述了再空间中的一个点,接触板,触摸屏等使用绝对坐标的输入设备可以上报这类事件。事件位掩码absBitmask描述了设备可以上报的事件的维度信息(ABS_X,ABS_Y,ABS_Z),以及是否支持多点事件。
- EV_REL, 相对坐标类型事件。这类事件描述了事件在空间中相对于上次事件的偏移量。鼠标,轨迹球等基于游标指针的设备可以上报此类事件。事件位掩码relBitmask描述了设备可以上报事件的维度信息(REL_X,REL_Y,REL_Z)。
- EV_SW,开关类型的事件。这类事件描述了若干固定状态直接的切换。手机上的静音模式开关按钮、模式切换键盘灯设备可以上报此类事件。事件位掩码swBitmask表示了设备可以切换的状态列表。
在EventHub的openDeviceLocked()接口通过事件位掩码确定了设备可以上报的事件类型后,便可以据此确定设备的类型了。Android在EventHub.h中定义了12中设备类型,为了叙述简介,我们省略了INPUT_DEVICE_CLASS_前缀: - KEYBOARD, 可以上报鼠标按键以外的EV_KEY类型事件的设备都属于此类,如键盘,机身按钮(音量键,电源键等)。
- ALPHAKEY, 可以上报字符按键的设备,例如键盘,此类型的设备必定属于KEYBOARD。
- DPAD, 可以上报方向键的设备。例如键盘,手机导航键等。这类设备也同属于KEYBOARD。
- GAMEPAD, 可以上报游戏按键的设备,如游戏手柄。这类设备同时也属于KEYBOARD。
- TOUCH, 可以上报EV_ABS类型事件的设备都属于此类,如触摸屏和触控板。
- TOUCH_MT, 可以上报EV_ABS类型事件,并且其事件位掩码指示其支持多点事件的设备属于此类。例如多点触摸屏。这类设备同时也属于TOUCH类型。
- CURSOR, 可以上报EV_ABS类型事件,并且可以上报BTN_MOUSE子类EV_KEY事件的设备属于此类,例如鼠标和轨迹球。
- SWITCH, 可以上报EV_SW类型事件的设备。
- JOYSTICK, 属于GAMEPAD类型,并且属于TOUCH类型的设备。
- VIBRATOR, 支持力反馈的设备。
- VIRTUAL, 虚拟设备。
-
EXTERNAL, 外部设备,即非内建设备。例如外接鼠标,键盘,游戏手柄等。
确定设备类型之后,InputReader的createDeviceLocked()接口为InputDevice分配InputMapper。除了VIRTUAL和EXTERNAL没有对应的InputMapper,以及KEYBOARD,ALPHAKEY,DPAD与GAMEPAD 4者公用KeyboardInputMapper以外,InputReader为每种设备类型定义了对应的InputMapper.
InputMapper分配
一个设备节点可能托管很多种类型的物理输入设备。例如Android模拟器中只有event0一个设备节点,但是其负责按键,触摸屏事件的上班工作。此时,这个设备节点对应的InputDevice将会拥有KeyboardInputMapper与SingleTouchInputMapper两个InputMapper。不过得益于InputMapper的职责链的设计模式,一个InputDevicece处理多种输入事件一点也不吃力。
InputReader原始事件的读取与加工过程
Reader子系统分为读取(EventHub)和价格处理(ImputReader)两个部分。
InputReader原始事件加工
InputDispatcher通用事件派发流程
按键事件往往携带系统功能,例如HOME键,POWER键以及Volume键等,InputDispatcher需要对按键事件做更多附件工作,很多工作都是在Policy里面做的,先说明下通用事件派发流程总结。
- 在事件进入派发队列之前的处理位于InputReader线程中。而其他操作则位于派发线程找那个。
- 事件以EventEntry子类的形式存在于InputDispatcher中。
- 事件派发是串行的,在队首的事件派发完成之前,不好进行其他事件的派发。
-
在选择InputTarget的过程中,如果发现有一个目标窗口尚未准备好接受事件,则暂停当前事件的派发,并通过设置nextWakeupTime在下次派发循环时再次派发。
Dispatcher通用事件派发流程
InputChannel对工作原理
InputChannel本质是一对SocketPair(非网络套接字)。SocketPair用来使用本机进程间的通信。一对SocketPair通过socketpair()接口创建。使用者可以因此而得到两个相互连接的文件描述符。这两个描述符可以通过套接字接口send()和recv()进行写入和读取,并且向其中一个文件描述符中写入的数据,可以从另一个描述符中读取。同pipe()所创建的管道不同,SocketPair的两个文件描述符是双向通信的,因此非常适合进程间的交付通信。
InputChannel就是SocketPair描述符及其操作的封装,而且是成对使用的。匹配的两个InputChannel分别保有一个SocketPair的描述符,并分别分配给InputDispatcher与窗口。因此InputDispatcher向保有的InputChannel中写入输入事件,可以由窗口从自己的InputChannel中读取。并且窗口可以将事件处理完毕的反馈写入到InputChannel中,InputDispatcher再将反馈进行读取。
InputChannel对工作原理
Connection工作原理
在InputDispatcher中,InputChannel被封装成一个Connection对象。Connection描述了从InputDispatcher到目标窗口中的一个链接,其中保存了向窗口发送的事件状态信息。在Connection中,重要的成员有:
- minputPublisher, InputPublisher类的一个对象,它封装InputChannel并直接对其写入和读取。另外,它也负责InputMessage结构体的封装和解析。
- outboundQueue, 用于保存等待通过此Connection进行发送的事件队列。
-
waitQueue, 用于保存已经通过此Connection将事件发送给窗口,正在等待窗口反馈的事件队列。
Connection工作原理
窗口端的连接
当窗口端通过addWindow()接口获取InputChannel后,便会使用它创建一个InputEventReceiver对象,InputEventReceiver对象可以接收来之InputChannel的输入事件,并触发其onInputEvent()回调。
窗口端的连接
总结
以上只是根据《深入理解Android 卷III》学习记录,很多细节没有整理出来,有兴趣学习的建议翻阅书本。