Jimmy Chen

A Programmer

(转载)Input系统: InputReader处理按键事件

前言

前面几篇文章,已经为 Input 系统的分析打好了基础,现在是时候进行更深入的分析了,请读者务必仔细阅读前面的文章,重复内容本文不再具体说明。

通常,手机是不带键盘的,但是手机上仍然有按键,就是我们经常使用的电源键以及音量键,这就是本文要来分析按键事件的原由。

为了这几个按键,就要把按键事件处理的源码分析一遍,这是否值得呢?我以前觉得没必要浪费时间,但是后来在工作中遇到一些任务,我发现这已经不是一个值不值得的问题,而是一个必须的事情。举一个最简单的例子,如何把一个按键映射为电源键?带着这个问题,让我们开始吧。

认识按键事件

通过 adb shell getevent 可以获取输入设备产生的元输入事件,当按下电源键,可以产生如下的按键事件

每一行数据都有固定格式,/dev/input/event0 是内核为输入设备生成的设备文件,它代表一个输入设备,它后面的数据格式为 type code value

以第一行为例,几个数据的含义如下

  1. 0001 代表输入设备产生事件的类型。此时电源键产生的是一个按键类型事件,而如果手指在触摸设备上滑动,产生是一个坐标类型事件。
  2. 0074 表示按键的扫描码(code),这个扫描码是与输入设备相关,因此不同的设备上的电源键,产生的扫描码可能不同。
  3. 00000001 表示按键值(value)。 00000001 表示按键被按下,00000000 表示按键抬起。

上面的数据看起来非常晦涩,可以通过 adb shell getevent -l 显示输入系统对这些数据的分析结果,当按下电源键,会出现如下结果

第一行和第三行数据,很清晰,分别表示按键按下和抬起。

第二条和第四条的数据是一样的,它是一个同步事件,表示需要接收这些元事件的系统对之前的数据进行同步。

同步事件,对按键事件并没有太大的作用,因为按键的按下事件或抬起事件,只用一条数据就可以表示。

如果是触摸事件,由于它有很多方面的数据,例如,x, y 坐标,压力值,等等,因此它需要多条数据来表示。那么同步事件的作用就出来了,它要求系统对一个触摸事件的所有数据进行整合。

现在我们已经对按键事件有了基本的认识,那么接下来正式开始分析流程。

处理按键事件

从前面文章可知,InputReader 从 EventHub 中获取了输入事件的数据,然后调用如下函数进行处理

InputReader 首先找到属于同一个设备的多个事件,然后交给 InputDevice 进行批量处理

虽然 InputReader 把事件交给 InputDevice 进行批量处理,但是 InputDevice 是把事件逐个交给它的 InputMapper 处理。

InputMapper 的作用是为 InputReader 加工事件。

对于键盘类型的输入设备,它的 InputMapper 实现类为 KeyboardInputMapper,它对按键事件处理如下

这时有很多与按键相关的东西,但是我现在只关心手机上电源键以及音量键的处理流程,因此这里的处理过程主要分为两步

  1. 根据按键配置文件,把扫描码(scan code)转换为按键码(key code),并从配置文件中获取策略标志位(policy flag)。不同的输入设备的同一种功能的按键,例如电源键,产生的扫描码不一定都相同,Android 系统需要把扫描码,映射为Android 系统的按键码。如果一来,Android 系统就能统一处理所有输入设备的同一种功能的按键。
  2. 创建一个事件 NotifyKeyArgs,并加入到 QueuedInputListener 队列中。从前面的文章可知,当 InputReader 处理完从 EventHub 读到的事件后,会刷新这个队列,从而把事件发送给 InputClassifier。而对于按键事件,InputClassifier 不做任何加工,直接把事件传递给 InputDispatcher。

InputReader 对按键事件的处理就这么点事儿,so eary !? 确实 easy,但是我们还有一个问题没有搞清楚,扫描码是如何映射为按键码的?

扫描码映射按键码

扫描码转化为按键码的过程有点小复杂

  1. 首先根据 kcm(key character map) 文件进行转换。
  2. 如果第一步失败,那么根据 kl(key layout) 文件进行转换。
  3. 如果前两步,有一个成功,那么再根据meta按键状态,重新使用 kcm 文件对按键码再次进行转换。这个只对键盘起作用,例如按下 shift ,再按字母键,那么会产生大写的字母的按键码。而对于电源键和音量键,此步骤可以忽略。

可以发现,kcm 和 kl 文件都可以把按键的扫描码进行转换为按键码,然而 kcm 文件一般都只是针对键盘的按键,而对于电源键和音量键,一般都是通过 kl 文件进行转换的。

注意,键盘类型的输入设备和键盘输入设备,不是同一个意思。键盘类型的输入设备,表示类型为键盘的所有输入设备,而键盘输入设备就是我们常说的键盘。

那么如何找到输入设备的 kl 文件呢?

首先通过 adb shell getevent 找到按键事件关联的设备文件节点,再通过 adb shell dumpsys input 导出所有设备的信息,包括设备节点和按键配置文件。

对比两种信息,就可以的到设备的按键配置文件,如下

从这个信息就可以看出,输入设备的 kl 文件为 /system/usr/keylayout/Generic.kl,它的电源键映射如下

其中,116是十进制,它的十六进制为 0x74,正好就是 adb shell getevent 显示的电源按键的扫描码。

POWER 就是被映射成的按键码,但是它是一个字符,而实际使用的是 int 类型,这个关系的映射是在下面定义的

因此,电源按键的扫描码 0x74,被映射为按键码 26,正好就是上层 KeyEvent.java 定义的电源按键值

在很早的 Android 版本上,配置文件中,电源键还会定义一个策略标志位,如下

其中,WAKE 就是一个策略标志位,它表示需要唤醒设备,不过在最近的 Android 版本中,去掉了这个标志位,上层在处理电源按键时,会自动唤醒设备。

结束

InputReader 处理按键事件的过程其实很简单,就是把按键事件交给 KeyboardInputMapper 处理,KeyboardInputMapper 根据配置文件,把按键的扫描码转换为按键码,并同时从配置文件中获取策略标志位,然后把这些信息包装成一个事件,发送到下一环。

现在,如果项目上让你完成功能按键的映射,或者解除某个按键的电源功能,你会了吗?

本文转自 https://juejin.cn/post/7168875586826764318,如有侵权,请联系删除。

发表回复

您的电子邮箱地址不会被公开。 必填项已用 * 标注