
注:以下内容基于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桌面并消费掉事件。