深入理解Android输入系统

1 . 研究输入事件从设备节点开始到窗口处理函数的流程;
2 . 介绍原始输入事件的读取与加工的原理;
3 . 研究事件派发机制;
4 . 讨论事件在输入系统与窗口之间传递与反馈的过程;
5 . 介绍焦点窗口的选择、ANR的产生以及软件方式模拟用户操作的原理;

1 . 初识Android输入系统

  接下来讲两点:输入设备和输入事件。
  触摸屏和键盘是Android最普遍的也是最标准的输入设备,其实Android所支持的输入设备的种类不止这两个,鼠标、游戏手柄都可以。当输入设备可用时,Linux内核会在/dev/input/目录下创建对应的名为event0、event1...eventn的设备节点。而当输入设备不可用时,则会将对应的节点删除。

  使用以下代码可以查看输入设备节点:

//进入adb shell;
adb shell
//进入到/dev/input/目录;
cd /dev/input/
//查看该目录下的输入节点;
ls

  当用户操作输入设备时,Linux内核接收到相应的硬件中断,然后将中断加工成原始的输入事件数据并写入其对应的设备节点中,在用户空间可以通过read()函数将事件数据读出。
  Android输入系统的工作原理概括来说,就是监控/dev/input/下的所有设备节点,当某个节点有数据可读时,将数据读出并进行一系列的翻译加工,然后在所有的窗口中寻找合适的事件接收者,并派发给它。

1.1 getevent与sendevent工具

  Android系统提供了getevent与sendevent两个工具让开发者从设备节点中直接读取输入事件或者写入输入事件。

getevent

  getevent监听输入设备节点的内容,当输入事件被写入节点时,getevent会将其读出并打印在屏幕上。由于getevent不会对事件数据做任何加工,因此其输出的内容是由内核提供的最原始的事件。
  其用法如下:

//device_path是可选参数,用以指明需要监听的设备节点路径。如果省略此参数,则监听所有设备节点的事件;
adb shell getevent [-选项] [device_path]

//-t 表示打印事件的事件戳;
adb shell getevent -t /dev/input/event0

adb shell getevent -t

  以我的手机为例,使用adb shell getevent -t指令,按下和松开音量上键打印的数据如下:

//按下音量上键;
[1218774.992268] /dev/input/event5:0001 0073 00000001
[1218774.992268] /dev/input/event5:0000 0000 00000000

//松开音量上键;
[1218775.188299] /dev/input/event5:0001 0073 00000000
[1218775.188299] /dev/input/event5:0000 0000 00000000

  注意上面输出的是十六进制的。每条数据有五项信息:产生事件时的时间戳([1218774.992268])、产生事件的设备节点(/dev/input/event5)、事件类型(0001)、事件代码(0073)、事件的值(00000001)。其中时间戳、事件类型、事件代码、事件的值是原始事件的四项基本元素。除时间戳之外,其他三项元素的实际意义依照设备类型及厂商的不同而有所区别。
  在上面例子中,类型0001表示此事件是一条按键事件,代码0073表示音量上键,值00000001表示按下,值00000000表示抬起。这两条原始数据被输入系统包装成两个KeyEvent对象,作为两个按键事件派发给Framework中感兴趣的模块或者应用程序。

  注意:一个原始事件所包含的信息量是比较有限的。而在Android API中所使用的某些输入事件,如触摸屏点击/滑动,其中包含了很多的信息,比如X、Y坐标、触摸点索引等,其实是输入系统整合了多个原始事件后的结果。

sendevent

  输入设备的节点不仅在用户空间可读,而且是可写的。因此可以将原始事件写入节点中,从而实现模拟用户输入的功能。sendevent工具的作用正是如此。用法如下:

sendevent <节点路径> <类型> <代码> <值>

  注意,比如上面例子中输出的是二进制,但是我们输入的时候是十进制,所以我们要把数据转化为十进制。

  我的手机没有root,无法进行写入的操作。

1.2 Android输入系统简介

  输入事件的源头是位于/dev/input/目录下的设备节点,而输入系统的终点是由WMS管理的某个窗口。最初的输入事件为内核生成的原始事件,而最终交付给窗口的则是KeyEvent或MotionEvent对象。因此Android输入系统的主要工作是读取设备节点中的原始事件,将其加工封装,然后派发给一个特定的窗口以及窗口中的控件。这个过程由InputManagerService(简称IMS)系统服务为核心的多个参与者共同完成。

  输入系统的总体流程和参与者的流程图,如下所示:

1 . Linux内核:接收输入设备的中断,并将原始事件的数据写入设备节点中;
2 . 设备节点:作为内核与IMS的桥梁,它将原始事件的数据暴露给用户空间,以便IMS可以从中读取事件;
3 . InputManagerService:一个Android系统服务,它分为Java层与Native层两部分。Java层负责与WMS通信。而Native层则是InputReader和InputDispatcher两个输入系统关键组件的运行容器;
4 . EventHub:直接访问所有的设备节点。通过一个名为getEvents()的函数将所有输入系统相关的待处理的底层事件返回给使用者。这些事件包括原始输入事件、设备节点的增删等;
5 . InputReader:是IMS中的关键组件之一。它运行于一个独立的线程中,负责管理输入设备的列表与配置,以及进行输入事件的加工处理。它通过其线程循环不断地通过getEvents()函数从EventHub中将事件取出并进行处理。对于设备节点的增删事件,它会更新输入设备列表与配置。对于原始输入事件,InputReader对其进行翻译、组装、封装为包含更多信息、更具可读性的输入事件,然后交给InputDispatcher进行派发;
6 . InputReaderPolicy:为InputReader的事件加工处理提供一些策略配置,例如键盘布局信息等;
7 . InputDispatcher:是IMS中的另一个关键组件。它也是运行于一个独立的线程中。InputDispatcher中保管了来自WMS的所有窗口的信息,其收到来自InputReader的输入事件后,会在其保管的窗口中寻找合适的窗口,并将事件派发给此窗口;
8 . InputDispatcherPolicy:为InputDispatcher的派发过程提供策略控制。例如截取某些特定的输入事件用作特殊用途,或者阻止将某些事件派发给目标窗口。一个典型的例子就是HOME键被InputDispatcherPolicy截取到PhoneWindowManager中进行处理,并阻止窗口收到HOME键按下的事件;
9 . WMS:虽然这个不是输入系统中的一员,但是它却对InputDispatcher的正常工作起到了至关重要的作用。当新建窗口时,WMS为新窗口和IMS创建了事件传递用的通道。另外,WMS还将所有窗口的信息,包括窗口的可点击区域、焦点窗口等信息,实时地更新到IMS的InputDispatcher中,使得InputDispatcher可以正确地将事件派发到指定的窗口;
10 . ViewRootImpl:对某些窗口,如壁纸窗口、SurfaceView的窗口来说,窗口就是输入事件派发的终点。而对其他的,比如Activity、对话框等使用了Android控件系统的窗口来说,输入事件的终点是控件。ViewRootImpl将窗口所接收的输入事件沿着控件树将事件派发给感兴趣的控件;

  简单来说,内核将原始事件写入设备节点中,InputReader不断地通过EventHub将原始事件取出来并翻译加工成Android输入事件,然后交给InputDispatcher。InputDispatcher根据WMS提供的窗口信息将事件交给合适的窗口。窗口的ViewRootImpl对象再沿着控件树将事件派发给感兴趣的控件。控件对其收到的事件作出响应,更新自己的画面、执行特定的动作。所有这些参与者以IMS为核心,构建了Android庞大而复杂的输入体系。

1.3 IMS的构成

  IMS分为Java层与Native层两个部分,其启动过程是从Java部分的初始化开始,进而完成Native部分的初始化。
  同其他系统服务一样,IMS在SystemServer中的ServerThread线程中启动。
  在framework/base/services/java/com/android/server/SystemServer.java类中。

private void run(){
    //---;
    startBootstrapService();
    startCoreService();
    startOtherServices();
    //---;
}

private void startOtherServices(){
    //---;
    InputManagerService inputManager = null;
    //新建IMS对象;
    inputManager = new InputManagerService(context);
    wm = WindowManagerService.main(context,inputManager,mFactoryTestMode != FactoryTest.FACTORY_TEST_LOW_LEVEL,!mFirstBoot,mOnlyCore,new PhoneWindowManager());
    ServiceManager.addService(Context.WINDOW_SERVICE,wm);
    //将IMS发布给ServiceManager,以便其他人可以访问IMS提供的接口;
    ServiceManager.addService(Context.INPUT_SERVICE,inputManager);
    //设置向WMS发起回调的callback对象;
    inputManager.setWindowManagerCallbacks(wm.getInputMonitor());
    //正式启动IMS;
    inputManager.start();
    
    final InputManagerService inputManagerF = inputManager;
    if(inputManagerF != null){
        inputManagerF.systemRunning();
    }
}

  IMS的诞生分为两个阶段:
1 . 创建新的IMS对象;
2 . 调用IMS对象的start()函数完成启动;

(1) . IMS的创建:
  IMS的构造函数如下:

public InputManagerService(Context context) {
    this.mContext = context;
    //使用DisplayThread的Looper新建一个InputManagerHandler,InputManagerHandler将运行在WMS的主线程中;
    this.mHandler = new InputManagerHandler(DisplayThread.get().getLooper());
    mUseDevInputEventForAudioJack = context.getResources().getBoolean(R.bool.config_useDevInputEventForAudioJack);
    Slog.i(TAG, "Initializing input manager, mUseDevInputEventForAudioJack=" + mUseDevInputEventForAudioJack);
    //每一个分为Java和Native两部分的对象在创建时都会有一个这样的函数;
    mPtr = nativeInit(this, mContext, mHandler.getLooper().getQueue());
    String doubleTouchGestureEnablePath = context.getResources().getString(R.string.config_doubleTouchGestureEnableFile);
    mDoubleTouchGestureEnableFile = TextUtils.isEmpty(doubleTouchGestureEnablePath) ? null : new File(doubleTouchGestureEnablePath);
    LocalServices.addService(InputManagerInternal.class, new LocalService());
}

  可以看出,IMS的构造函数非常简单,绝大部分的初始化工作都位于Native层。参考nativeInit()函数的实现。

  nativeInit()函数创建了一个类型为NativeInputManager对象,它是Java层与Native层互相通信的桥梁。在NativeInputManager中实现了InputReaderPolicyInterface和InputDispatcherPolicyInterface两个接口。也就说明了先前说的InputReaderPolicy和InputDispatcherPolicy是由NativeInputManager实现的,然而它仅仅为两个策略提供接口实现而已,并不是策略的实际实现者。NativeInputManager通过JNI回调Java层的IMS,由它完成决策。
  在NativeInputManager的构造函数中,创建了两个关键的类,分别是EventHub和InputManager。EventHub复杂的构造函数使其在创建后便拥有了监听设备节点的能力。
  InputManager的构造函数创建了四个对象,分别为IMS的核心参与者InputReader和InputDispatcher,以及它们所在的线程InputReaderThread和InputDispatcherThread。注意InputManager的构造函数的参数readerPolicy与dispatcherPolicy,它们都是NativeInputManager中的。
  到了这里,IMS创建完成。在这个过程中,输入系统的重要参与者均完成创建。

(2) . IMS的启动与运行
  完成IMS的创建之后,ServerThread执行InputManagerService.start()函数以启动IMS。InputManager的创建过程分别为InputReader与InputDispatcher创建了承载它们运行的线程,然而并未启动这两个线程,因此IMS的参与者仍处于待命状态。此时start()函数的功能就是启动这两个线程,使得InputReader与InputDispatcher开始工作。

  当两个线程启动后,InputReader在其线程循环中不断地从EventHub中抽取原始输入事件,进行加工处理后将加工所得的事件放入InputDispatcher的派发队列中。InputDispatcher则在其线程循环中将派发队列中的事件取出,查找合适的窗口,将事件写入窗口的事件接收管道中。窗口事件接收线程的Looper从管道中将事件取出,交由事件处理函数进行事件响应。

2 . 原始事件的读取与加工

3 . 输入事件的派发

4 . 输入事件的发送、接收与反馈

5 . 关于输入系统的其他重要话题

-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-----------------last line for now-----------------