代码是基于android4.1的。
1recovery输入事件及处理分析
1.1时序图
1.2代码分析
1.2.1 输入事件初始化
Recovery的入口是recovery.cpp中的main函数,当然会根据参数的不同,进入recovery的模式也就不一样,这里我们就不一一介绍了,我们这里主要看图形界面模式,即有个人机交互的见面,用户可以通过按键选择不同的执行操作。
根据上面的时序图中,我们可以看到,在main函数中,需要做一些界面显示、输入事件的初始化工作。而在这里,我们就主要先关注输入事件的初始化工作,即在main函数中调用了ui.cpp的Init()方法,下面看看其代码:
void RecoveryUI::Init() { }
|
在该方法中,主要完成了两个动作,第一就是初始化话输入事件,注册了回调函数,当有输入事件的时候回调,第二就是创建了一个新的线程,用于读取输入事件的数据。
我们先看看events.c中的ev_init()方法,输入事件初始化,代码如下:
int ev_init(ev_callback input_cb, void *data) { } #define test_bit(bit, array) \ |
代码看起来还是非常简单的,输入设备的设备节点都在/dev/input/目录下,所以需要扫面下面所有的设备节点。
调用openat()打开设备节点,这是linux的系统调用,这里就不说了。
调用ioctl的,并用宏EVIOCGBIT产生参数,获取一个设备的特性,这里特性会说明该设备是按键设备还是触摸设备等,并将特性存在中数组ev_bits中。
定义了test_bit的宏,用于判断ev_bits中的特性是否是我们想要的,在linux input系统中,EV_KEY指的是按键设备,EV_REL值相对坐标,如光标移动。看到这里了,如果我们想要接受触摸消息,那么我将在这里添加EV_ABS的支持。后续会说明添加具体方法。
当打开的设备是我们想要监听的设备的时候,我们将设备节点的文件描述符等信息添加到ev_fds结构体数组中,还有将注册的回调函数input_cb添加到结构体数据ev_fdinfo中。
到此就完成了输入事件的初始化,接下来就看在创建的新线程中读取输入事件。
1.2.2 创建线程读取输入事件
在前面的RecoveryUI::Init() 方法中,我们看到了这么一句:
|
调用了pthread_create()方法创建新的线程,该方法的原型如下:
int pthread_create(pthread_t*restrict tidp,const pthread_attr_t *restrict_attr,void*(*start_rtn)(void*),void *restrict arg);
若成功则返回0,否则返回出错编号
返回成功时,由tidp指向的内存单元被设置为新创建线程的线程ID。attr参数用于制定各种不同的线程属性。新创建的线程从start_rtn函数的地址开始运行,该函数只有一个万能指针参数arg,如果需要向start_rtn函数传递的参数不止一个,那么需要把这些参数放到一个结构中,然后把这个结构的地址作为arg的参数传入。
linux下用C开发多线程程序,Linux系统下的多线程遵循POSIX线程接口,称为pthread。 由 restrict 修饰的指针是最初唯一对指针所指向的对象进行存取的方法,仅当第二个指针基于第一个时,才能对对象进行存取。对对象的存取都限定于基于由 restrict 修饰的指针表达式中。 由 restrict 修饰的指针主要用于函数形参,或指向由 malloc() 分配的内存空间。restrict 数据类型不改变程序的语义。 编译器能通过作出 restrict 修饰的指针是存取对象的唯一方法的假设,更好地优化某些类型的例程。 下面看四个参数:
第二个参数用来设置线程属性。
第三个参数是线程运行函数的起始地址。
第四个参数是运行函数的参数。
另外,在编译时注意加上-lpthread参数,以调用链接库。因为pthread并非Linux系统的默认库。
在我们这里用到的代码中,第一个参数的定义为:pthread_t input_t;为指向线程标识符的指针。接下来我们主要看新线程的入口函数input_thread()方法,代码如下:
void* RecoveryUI::input_thread(void *cookie) { } |
在一个循环里面不断的查询是否有输入事件,那么,我们看看events.c中的ev_wait()方法,代码如下:
int ev_wait(int timeout) { } |
还记得在输入事件初始化的时候,将按键等我们想监听的设备点信息添加到了ev_fds结构体中,在这里我们将用到了。
系统调用poll()方法,如果ev_fds包含的设备节点中有消息,那么返回的值r将大于0,所以ev_wait()方法的返回值为0。再回头看看input_thread()方法,当ev_wait()返回值等于零的时候,将调用ev_dispatch()派发输入消息。那么我们看看events.c中的该方法代码:
void ev_dispatch(void) { } |
找到ev_fdinfo结构体数组中之前注册的回调函数,实际是调用了ui.cpp中的RecoveryUI::input_callback()方法,下面看看其代码:
int RecoveryUI::input_callback(int fd, short revents, void* data) { } else if (ev.type == EV_REL) { } |
在之前的调用用,只是通知有输入消息,那么在回调函数中,我们就需要去读取输入消息了,定义了结构体input_event的变量ev,调用了events.c的ev_get_input()方法,看下其代码:
int ev_get_input(int fd, short revents, struct input_event *ev) { } |
在该方法中,非常简单,系统调用read()方法去读fd指定的设备节点的数据,并保存在ev变量中。
我们回到input_callback()方法中看,调用ev_get_input()后,读取到的input数据存储在ev结构体中,我们看看其结构体的定义:
struct input_event { }; |
在input_callback()方法中,我们看到了调用ui.cpp的RecoveryUI::process_key()方法处理按键消息。
1.2.3 处理按键消息
在上一小节中,讲到了在ui.cpp的RecoveryUI::process_key()方法中处理按键消息,下面我们看看其代码:
void RecoveryUI::process_key(int key_code, int updown) { } else { } |
在代码中,我们可以看到,这里指处理按键的up消息,所以我们只需要关注switch语句中的RecoveryUI::ENQUEUE:处理。
其实传进来的参数 key_code就是按键号,在这里的处理,就是把它存入key_queue[]数组中。但是我们知道,process_key()方法是在新开的线程中被调用的,当然还需要将消息传送到主线程中去,所以,这里就需要用到了互斥锁和线程阻塞和唤醒。
在主线程初始化的时候,就初始化了互斥锁和线程阻塞唤醒的条件变量,代码如下:
|
我对互斥锁的理解是被锁的代码保证当前只有一个调用着,这样可以完成代码的同步操作。
我们在设计的时候,当没有输入消息的时候,主线程会进入阻塞状态,当有输入消息的时候,将会唤醒主线程。
所以,在上面代码中,将对数组key_queue[]的赋值放到互斥锁里面,然后调用pthread_cond_signal(&key_queue_cond)唤醒主线程开始读取按键消息。
1.2.4 主线程读取输入消息
int RecoveryUI::WaitKey() { } |
这里的整个函数代码都放在互斥锁里面,是在主线程中被调用的。我们知道当没有输入消息的时候,key_queue_len=0,所以将在中 pthread_cond_timedwait()阻塞。对于这个阻塞,解除阻塞有两种方法,第一种当然是在有输入事件的时候,调用pthread_cond_signal(...)解除阻塞,第二种的timeout时间到了,会解除阻塞唤醒主线程。
所以,当有输入事件的时候,主线程被唤醒,然后将key_queue[]数组中的第一个元素赋值给key变量,然后返回。这里还有一个对key_queue[]的操作,当取出数组第一个元素的时候,将后面的元素通过指针的方式,移动到前面一位。
好了,这时候应该知道了主线程如何获取到案件消息了。下面我们将从recovery.cpp的main函数开始,讲解主线程得得到按键消息是怎么处理的。在main方法中,进入人机交互界面调用的方法是prompt_and_wait(),其代码如下:
static void prompt_and_wait(Device* device) { } |
在该方法中,有一个for的无线循环,在该循环中,调用了get_menu_selection()方法,这个方法是核心,它会经过一步步调用,最终调用到RecoveryUI::WaitKey()方法获取到按键事件,然后做出相应的处理,并返回体现用户选择的chosen_item变量,再经过InvokeMenuItem()处理,得到用户真正的操作,下面的switch方法中,就是对用户选择做出相应的动作。
那么,在这里,我们就主要关注get_menu_selection()方法,里面有我们想要的东西,其代码如下:
static int get_menu_selection(const char* const * headers, const char* const * items, } |
这里首先需要调用 ui->StartMenu(),在屏幕上显示菜单对话框,接着在一个while()循环中等待用户按键消息。这里我们看到了调用ui->WaitKey()方法,其实就是我们前面介绍的,读取按键消息,返回的是按键号,然后根据按键号,调用HandleMenuKey()方法处理,得到用户的意图,有上下移动光标,选择当前菜单进入。
好了,有了上面的介绍,那么现在我们开始加入触摸的支持了。
2 添加touch支持
有了上面的介绍,加入触摸的支持应该就不是什么难事了的。说起来应该有三个步骤:
第一,应该添加触摸消息的输入。
第二,处理触摸消息,将触摸消息处理成上、下移动、点击三种事件。
第三,添加主线程读取触摸消息及处理。
当然,还必须保证在recovery模式中,触摸驱动的加载。按照上面定的三个步骤,我们一步步来实现。
2.1添加触摸消息的输入
int ev_init(ev_callback input_cb, void *data) { } |
添加了这么一句,就可以将触摸事件的设备节点添加到了ev_fds[]结构体的数组中了。
2.2 处理触摸消息
在处理触摸消息之前,我给大家一个打印信息,是对一次触摸消息的打印:
printf,type=3,code=57,value=0 printf,type=3,code=48,value=200 printf,type=3,code=53,value=549 printf,type=3,code=54,value=596 printf,type=3,code=50,value=1 printf,type=0,code=2,value=0 printf,type=0,code=0,value=0 printf,type=3,code=57,value=0 printf,type=3,code=48,value=200 printf,type=3,code=53,value=549 printf,type=3,code=54,value=596 printf,type=3,code=50,value=1 printf,type=0,code=2,value=0 printf,type=0,code=0,value=0 printf,type=3,code=48,value=0 printf,type=0,code=0,value=0 |
我触摸了屏幕立即抽开,就有了上面的打印。在上面我们可以看到,一条打印代表一条输入消息,需要5条消息才能描述一个触摸面,上面描述了两个触摸面,两次的坐标是一样的。input_event,type=3,说明是触摸消息。下面看看input_event.code代表的意思:
#define #define #define #define #define |
好了,有了上面的一点点介绍,就可以对其处理了。
需要在回到函数中添加处理触摸事件的方法,代码如下:
int RecoveryUI::input_callback(int fd, short revents, void* data) { } |
在介绍touch_handle_input()方法方法之前,先看看我定义的几个变量及方法,在ui.h中添加如下:
struct TouchEvent{ |
定义了结构体TouchEvent用来存储触摸事件的x、y轴坐标,因为触摸屏支持5个手指触摸,所以mTouchEvent[5]数组分别存储5个手指的坐标,lastEvent表示最新的一个触摸坐标,firstEvent表示触摸按下的第一个坐标。
我自己定义了touch_handle_input()方法,处理触摸消息,其定义在ui.cpp中,代码如下:
int RecoveryUI::touch_handle_input(input_event ev){ } |
其实代码量也没多少的。触摸消息是一条条传过来的,而且需要5条消息才能描述一个触摸面,所以需要定义一些变量来存储消息。
对于滑动消息,我是根据一次滑动的距离为20个坐标点的时候,认为是一次up、或down动作,而根据第一次触摸的坐标和离开的时候的坐标相等的时候,认为is一次点击动作。
我们看下手指离开触摸屏的处理,当手指离开的时候,如果firstEvent.y==lastEvent.y,那么然为是一个点击的动作,则需要作出响应。GetScreenPara()方法是在screen_ui.cpp中定义的,代码如下:
int* ScreenRecoveryUI::GetScreenPara() { } |
其实这个方法就是获取了当前显示菜单的布局数据,通过这三个数据,我就可以计算出每列菜单所在的坐标范围,举个列子,用户可以选择的第2个菜单的坐标范围应该是从
(menu_top+1)*CHAR_HEIGHT到(menu_top+2)*CHAR_HEIGHT之间。有了这些数据,我就可以根据点击的y坐标,计算出用户点击的是哪一列菜单了。
好了,触摸处理的三个动作,我存储在一个变量touch_code中,触摸消息与按键消息公用一个数据数组key_queue[],所以为了避免与按键消息的冲突,我将触摸消息处理后定义了负数,存在touch_code变量中。描述三个动作的三个变量,是在Device中原来已经定义好了的:
Device::kInvokeItem = -4
Device::kHighlightDown = -3 //向下移动
Device::kHighlightUp = -2 //向上移动
2.3 主线程处理触摸消息
static int get_menu_selection(const char* const * headers, const char* const * items, } |