在Android 2.0及以前的所有版本中,获取用户消息的方式基本上都是相同的,具体如下:
-
在WmS中有一个子类KeyQ,该类基于KeyInputQueue类,而该类内部则包含一个线程对象,即KeyQ对象是一个线程对象。该线程的任务是调用native方法从输入设备中读取用户消息,包括案件、触摸屏、鼠标、轨迹球等各种消息,并把读取到的消息保存到一个QueueEvent队列中。
-
在WmS中有另外一个子类叫做InputDispatcherThread,该类也是一个线程类,即内部包含一个线程。该线程的任务就是从上面的QueueEvent队列中读取用户消息,并对这些消息进行一定的加工,然后判断应该把这个消息发送给哪个应用窗口。
-
在每一个应用窗口对象ViewRoot中都包含一个W子类,该类是一个Binder类,InputDispatchThread通过IPC方式调用W所提供的函数。从而把消息发送给对应的客户端窗口。
2.2版本中的这种处理过程有两点被Android社区所诟病。
- 对所有原始消息的加工都在KeyQ类的Java代码中完成,这加大了消息处理的延迟。
- InputDispatcherThread是通过Binder方式传递按键消息的,而Binder的延迟降低了用户操作的相应速度。
以上两点给用户的体验就是界面操作的延迟,比如滑动触摸屏,界面的移动会延迟于指尖的移动。于是从2.3开始,Android团队对消息处理逻辑进行了重构:
- 获取消息的代码全部使用C++完成,包括对消息进行加工转换。
- 抛弃了使用Binder方式传递用户消息到客户端,而是使用Linux的Pipe机制。
- 首先,InputReader线程会持续调用输入设备的驱动,读取所有用户输入的消息,该线程和InputDispatcher线程都在系统进程(system_process)空间中运行。InputDispatcher线程从自己的消息队列中取出原始消息,取出的消息有可能经过两种方式进行派发。
- 经过管道(Pipe)直接派发到客户窗口中。
- 先派发到WmS中,由WmS经过一定的处理,如果WmS没有处理该消息,则再派发给客户窗口中,否则,不派发到客户窗口。
- 应用程序添加窗口时,会在本地创建一个ViewRoot对象,然后通过IPC调用WmS中的Session对象的addWindow()方法,从而请求WmS创建一个窗口。WmS会把窗口的相关信息保存在内部的一个窗口列表类InputMonitor中,然后使用InputManager类把这些窗口信息传递给InputDispatcher线程。传递的过程中,InputManager类需要调用JNI代码,把这些窗口信息传递给NativeInputManager对象中。
- 当InputDispatcher得到用户消息后,会根据NativeInputManager中保存的所有窗口信息判断当前的活动窗口是哪个,并把消息传递给该活动窗口。另外,如果是按键消息,InputDispatcher会先回调InputManager中定义的回调函数,这既会回调InputMonitor中的回调函数,又会回调WmS中定义的相关函数,所以这些回调函数的返回值类型是boolean。对于系统按键消息,比如“Home键”、电话按键等,WmS内部会按默认的方式处理,并返回false,从而InputDispatcher不会继续把这些按键消息传递给客户窗口。对于触摸屏消息,InputDispatcher则直接传递给客户窗口。
- 在InputDispatcher和客户窗口之间使用了管道(Pipe)机制进行消息传递。Pipe是Linux的一种系统调用,Linux会在内核地址空间中开辟一段共享内存,并产生一个Pipe对象。每个Pipe对象内部都会自动创建两个文件描述符,一个用于读,另一个用于写。应用程序可以调用pipe()函数产生一个Pipe对象,并获得该对象中的读、写文件描述符。文件描述符是全局唯一的,从而使得两个进程之间可以借助这两个描述符,一个往管道中写数据,另一个从管道中读数据。管道只能是单向的,因此,如果两个进程要进行双向消息传递,必须创建两个管道。当客户窗口请求WmS创建窗口时,WmS内部会创建两个管道,其中一个管道用于InputDispatcher向客户窗口传递消息,另一个用户客户窗口向InputDispatcher报告消息的执行结果。因此,有多少个客户窗口,就有多少个管道与InputDispatcher相连。
- 由于创建管道属于Linux系统调用,Java不能直接调用,另外也由于程序结构的需要,Android中把调用管道的相关操作封装到了InputChannel.java类中,同时该类中保存了InputDispatcher和客户窗口的管道收、发描述符。因此,InputChannel也可以理解为一个“通道”,在InputDispatcher端存在一个服务端消息通道(serverChannel)。在客户窗口端存在一个客户端消息通道(clientChannel)。管道和通道的区别是,管道是Linux系统的概念,使用pipe()系统调用就可以创建一个管道,而通道是Android内部定义的一个概念,主要保存了通信双发所使用的管道描述符。
- 首先,在ViewRoot中定义了一个InputHandler对象,当底层得到按键消息后,会回调到该InputHandler对象的handleKey()函数,该函数内部发送一个异步DISPATCH_KEY消息,消息的处理函数为deliverKeyEvent(),该函数内部分以下三步执行:
- 调用mView.dispatchKeyEventPreime(),这里的PreIme的意思就是在Ime之前,即输入法之前。因为对于View系统来讲,如果有输入法窗口存在,会先将案件消息派发给输入法窗口,只有当输入法窗口没有处理该消息时,才会把消息继续派发给真正的视图。所以如果想在输入法之前处理某些按键消息,可以重写该dispatchKeyEventPreime()方法,如果该函数返回为true,则可以直接返回了,但是在返回之前如果WmS要求返回一个处理回执,则需要先调用finishInputEvent()报告给WmS已经处理的该消息,从而使得WmS可以继续派发下一个消息。
- 接下来就需要把该消息派发到输入法窗口。当然如果此时输入法窗口不存在,就直接派发到真正的视图。
- 调用deliverKeyEventToViewHierarchy(),将消息派发给真正的视图。该函数内部又分为四步:
- 调用checkForLeavingTouchModeAndConsume()判断该消息是否会导致离开触摸模式,并且会消耗掉该消息,一般情况下该函数总是会返回false。
- 调用mView.dispatchKeyEvent()将消息派发给根视图。对于应用窗口而言,根视图就是PhoneWindow的DecorView对象。而DecorView的dispatchKeyEvent内部会判断去处理音量键、系统快捷键等
- 如果应用程序中没有处理该消息,则默认会判断该消息是否会引起视图焦点的变化,如果会就进行焦点切换。
和按键派发类似,当消息获取模块通过pipe将消息传递给客户端,InputQueue中的next()函数内部调用nativePollOnce()函数中会读取该消息,如果有消息,则回调ViewRoot内部的mInputHandler对象的dispatchMothion()函数,该函数仅仅是发起一个DISPATCH_POINTER异步消息,消息的处理函数是deliverPointerEvent()。执行完该函数后,调用finishInputEvent()向消息获取模块发送一个回执,以便其进行下一次消息派发。
- 邮箱 :charon.chui@gmail.com
- Good Luck!