Android 设备输入事件(input)派发原理总结

5,097 阅读10分钟

注:以下内容基于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直接使用IMSWMS通过IMS向事件派发线程传递窗口信息并设置当前FocusedwindowWMS同时也负责创建和注册system_server进程与App进程传递事件的通道:InputChannelWMSIMS注册事件发送端InputChannelIMS最终调用到了Native层的InputManager

应用程序通过Java层的InputManager访问IMS,Java层的InputManager包装了IMSbinder代理对象。

IMS持有Native层的NativeInputManagerNativeInputManager持有Native的InputManger

Native层的InputManager包含了一个InputDispatcher和一个InputReaderInputReader包含了一个EventHub

InputDispatcher继承了InputListenerInterface,实现了notifyKeynotifyMotion等方法。创建InputReader时将InputDispatcher传给了InputReaderInputReaderInputListenerInterface类型持有InputDispatcher

InputReader有一个对应的InputReaderThread线程。在InputReaderThread里循环调用EventHub获取事件。

EventHub负责打开/dev/input目录下的所有设备,和通过inotify机制监听/dev/input下面的设备文件变化,并负责读取所有的FD

EventHub使用epoll系统调用监控inotify和设备文件FD的可读事件,当某个设备可读时,从epoll_wait返回,紧接着读取有数据的设备的事件数据并将数据返回给InputReaderInputReader将原始的生事件加工成熟事件后交给InputDispather

InputDispatcher对应了一个InputDispatcherThread线程,在InputDispatcherThread使用Native层的Looper等待派发出去的事件的回应,同时如果InputReader有事件会调用InputDispatcher将事件添加到一个队列中(mInboundQueue)并唤醒InputDispatcherThreadInputDispatcherThread被唤醒后调用InputDispatcher开始派发事件队列中的事件。

事件派发过程是先找到当前的window,然后根据window找到Connection,然后将事件加到ConnectionoutboundQueue, 然后从outboundQueue队头取一个消息,调用ConnectionInputPublisher发送事件,InputPublisher最终会调用InputChannelInputChannel用自己保存的FD调用socketpairsenMsg函数将事件发出。

一个window对应一个InputChannel对应一个Connection

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

App端

App窗口在调用WMSaddWindow时,WMS会为App窗口建立一对InputChannelInputChannel基于socketpair(socketpair的两端是对等的,没有server和client之分),一端给InputDispatcher使用,一端返回给App进程用来接收事件,WMS会将服务端的InputChannel注册到InputDispatcher,这样InputDispather就可以用来给窗口发送事件并接收窗口事件了,接收事件的原理是将InputChannelFD加入到InputDispatherLooper中,因为InputDispatherThread阻塞在Looper.pollOnce上,当InputDispather收到窗口发来的finish事件后InputDispatherThread会被唤醒,然后由InputDistather处理finish消息。

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

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

备选事件:系统可以为某些事件配置回滚,比如一个按键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支持跨进程传输。
  • 保存socketpairFD,App进程持有一端,WMS进程持有一端。
  • InputChannel负责事件最终的读写。

InputEventReceiver

  • 包装了InputChannel,负责将InputChannelFD加入到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切换,只要还是用户现在看到的window0.5s内的事件都还是给这个window。超过0.5s的事件就依照新查找到的窗口而定。

ANR是个dialog,是AMS弹的,事件的源头是InputDispatcher,经过InputDispatcher -> NativeInputManager -> InputManagerService -> AMS

事件注入的方式

事件注入可以协助我们实现UI自动化测试,Android上可以使用以下几种方法进行事件注入。

1,使用Instrumentation类,调用其比如sendPointerSync方法。

2,使用adb getevent/putevent命令行工具

3,通过IMSinjectInputEvent方法(App进程可以通过InputManager访问IMS),由于injectInputEventapi hide的方法,因此只能通过hack的方式访问。

注意

  • 因为ANRAMS会发出一个android.intent.action.ANR广播,通过监听这个广播可以收集App的ANR事件并上报给服务端。
  • HOME键在派发前会被IMS拦截,因为IMSInputDispatcher的派发前polocyIMS会将事件转交给PhoneWindowManager,由PhoneWindowManager启动HOME桌面并消费掉事件。