Android输入系统简述

触摸屏与键盘是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监测设备有无数据,具体的细节如下:

  1. fd1 = inotify.init(“/dev/input”) //使用inotify.init来监测文件目录得到一个文件句柄fd1。

  2. fd2 = open(“/dev/input/event0”) //打开设备节点
    fd3 = open(“/dev/input/event1”)

  3. 使用epoll_wait监测fd1、fd2、fd3

  4. 当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 -R

reboot 重启开发板

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(分发)线程:

  1. 发什么?
    先获得InputReader线程发送过来的事件,将它们放入队列里面
    InputReader线程将事件“放入”mInboundQueue
    ——–>放入队列前先稍加处理(先分类,先处理紧急的事件,比如来电静音)
    然后从InputReader“取出”事件后还需稍加处理:
    ——–>对于global/system按键,先处理掉,即放入mcommandQueue后依次处理
    ——–>对于user按键(要发送给APP的按键)会放入队列里面,哪个队列呢?
    ——–>先查找目标APP,得到connection
    ——–>然后放入这个connection的outboundQueue

  2. 最后发给谁?
    发给APP
    选择目标APP—>发送给它

Android系统的四种基本组件是Activity、Service、Broadcast Receiver和Content Provider。 1. Activity(活动):Activity是Android中用户界面的展示单元,用于用户与应用程序进行交互。每个Activity都是一个独立的页面,用户可以通过点击按钮、输入文本等操作与Activity进行交互。Activity可以包含布局文件,用于定义界面的外观和交互行为。通过Activity,用户可以浏览应用的不同页面,并执行各种操作。 2. Service(服务):Service是在后台执行长时间运行操作的组件,与用户界面无关。Service可以在后台下载文件、播放音乐、执行网络请求等多种任务,而不会妨碍用户与应用的交互。Service不可见,但可以通过调用startService()或bindService()方法来启动或绑定Service。 3. Broadcast Receiver(广播接收器):Broadcast Receiver是用于接收并响应系统或应用中的广播消息的组件。广播消息可以是系统事件(如电量低提示)或其他应用发送的自定义广播。Broadcast Receiver可以注册和监听指定类型的广播消息,并在接收到广播时执行相应的操作,如通知用户、更新数据等。 4. Content Provider(内容提供器):Content Provider是用于在应用程序之间共享数据的组件。它允许应用程序将数据存储在一个中央位置,并提供数据的访问接口供其他应用程序使用。Content Provider可以对数据进行增删改查的操作,并通过URI来标识数据的位置和访问权限。其他应用程序可以通过Content Resolver访问Content Provider提供的数据。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值