触摸屏与键盘是Android最普遍也是最标准的输入设备。其实Android所支持的输入设备的种类不止这两个,鼠标、游戏手柄均在内建的支持之列。当输入设备可用时,Linux内核会在/dev/input/下创建对应的名为event0~n或其他名称的设备节点。而当输入设备不可用时,则会将对应的节点删除。在用户空间可以通过ioctl的方式从这些设备节点中获取其对应的输入设备的类型、厂商、描述等信息。
当用户操作输入设备时,Linux内核接收到相应的硬件中断,然后将中断加工成原始的输入事件数据并写入其对应的设备节点中,在用户空间可以通过read()函数将事件数据读出。
Android输入系统的工作原理概括来说,就是监控/dev/input/下的所有设备节点,当某个节点有数据可读时,将数据读出并进行一系列的翻译加工,然后在所有的窗口中寻找合适的事件接收者,并派发给它。
整个输入系统大致可以概括为三个部分:
读取—–>分发—–>处理
下面分析一下“读取”和“分发”这两个部分启动过程的源码。
首先输入系统对读取和分发创建了两个线程,分别是“InputReaderThread ”和“InputDispatcherThread”
InputReaderThread:这个线程在Dispatcher线程之后才启动,主要功能是读取驱动程序获得输入事件,处理完后发给Dispatcher线程,并且监测设备是否有拔插。
InputDispatcherThread:这个线程比Reader线程先启动,主要负责分发输入事件,为什么这个先启动呢?因为如果Reader先启动而Dispatcher还未启动,此时如果有输入事件,就可能导致输入事件不能得到处理而导致数据丢失。
下面分别分析一下“Reader线程”和“Dispatcher线程”的实现过程:
先来分析一下“Reader线程”,它使用的是android系统里面的EventHub来读取事件的,对应的android源码文件为“EventHub.h”,里面它用结构体来表示和保存输入事件,即这个输入事件包含的属性都用这个结构体来表示,结构体的内容如下:
struct RawEvent {
nsecs_t when;
int32_t deviceId;
int32_t type;
int32_t code;
int32_t value;
};
结构体参数中的“type”包含有以下一些输入类型:
DEVICE_ADDED
DEVICE_REMOVED
FINISHED_DEVICE_SCAN
EV_KEY
EV_ABS
EV_REL
…
上面这个结构体是android里面实现的,为了兼容不同的输入设备,驱动程序自然需要去适配android的输入系统才能保证任何一钟输入设备都能直接使用在android系统上,这里我们先看一下linux驱动程序上报了些什么输入事件,中间又是怎么经过转换去适配andorid的输入系统的。
首先看下linux驱动程序上报的输入事件格式,对应的内核源码文件是“input.h”,里面可以看到有个结构体如下:
struct input_event {
struct timeval time;
__u16 type;
__u16 code;
__s32 value;
};
对应的输入类型有如下:
Type:
EV_KEY
EV_ABS
EV_REL
…
这里可以看到有跟上面android的输入系统一样的的输入类型,但是少了例如“DEVICE_ADDED”“DEVICE_REMOVED”等一些类型,这些是android系统用来做为其它处理使用的,我们可以发现他们的结构体名字并不相同,所以最终android系统会将“input_event ”构造为“RawEvent”。
那么EventHub又是如何监测设备拔插的呢?
前面一章讲过一个基础知识,就是“inotify”和“epoll”,inotify监测“/dev/input”来判断目录下有无输入设备,使用epoll监测设备有无数据,具体的细节如下:
fd1 = inotify.init(“/dev/input”) //使用inotify.init来监测文件目录得到一个文件句柄fd1。
fd2 = open(“/dev/input/event0”) //打开设备节点
fd3 = open(“/dev/input/event1”)使用epoll_wait监测fd1、fd2、fd3
当Epoll返回的时候就表示监测到数据了,如果返回的文件句柄是fd1的话就表示有设备节点被创建或删除了,如果是创建的话getEvents函数就会构造出一个Raw_Event结构体,里面的type成员就等于”DEVICE_ADDED”,监测到是删除的话就是” DEVICE_REMOVED”,如果返回的文件句柄是fd2或fd3话就表示这些输入设备有数据了,getEvents函数就会去读取驱动程序得到原始的input_event结构体,用它来构造出一个RawEvent结构体,它的type就等于具体的输入事件
Reader线程的核心类:
mEventHub类(它是用来表示多个输入设备的):
成员有:
—–mDevices(里面是一个个(记录好的)输入设备,根据编号和device指针)
—–device(里面包含fd、信息、映射信息)
———–其中信息包含以下内容:
———–identify(包含:name、bus、VID(厂家ID)、PID(设备ID) )根据这些信息来找到配置文件,这些信息是通过ioctrl访问驱动程序得来的,会根据这些信息来打开配置文件(包含三种配置文件:IDC(input device conf)、keylayout、kcm(key char map))
Kl(即keylayout)配置文件:假设linux内核下的‘1’按键编码为2,而android下的‘1’按键编码为8,这中间就需要一个配置文件做转换,我们可以自己写一个配置文件,这个文件放在 “/data/system/devices/keylayout/* * *.kl”下,编写时可以参考“system/usr/keylayout/Generic.kl
kl文件格式:
key 17(内核中的code值) W(AKEYCODE_W->即android的按键W)
假设我们想在kl文件里面自定义两项(*键和#键)
,在kl文件直接添加如下两项即可:
Key 227 STAR———–内核上报code值为227
Key 228 POUND——–内核上报code值为228
227表示:scancode ——— START表示:android keycode 表示按下的*键
这里我们可以测试验证一下我们自定义的扫描值有没有起作用,测试使用的是自己编写好的“模拟输入驱动程序”,代码如下,代码都有备注:
/* 参考drivers\input\keyboard\gpio_keys.c */
#include <linux/module.h>
#include <linux/version.h>
#include <linux/init.h>
#include <linux/fs.h>
#include <linux/input.h>
static struct input_dev *input_emulator_dev;
static int input_emulator_init(void)
{
int i;
/* 1. 分配一个input_dev结构体 */
input_emulator_dev = input_allocate_device();;
/* 2. 设置 */
/* 2.1 能产生哪类事件 */
set_bit(EV_KEY, input_emulator_dev->evbit);//按键类事件
set_bit(EV_REP, input_emulator_dev->evbit);//长按时会重复上报事件
/* 2.2 能产生所有的按键 */
for (i = 0; i < BITS_TO_LONGS(KEY_CNT); i++)
input_emulator_dev->keybit[i] = ~0UL;//把keybit每一位都设置为1(即使能)
/* 2.3 为android构造一些设备信息 */
input_emulator_dev->name = "InputEmulatortest";
input_emulator_dev->id.bustype = 1;
input_emulator_dev->id.vendor = 0x1234;//暂时先随意定义
input_emulator_dev->id.product = 0x5678;//暂时先随意定义
input_emulator_dev->id.version = 1;
/* 3. 注册 */
input_register_device(input_emulator_dev);
return 0;
}
static void input_emulator_exit(void)
{
input_unregister_device(input_emulator_dev);
input_free_device(input_emulator_dev);
}
module_init(input_emulator_init);
module_exit(input_emulator_exit);
MODULE_LICENSE("GPL");
之后使用gcc编译生成“.ko”文件即可。
测试过程如下:
在android命令行输入下面指令修改权限:
busybox chmod 777 /data/system/devices -Rreboot 重启开发板
insmod InputEmulator.ko 加载驱动
会显示:
[ 182.812000] input: InputEmulatortest as /devices/virtual/input/input4
即生成input4这个设备节点,对应event4发送‘*’键测试(一次性发送3条):
sendevent /dev/input/event4 1 227 1
sendevent /dev/input/event4 1 227 0
sendevent /dev/input/event4 0 0 0发送‘#’键测试:
sendevent /dev/input/event4 1 228 1
sendevent /dev/input/event4 1 228 0
sendevent /dev/input/event4 0 0 0我们打开记事本或浏览器等有输入栏的APP,输入上面的指令,即模拟驱动设备上传scancode码,可以看到会跟键盘输入的一样,出现‘*’和‘#’字符
这里还需要补充一个知识点:
keylayout: 只是用来表示驱动上报的scancode对应哪一个android按键(AKEYCODE_x),它只是表示按键被按下,具体对应哪一个字符,由kcm文件决定。
kcm(key char map): 用来表示android按键(AKEYCODE_x)对应哪一个字符,表示同时按下其他按键后,对应哪个字符。
如果你想修改按键按下时系统默认的对应的字符,可以自行修改kcm文件,它的格式如下:
key B {
label: ‘B’ # 印在按键上的文字
base: ‘b’ # 如果没有其他按键(shift, ctrl等)同时按下,此按键对应的字符是’b’
shift, capslock: ‘B’
}
即收到按键‘B’的时候默认显示的是‘b’,可自行修改成你想要显示的值
Reader线程可以总结如下:
在reader线程里使用EventHub这个类的对象来表示多个输入设备,使用KeyedVector来表示这个设备,KeyedVector形参中有编号和输入设备(device *),device指针里面有fd(表示打开的这个设备的文件句柄),还有identify(通过ioctrl来获得设备的一些信息,上面有详细介绍),里面还有configurationFile表示idc文件名,configuration则表示idc属性,有哪些属性呢(例如按键是内嵌的还是外挂的),android打开设备的时候还会加载一些配置文件,即KeyMap,里面包含有kl文件(keylayoutMap:把扫描码转为andorid系统使用的keycode)和kcm(keycharacterMap)文件。转换成功后构造成一个args,然后上报给下一级的“Dispatcher”线程处理。
下面简述一下Dispatcher的大致处理过程:
InputDispatcher(分发)线程:
发什么?
先获得InputReader线程发送过来的事件,将它们放入队列里面
InputReader线程将事件“放入”mInboundQueue
——–>放入队列前先稍加处理(先分类,先处理紧急的事件,比如来电静音)
然后从InputReader“取出”事件后还需稍加处理:
——–>对于global/system按键,先处理掉,即放入mcommandQueue后依次处理
——–>对于user按键(要发送给APP的按键)会放入队列里面,哪个队列呢?
——–>先查找目标APP,得到connection
——–>然后放入这个connection的outboundQueue最后发给谁?
发给APP
选择目标APP—>发送给它