作者:任韬,腾讯移动客户端开发 高级工程师
商业转载请联系腾讯 WeTest 获得授权,非商业转载请注明出处。
原文链接:http://wetest.qq.com/lab/view/349.html


WeTest 导读

当我们在写带有 UI 的程序的时候,如果想获取输入事件,仅仅是写一个回调函数,比如 (onKeyEvent,onTouchEvent….),输入事件有可能来自按键的,来自触摸的,也有来自键盘的,其实软键盘也是一种独立的输入事件。那么为什么我能通过回调函数获取这些输入事件呢?系统是如何精确的让程序获得输入事件并去响应的呢?为什么系统只能同一时间有一个界面去获得触摸事件呢? 下面我们通过 Android 系统输入子系统的分析来回答这些问题。

一、输入事件的转发流程

二、物理设备是如何将输入数据发送给内核的

物理设备将数据发送给内核是通过设备驱动传输的,在 linux 下的/dev/input/目录下有几个设备文件,event0,event1,event2……… 这些设备文件实际上是驱动创建的,他们共用一个主设备号,仅仅是次设备号不同,表示这是一类设备。比如触摸屏对应 event0,触摸屏驱动被挂载后,驱动程序会进行初始化,主要是初始化 CPU 引脚,设置中断处理程序。

很好理解,触摸屏是一个物理设备,但是我们的驱动程序运行在 CPU 中,这是两个不同的设备,他们在物理上的连接是通过导线将对应的引脚相连接的,只不过导线在 PCB 板中很小,驱动程序就是初始化 CPU 中跟触摸屏连接的引脚,但让每个引脚都会对应寄存器,这个在 CPU 的芯片手册中很详细 (DataSheet)。

当按下触摸屏的时候触摸屏有个引脚电平变低了,相连的 CPU 引脚检查到这个连接的引脚电压变低了,那么就会触发中断,这个在触摸驱动中初始化好的,CPU 有个中断向量表,这里就到了我们驱动中写好的中断处理函数,中断处理函数中就会读取触摸屏的数据,就是通过相连接的引脚组成的二进制数据比如 (01011010),这个时候我们的内核就拿到的触摸屏的数据。

触摸屏芯片的时序图

三、内核是如何把输入数据发送给用户空间 Android framework 的

内核拿到触摸屏的数据后,经过平滑处理,滤波,数据还是在内核空间,那么 Android 怎么拿到触摸数据呢? Android 实际上是运行在 linux 内核上一组进程,这一组进程组合为用户提供 UI,应用程序的安装等等服务。

手机开机流程是 linux 内核先启动,启动完成之后会将 Android 进程组启动起来,FrameWork 属于这个进程组之中。Framewok 中有个服务 InputManagerService,我们看 Android 源码它在哪里实例化的:

SystemServer.java----------->

startOtherServices()------>

/构造 InputManagerService/

inputManager = new InputManagerService(context);

/* 将 inputManager 传递给 WindowManagerService 去

wm=WindowManagerService.main(context, inputManager,

mFactoryTestMode !=FactoryTest.FACTORY_TEST_LOW_LEVE ! mFirstBoot, mOnlyCore);

/给 InputManagerService 设置回调/

inputManager.setWindowManagerCallbacks(wm.getInputMonitor());

/* 全初始化好后,SystemServer 调用 start() 函数让 InputManager 中两个线程开始运行。先看 InputReaderThread,它是事件在用户态处理过程的起点 */

inputManager.start();

所以可以看到它在 SystemServer 进程中实例化并且启动,所以我们首先需要看看 InputManagerService 的构造函数都做了什么?

构造函数会调用到 jni 创建 NativeInputManager 的 c++ 对象, NativeInputManager 构造函数中创建

Sp eventHub = new EventHub()

mInputManager = new InputManager(eventhub,this,this);

eventHub 对象构造函数做了下面几件事情:

  1. 创建 epoll 对象,之后就可以把各个输入设备的 fd 添加进来多路等待输入事件

    1. 利用 inotify 机制监听/dev/input 目录下的变更,如果有则意味着设备变换,需要处理,输入设备的增减删除操作的监听,将代表 inotify 的 fd 添加到 epoll 中
    2. 创建 pipe,管道只能用来在具有公共祖先的两个之间通信.读端添加 epoll 中

InputManager 对象构造函数做了下面几件事:

  1. 创建 InputDispatcher

  2. 创建 InputReader(eventhub,inputdispatcher),InputDispatcher 继承 InputListenerInterface

  3. 创建 InputReaderThread

  4. 创建 InputDispatcherThread

我们还记得最 SystemServer.java 中最后通过 inputManager.start(); 来运行我们的 InputManagerService,所以继续看 start 方法,实际上在 native 层的 inputManager 对象中,将上面创建的两个线程 InputReaderThread 和 InputDispatcherThread 的 start 方法中。

对于 InputReaderThread 的 start 方法:

  1. 调用构造函数中保存的 eventHub 的 getEvents 方法获取 input 事件,在 getEvent 方法中做的事

1)判断是不是需要打开 input 设备驱动,如果需要打开设备驱动,扫描/dev/input 目录下的设备文件并打开这些设备,同时会判断设备列表中有没有虚拟键盘,没有的话就创建一个 device 添加进去

2)到下一步中至少系统存在两个输入设备,一个是触摸屏,一个是虚拟键盘,因为上面这次 getEvent 的调用需要打开设备,所有就将这些动作封装成 RawEvent 事件,这里两个 DEVICE_ADDED 事件 +FINISH_DEVICE_SCAN 事件,将这些事件返回,不会往下走了

3)如果第二次进入 getEvents 方法中就会等待读取输入事件,将读取的 touch 事件发送返回

到这里我们就知道了内核空间的触摸输入数据是如何传递到了用户空间的 Android framework 中的,实际上就是通过/dev/input 目录下,去扫描这个目录,如果有 device 就打开这个 device ,并添加到 epoll 对象中,多路等待输入事件,在 loop 中获取数据。

四、Android framework 是怎样将输入数据发送给 APP 进程的

Android framework 获取了触摸输入的数据,但是在系统中有那么多进程,那么多进程都在获取输入,它是如何进一步处理,准确的分发事件的呢?

InputReaderThread 的 start 方法中做的第二件事情:

调用 processEventsLocked 方法处理上面的 getEvents 方法返回的的 RawEvent

1)根据 RawEvent 的类型不同,调用不同的方法处理,有 ● 普通的 touch 事件

.● 添加设备的事件

.● 删除设备的事件

.● FiNISHED_DEVICE_SCAN

2)对于 touch 事件: 调用这个 touch 事件对应的输入设备 (之间创建的 InputDevice) 的 process 方法,该方法内部调用内部的 InputMapper 的 process 方法,一个输入设备有很多个 Mapper,遍历所有的 Mapper,并调用 process,假定我们是一个支持多点触摸的 touch screen,它的 mapper 是 MultiTouchInputMapper,调用它的 process 方法。

3)MultiTouchInputMapper 的 process 方法内部会这样处理:

首先每次一个 touchEvent 获取 Slot,在没有收到 EV_SYN 之前对应的 Slot 都是相同的,然后依次处理 x,y,pressure,touch_major,这些值初始化 slot 的各个变量;

当收到 ev.type== EV_SYN 并且 ev.code = SYN_MT_REPORT 那么当前的 slot 的 index 加 1,给下一次触摸事件去记录,同时 sync 函数处理这次触摸事件;

然后 CurrentCookedPointerData 和 LastCookedPointerData 进行一些列的操作,up,down 还是 move 事件,然后对应的不同事件,调用 dispatchMotion,内部调用 InputDispatcher 的 notifyMotion

4)对于 InputDispatcher 的 notifyMotion:

● 如果 InputDispatcher 设置了 inputFilter,那么首先调用 inputFilter 来消费这些事件

● 如果没有 inputFiler,或者 inputFilter 对这些事件不感兴趣,那么就会构造一个 MotionEntry,添加到 mInboundQueue,并唤醒 InputDispatcher 线程处理

5)对于 InputDispatcher 的线程处理循环:

● 优化 app 切换延迟,当切换超时,则抢占分发,丢弃其他所有即将要处理的事件;

● 分发事件:

首先调用 findTouchedWindowTagetsLocked 寻找有 focus 的 window 窗口, 并把这些创建保存在 inputTargets 数组中;

之前注册的 monitor 的 InputChannel 这里也会添加到 inputTargets 数组中;

然后向 inputTargets 数组一一分发事件。

到这里我们就知道了是如何找到这个 APP 进程的了。

五、APP 进程是如何将输入数据发送给它对应的 Activity 的

Activity 是一个进程的基本组件,可以认为它代表了一个界面,是一堆 View 的集合,每次 Activity 启动的时候都做了什么呢?

1、实际上取决于它背后的 ViewRootImpl 做了什么,在 ViewRootImpl.java 中的 setView 方法中,实例化 InputChannel,当然会判断当前的窗口能不能接受输入事件,接着在调用到 session.java 中的 addToDisplay 方法传递给 WindowManagerService,实际上是调用 WindowManagerService 的 addWindow 方法,在 WindowManagerService 中会创建一对 InputChannel[],然后 InputChannel[1] 转移到这个 inputChannel, 然后 setView 方法继续创建一个 WindowInputEventReceiver 对象,然后将上面创建好的 InputChannel

2、WindowManagerService 中的 addWindow 方法:

InputChannel[] inputChannels = InputChannel.openInputChannelPair(name)

/Channel[0] 保存在 server 端/

win.setInputChannel[inputChannels[0]]

/* Channel[1] 返回给 ViewRootImpl 端 */

inputChannels[1].transferTo(outInputChannel)

/注册到 inputManagerService 中/

mInputManager.registerInputChannel(win.mInputChannel,win.mInputWindowHandle)

到这里我们就能明白如何将时间分发给对应的 Activity 了,其实是给了它背后的 ViewRootImpl。

六、Activity 又是如何将输入数据发送给具体的 View 的

最后一步就是将事件分发到 Activity 中具体的 View 了,从 ViewRootImpl 中将事件分发给具体的 View,很好理解,因为触摸的范围在到这里是知道的,每个 View 的位置以及状态到这里也是知道的,因为 View 要正确渲染的话,Android 图形框架会搞定这一切,测量每个 View 的大小,确定每个 View 的位置,ViewRootImpl 会一层一层将数据分发到自己每个 View 中,但是每个 View 自己知道这个触摸事件是不是作用在自己身上的,如果不是就丢弃了,往下面分发。

总结

触摸事件的分发流程看起来挺复杂,但是 Android 实现的还是很优雅的,我们去分析它的流程,对于我们想实现一些比较的酷的功能是有帮助的。当然对于我们调试代码也会有帮助,当发现触摸后,系统无响应,将上面的流程分解,总是能分析出原因。


腾讯 WeTest 提供上千台真实手机,随时随地进行测试,保障应用/手游品质。节省百万硬件费用,加速敏捷研发流程。

同时腾讯 WeTest 兼容性测试团队积累了 10 年的手游测试经验,旨在通过制定针对性的测试方案,精准选取目标机型,执行专业、完整的测试用例,来提前发现游戏版本的兼容性问题,针对性地做出修正和优化,来保障手游产品的质量。目前该团队已经支持所有腾讯在研和运营的手游项目。

欢迎进入:http://wetest.qq.com/product/cloudphone 体验安卓真机

欢迎进入:http://wetest.qq.com/product/expert-compatibility-testing 使用专家兼容测试服务。 WeTest 兼容性测试团队期待与您交流!You Create,We Test!

如果对使用当中有任何疑问,欢迎联系腾讯 WeTest 企业 QQ:800024531


↙↙↙阅读原文可查看相关链接,并与作者交流