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

注:如下内容基于Android API Version 27(Android 8.1)Linux Kernel 3.18.0android

概述

Android的input事件接收和派发发生在WMS(WindowManagerService)所在进程,也就是system_server进程,input系统在WMS端有两个关键线程:读取线程和派发线程。读取线程扫描输入设备并从设备中主动读取输入事件,而后将事件交给派发线程。App进程向WMS添加窗口时WMS会建立一个事件通道(InputChannel),通道的一头返回给App进程,一头注册到input派发线程,同时WMS会将当前的全部窗口的层级和大小等信息传给input派发线程,input派发线程收到事件后查看当前focused窗口或者能够处理当前事件的窗口,而后经过事件通道直接将事件派发给窗口,App进程将从WMS返回的事件通道的FD(文件描述符)添加到本身主线程Looper中,收到输入事件后由主线程直接处理事件。api

事件传递过程

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代理对象。socket

IMS持有Native层的NativeInputManagerNativeInputManager持有Native的InputMangeride

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

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

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

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桌面并消费掉事件。
相关文章
相关标签/搜索