注:以下内容基于Android API Version 27(Android 8.1)Linux Kernel 3.18.0
概述
Android的input事件接收和派发发生在WMS
(WindowManagerService
)所在进程,也就是system_server
进程,input系统在WMS
端有两个关键线程:读取线程和派发线程。读取线程扫描输入设备并从设备中主动读取输入事件,然后将事件交给派发线程。App进程向WMS
添加窗口时WMS
会创建一个事件通道(InputChannel
),通道的一头返回给App进程,一头注册到input派发线程,同时WMS
会将当前的所有窗口的层级和大小等信息传给input派发线程,input派发线程收到事件后查看当前focused
窗口或者可以处理当前事件的窗口,然后通过事件通道直接将事件派发给窗口,App进程将从WMS
返回的事件通道的FD(文件描述符)添加到自己主线程Looper
中,收到输入事件后由主线程直接处理事件。
事件传递过程
IMS端
input系统在Java层代表是IMS
(InputManagerService
),IMS
随着system_server
进程启动,IMS
启动过程中会启动两个线程,一个事件读取线程(InputReaderThread
),一个事件派发线程(InputDispatcherThread
)。
WMS
直接使用IMS
,WMS
通过IMS
向事件派发线程传递窗口信息并设置当前Focused
的window
,WMS
同时也负责创建和注册system_server
进程与App进程传递事件的通道:InputChannel
。WMS
向IMS
注册事件发送端InputChannel
,IMS
最终调用到了Native层的InputManager
。
应用程序通过Java层的InputManager
访问IMS
,Java层的InputManager
包装了IMS
的binder
代理对象。
IMS
持有Native层的NativeInputManager
,NativeInputManager
持有Native的InputManger
。
Native层的InputManager
包含了一个InputDispatcher
和一个InputReader
,InputReader
包含了一个EventHub
。
InputDispatcher
继承了InputListenerInterface
,实现了notifyKey
和notifyMotion
等方法。创建InputReader
时将InputDispatcher
传给了InputReader
。InputReader
以InputListenerInterface
类型持有InputDispatcher
。
InputReader
有一个对应的InputReaderThread
线程。在InputReaderThread
里循环调用EventHub
获取事件。
EventHub
负责打开/dev/input
目录下的所有设备,和通过inotify
机制监听/dev/input
下面的设备文件变化,并负责读取所有的FD
。
EventHub
使用epoll
系统调用监控inotify
和设备文件FD的可读事件,当某个设备可读时,从epoll_wait
返回,紧接着读取有数据的设备的事件数据并将数据返回给InputReader
,InputReader
将原始的生事件加工成熟事件后交给InputDispather
。
InputDispatcher
对应了一个InputDispatcherThread
线程,在InputDispatcherThread
使用Native层的Looper
等待派发出去的事件的回应,同时如果InputReader
有事件会调用InputDispatcher
将事件添加到一个队列中(mInboundQueue
)并唤醒InputDispatcherThread
,InputDispatcherThread
被唤醒后调用InputDispatcher
开始派发事件队列中的事件。
事件派发过程是先找到当前的window
,然后根据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
的消息。
App端
App窗口在调用WMS
的addWindow
时,WMS
会为App窗口建立一对InputChannel
,InputChannel
基于socketpair
(socketpair
的两端是对等的,没有server和client之分),一端给InputDispatcher
使用,一端返回给App进程用来接收事件,WMS
会将服务端的InputChannel
注册到InputDispatcher
,这样InputDispather
就可以用来给窗口发送事件并接收窗口事件了,接收事件的原理是将InputChannel
的FD
加入到InputDispather
的Looper
中,因为InputDispatherThread
阻塞在Looper.pollOnce
上,当InputDispather
收到窗口发来的finish事件后InputDispatherThread
会被唤醒,然后由InputDistather
处理finish消息。
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
对应的窗口事件小循环)中将这个事件发给窗口。
备选事件:系统可以为某些事件配置回滚,比如一个按键App没有处理,系统可以派发一个与这个按钮功能类似的一个按键事件尝试让App处理。
每一个事件都至少在一次线程循环中被派发。
对于当前正在派发事件的窗口,事件是发送一个收到反馈再发下一个,如果本次发送没有收到反馈,不会发下一个。
如果用户按HOME键或触发了目标为其他窗口的事件,此时InputDispatcher
发现当前窗口正在等待上一个事件的反馈,就会将排在当前窗口上的所有事件都丢弃,然后将HOME事件或属于其他窗口的事件发送给对应的目标。
事件系统关键组件
Linux Kenel和设备驱动
- 根据插入的设备在
/dev/input
目录下创建event0~eventN
个设备节点。 - 监听设备输入产生的硬件中断。
- 将数据缓存起来,唤醒在设备文件上等待数据的进程。
EventHub(管理输入设备和读取输入事件)
- 使用
inotify
监听输入设备的添加和移除。 - 使用
epoll
机制监听输入设备的数据变化。 - 读取设备文件的数据。
- 将原始数据(生事件)返回给
InputReader
。
InputReader (将生事件加工成熟事件)
- 读取
IMS
提供的配置信息,比如键盘布局。 - 根据
IMS
提供的配置信息(包括键盘布局,显示屏信息)对原始事件实施一次转换。 - 将多个事件组合成一个可供上层消费的事件(比如将一组触摸屏的原始事件合并成一个
ACTION_DOWN
事件) 。
InputDispatcher (分发事件)
- 根据
IMS
提供的派发前策略过滤和拦截事件(比如HOME键) - 对于按键事件产生模拟按下重复事件,开始重复延迟是
500ms
,重复的间隔是50ms
。 WMS
会将当前的所有窗口和窗口信息传给InputDispatcher
,以供InputDispatcher
寻找派发窗口, 找到当前可以接收事件的窗口(比如key事件寻找focus的窗口,motion事件寻找包含这个事件坐标的窗口) 将事件派发给窗口。
InputChannel
InputChannel
支持跨进程传输。- 保存
socketpair
的FD
,App进程持有一端,WMS
进程持有一端。 InputChannel
负责事件最终的读写。
InputEventReceiver
- 包装了
InputChannel
,负责将InputChannel
的FD
加入到main looper并负责读写InputChannel
。 - 将事件封装成Java层的事件对象向上派发给
ViewRootImpl
。
ViewRootImpl
- 收到事件后按照一定的策略派发给view tree
关于ANR
InputDispatcher
根据事件找到目标窗口好要看目标窗口是否能够接受事件,能不能够接受事件是根据目标窗口现在有没有正在派发的事件,如果有,本次不派发,记录一个ANR
起始时间,并将这个待派发的事件挂起,如果后续又有新事件入列导致派发线程被唤醒,再次派发刚才挂起的事件,同样检查当前窗口是否能接受事件并更新ANR
时间,当ANR
时间达到5s
,通知IMS
处理ANR
事件。(如果新加入一个属于其他窗口的事件或者HOME键按下事件,会将当前窗口等待派发的事件都丢弃掉,除了已经加入到窗口自己的派发队列中的事件,这些时间会在其前面等待响应的事件响应后挨个发给窗口)
对于key
事件和motion
事件有所区别,key
事件对于同一个窗口必须是发完一个,下一个事件要重新查找窗口再派发,因为有可能上一个事件会导致焦点窗口改变,比如遥控器,用户点了一个按钮,弹出一个弹窗,而下一个事件应该发给新的弹窗,用户的预期是一个按键处理完了再处理另一个按键。
而motion
事件,如果当前窗口有正在处理的事件,后续事件只要是在第一个未响应事件发出的0.5s
之内发生都还是加入到这个窗口自己的派发队列,等前面的事件派发完了接着将队列中其他事件派发掉,如果超过0.5s
则不加入当前窗口派发队列,而是等待下一次派发周期重新查找窗口,并记录ANR
起始时间。这样的做法是考虑到motion
事件一般不考虑焦点,用户当前看到的是哪个window
,预期时间就应该给哪个window
,即便正在处理的事件会导致window
切换,只要还是用户现在看到的window
,0.5s
内的事件都还是给这个window
。超过0.5s
的事件就依照新查找到的窗口而定。
ANR
是个dialog
,是AMS
弹的,事件的源头是InputDispatcher
,经过InputDispatcher -> NativeInputManager -> InputManagerService -> AMS
事件注入的方式
事件注入可以协助我们实现UI自动化测试,Android上可以使用以下几种方法进行事件注入。
1,使用Instrumentation
类,调用其比如sendPointerSync
方法。
2,使用adb getevent/putevent
命令行工具
3,通过IMS
的injectInputEvent
方法(App进程可以通过InputManager
访问IMS
),由于injectInputEvent
是api hide
的方法,因此只能通过hack的方式访问。
注意
- 因为
ANR
时AMS
会发出一个android.intent.action.ANR
广播,通过监听这个广播可以收集App的ANR
事件并上报给服务端。 HOME
键在派发前会被IMS
拦截,因为IMS
是InputDispatcher
的派发前polocy
,IMS
会将事件转交给PhoneWindowManager
,由PhoneWindowManager
启动HOME
桌面并消费掉事件。