Android的用户输入处理
Android的用户输入系统获取用户按键(或模拟按键)输入,分发给特定的模块(Framework或应用程序)进行处理,它涉及到如下一些模块:html
- Input Reader: 负责从硬件获取输入,转换成事件(Event), 并分发给Input Dispatcher.
- Input Dispatcher: 将Input Reader传送过来的Events 分发给合适的窗口,并监控ANR。
- Input Manager Service: 负责Input Reader 和 Input Dispatchor的建立,并提供Policy 用于Events的预处理。
- Window Manager Service:管理Input Manager 与 View(Window) 以及 ActivityManager 之间的通讯。
- View and Activity:接收按键并处理。
- ActivityManager Service:ANR 处理。
它们之间的关系以下图所示(黑色箭头表明控制信号传递方向,而红色箭头表明用户输入数据的传递方向)。java

这块代码不少,但相对来讲不难理解,按照惯例,咱们先用一张大图(点击看大图)鸟瞰一下全貌先。android

四种不一样颜色表明了四个不一样的线程, InputReader Thread,InputDispatch Thread 和 Server Thread 存在于SystemServer进程里。UI Thread则存在于Activity所在进程。颜色较深部分是比较重要,须要重点分析的模块。app
初始化异步
整个输入系统的初始化能够划分为Java 和 Native两个部分,能够用两张时序图分别描述,首先看Java端,
函数

- 在SystemServer的初始化过程当中,InputManagerService 被建立出来,它作的第一件事情就是初始化Native层,包括EventHub, InputReader 和 InputDispatcher,这一部分咱们将在后面详细介绍。
- 当InputManager Service 以及其余的System Service 初始化完成以后,应用程序就开始启动。若是一个应用程序有Activity(只有Activit可以接受用户输入),它要将本身的Window(ViewRoot)经过setView()注册到Window Manager Service 中。(详见图解Android - Android GUI 系统 (2) - 窗口管理 (View, Canvas, Window Manager))。
- 用户输入的捕捉和处理发生在不一样的进程里(生产者:Input Reader 和 Input Dispatcher 在System Server 进程里,而消耗者,应用程序运行在本身的进程里),所以用户输入事件(Event)的传递须要跨进程。在这里,Android使用了Socket 而不是 Binder来完成。OpenInputChannelPair 生成了两个Socket的FD, 表明一个双向通道的两端,向一端写入数据,另一端即可以读出,反之依然,若是一端没有写入数据,另一端去读,则陷入阻塞等待。OpenInputChannelPair() 发生在WindowManager Service 内部。为何不用binder? 我的的分析是,Socket能够实现异步的通知,且只须要两个线程参与(Pipe两端各一个),假设系统有N个应用程序,跟输入处理相关的线程数目是 n+1 (1是发送(Input Dispatcher)线程)。然而,若是用Binder实现的话,为了实现异步接收,每一个应用程序须要两个线程,一个Binder线程,一个后台处理线程,(不能在Binder线程里处理输入,由于这样太耗时,将会堵塞住发送端的调用线程)。在发送端,一样须要两个线程,一个发送线程,一个接收线程来接收应用的完成通知,因此,N个应用程序须要 2(N+1)个线程。相比之下,Socket仍是高效多了。
- 经过RegisterInputChannel, Window Manager Service 将刚刚建立的一个Socket FD,封装在InputWindowHandle(表明一个WindowState) 里传给InputManagerService。
- InputManagerService 经过JNI(NativeInputManager)最终调用到了InputDispatchor 的 RegisterInputChannel()方法,这里,一个Connection 对象被建立出来,表明与远端某个窗口(InputWindowHandle)的一条用户输入数据通道。一个Dispatcher可能有多个Connection(多个Window)同时存在。为了监听来自于Window的消息,InputDispator 经过AddFd 将这些个FD 加入到Looper中,这样,只要某个Window在Socket的另外一端写入数据,Looper就会立刻从睡眠中醒来,进行处理。
- 到这里,ViewRootImpl 的 AddWindow 返回,WMS 将SocketPair的另一个FD 放在返回参数 OutputChannel 里。
- 接着ViewRootImpl 建立了WindowInputEventReceiver 用于接受InputDispatchor 传过来的事件,后者一样经过AddFd() 将读端的Socket FD 加入到Looper中,这样一旦InputDispatchor发送Event,Looper就会当即醒来处理。
接下来看刚才没有讲完的NativeInit。oop

- NativeInit 是 NativeInputManager类的一个方法,在InputManagerService的构造函数中被调用。代码在 frameworks/base/services/jni/com_android_server_input_inputManagerService.cpp.
- 首先建立一个EventHub, 用来监听全部的event输入。
- 建立一个InputDispatchor对象。
- 建立一个InputReader对象,他的输入是EventHub, 输出是InputDispatchor。
- 而后分别为InputReader 和 InputDispatchor 建立各自的线程。注意,当前运行在System Server 的 WMThread线程里。
- 接着,InputManagerService 调用NativeStart 通知InputReader 和 InputDispatchor 开始工做。
- InputDispatchor是InputReader的消费者,它的线程首先启动,进入Looper等待状态。
- 接着 InputReader 线程启动,等待用户输入的发生。
至此,一切准备工做就绪,万事具有,之欠用户一击了。 ui
Eventhub 和 Input Reader
Android设备能够同时链接多个输入设备,好比说触摸屏,键盘,鼠标等等。用户在任何一个设备上的输入就会产生一个中断,经由Linux内核的中断处理以及设备驱动转换成一个Event,并传递给用户空间的应用程序进行处理。每一个输入设备都有本身的驱动程序,数据接口也不尽相同,如何在一个线程里(上面说过只有一个InputReader Thread)把全部的用户输入都给捕捉到? 这首先要归功于Linux 内核的输入子系统(Input Subsystem), 它在各类各样的设备驱动程序上加了一个抽象层,只要底层的设备驱动程序按照这层抽象接口来实现,上层应用就能够经过统一的接口来访问全部的输入设备。这个抽象层有三个重要的概念,input handler, input handle 和 input_dev,它们的关系以下图所示:spa

- input_dev 表明底层的设备,好比图中的“USB keyboard" 或 "Power Button" (PC的电源键),全部设备的input_dev 对象保存在一个全局的input_dev 队列里。
- input_handler 表明某类输入设备的处理方法,好比说 evdev就是专门处理输入设备产成的Event(事件),而“sysrq" 是专门处理键盘上“sysrq"与其余按键组合产生的系统请求,好比“ALT+SysRq+p"(先Ctrl+ALT+F1切换到虚拟终端)能够打印当前CPU的寄存器值。全部的input_handler 存放在 input_handler队列里。
- 一个input_dev 能够有多个input_handler, 好比下图中“USB Mouse" 设备能够由”evdev" 和 “mousedev" 来分别处理它产生的输入。
- 一样,一个input_handler 能够用于多种输入设备,好比“USB Keyboard", "Power Button" 均可以产成Event,因此,这些Event均可以交由evdev进行处理。
- Input handle 用来关联某个input_dev 和 某个 input_handler, 它对应于下图中的紫色的原点。每一个input handle 都会生成一个文件节点,好比图中4个 evdev的handle就对应与 /dev/input/下的四个文件"event0~3". 经过input handle, 能够找到对应的input_handler 和 input_dev.
简单说来,input_dev对应于底层驱动,而input_handler是个上层驱动,而input_handle 提供给应用程序标准的文件访问接口来打通这条上下通道。经过Linux input system获取用户输入的流程简单以下:线程
- 设备经过input_register_dev 将本身的驱动注册到Input 系统。
- 各类Handler 经过 input_register_handler将本身注册到Input系统中。
- 每个注册进来的input_dev 或 Input_handler 都会经过input_connect() 寻找对方,生成对应的 input_handle,并在/dev/input/下产成一个设备节点文件.
- 应用程序经过打开(Open)Input_handle对应的文件节点,打开其对应的input_dev 和 input_handler的驱动。这样,当用户按键时,底层驱动就能捕捉到,并交给对应的上次驱动(handler)进行处理,而后返回给应用程序,流程以下图中红色箭头所示。
上图中的深色点就是 Input Handle, 左边垂直方向是Input Handler, 而水平方向是Input Dev。 下面是更为详细的一个流程图,感兴趣的同窗能够点击大图看看。

因此,只要打开 /dev/input/ 下的全部 event* 设备文件,咱们就能够有办法获取全部输入设备的输入事件,无论它是触摸屏,仍是一个USB 设备,仍是一个红外遥控器。Android中完成这个工做的就是EventHub。
EventHub实如今 framework/base/services/input/EventHub.cpp, 它和InputReader 的工做流程以下图所示:
- NativeInputManager的构造函数里第一件事情就是建立一个EventHub对象,它的构造函数里主要生成并初始化几个控制的FD:
- mINotifyFd: 用来监控""/dev/input"目录下是否有文件生成,有的话说明有新的输入设备接入,EventHub将从epool_wait中唤醒,来打开新加入的设备。
- mWakeReaderFD, mWakeWriterFD: 一个Pipe的两端,当往mWakeWriteFD 写入数据的时候,等待在mWakeReaderFD的线程被唤醒,这里用来给上层应用提供唤醒等待线程,好比说,当上层应用改变输入属性须要EventHub进行相应更新时。
- mEpollFD,用于epoll_wait()的阻塞等待,这里经过epoll_ctrl(EPOLL_ADD_FD, fd) 能够等待多个fd的事件,包括上面提到的mINotifyFD, mWakeReaderFD, 以及输入设备的FD。
- 紧接着,InputManagerService启动InputReader 线程,进入无限的循环,每次循环调用loopOnce(). 第一次循环,会主动扫描 "/dev/input/" 目录,并打开下面的全部文件,经过ioctl()从底层驱动获取设备信息,并判断它的设备类型。这里处理的设备类型有:INPUT_DEVICE_CLASS_KEYBOARD, INPUT_DEVICE_CLASS_TOUCH, INPUT_DEVICE_CLASS_DPAD,INPUT_DEVICE_CLASS_JOYSTICK 等。
- 找到每一个设备对应的键值映射文件,读取并生产一个KeyMap 对象。通常来讲,设备对应的键值映射文件是 "/system/usr/keylayout/Vendor_%04x_Product_%04x".
- 将刚才扫描到的/dev/input 下全部文件的FD 加到epool等待队列中,调用epool_wait() 开始等待事件的发生。
- 某个时间发生,多是用户按键输入,也多是某个设备插入,亦或用户调整了设备属性,epoll_wait() 返回,将发生的Event 存放在mPendingEventItems 里。若是这是一个用户输入,系统调用Read() 从驱动读到这个按键的信息,存放在rawEvents里。
- getEvents() 返回,进入InputReader的processEventLocked函数。
- 经过rawEvent 找到产生时间的Device,再找到这个Device对应的InputMapper对象,最终生成一个NotifyArgs对象,将其放到NotifyArgs的队列中。
- 第一次循环,或者后面发生设备变化的时候(好比说设备拔插),调用 NativeInputManager 提供的回调,经过JNI通知Java 层的Input Manager Service 作设备变化的相应处理,好比弹出一个提示框提示新设备插入。这部分细节会在后面介绍。
- 调用NotifyArgs里面的Notify()方法,最终调用到InputDispatchor 对应的Notify接口(好比NotifyKey) 将接下来的处理交给InputDispatchor,EventHub 和 InputReader 工做结束,但立刻又开始新的一轮等待,重复6~9的循环。
Input Dispatcher
接下来看看目前为止最长一张时序图,经过下面18个步骤,事件将发送到应用程序进行处理。

- 接上节的最后一步,NotifyKey() 的实如今Input Dispatcher 内部,他首先作简单的校验,对于按键事件,只有Action 是 AKEY_EVENT_ACTION_DOWN 和 AKEY_EVENT_ACTION_UP,即按下和弹起这两个Event别接受。
- Input Reader 传给Input Dispather的数据类型是 NotifyKeyArgs, 后者在这里将其转换为 KeyEvent, 而后交由 Policy 来进行第一步的解析和过滤,interceptKeyBeforeQueuing, 对于手机产品,这个工做是在PhoneWindowManager 里完成,(不一样类型的产品能够定义不一样的WindowManager, 好比GoogleTV 里用到的是TVWindowManager)。KeyEvent 在这里将会被分为三类:
- System Key: 好比说 音量键,Power键,电话键,以及一些特殊的组合键,如用于截屏的音量+Power,等等。部分System Key 会在这里当即处理,好比说电话键,但有一些会放到后面去作处理,好比说音量键,但无论怎样,这些键不会传给应用程序,因此称为系统键。
- Global Key:最终产品中可能会有一些特殊的按键,它不属于某个特定的应用,在全部应用中的行为都是同样,但也不包含在Andrioid的系统键中,好比说GoogleTV 里会有一个“TV” 按键,按它会直接呼起“TV”应用而后收看电视直播,这类按键在Android定义为Global Key.
- User Key:除此以外的按键就是User Key, 它最终会传递到当前的应用窗口。
- phoneWindowManager的interceptKeyBeforeQueuing() 最后返回了wmActiions,里面包含若干个flags,NativeInputManager在handleInterceptActions(), 假如用户按了Power键,这里会通知Android睡眠或唤醒。最后,返回一个 policyFlags,结束第一次的intercept 过程。
- 接下来,按键立刻进入第二轮处理。若是用户在Setting->Accessibility 中选择打开某些功能,好比说手势识别,Android的AccessbilityManagerService(辅助功能服务) 会建立一个 InputFilter 对象,它会检查输入的事件,根据须要可能会转换成新的Event,好比说两根手指头捏动的手势最终会变成ZOOM的event. 目前,InputManagerService 只支持一个InputFilter, 新注册的InputFilter会把老的覆盖。InputFilter 运行在SystemServer 的 ServerThread 线程里(除了绘制,窗口管理和Binder调用外,大部分的System Service 都运行在这个线程里)。而filterInput() 的调用是发生在Input Reader线程里,经过InputManagerService 里的 InputFilterHost 对象通知另一个线程里的InputFilter 开始真正的解析工做。因此,InputReader 线程从这里结束一轮的工做,从新进入epoll_wait() 等待新的用户输入。InputFilter 的工做也分为两个步骤,首先由InputEventConsistencyVerifier 对象(InputEventConsistencyVerifier.java)对输入事件的完整性作一个检查,检查事件的ACTION_DOWN 和 ACTION_UP 是否一一配对。不少同窗可能在Android Logcat 里看到过如下一些相似的打印:"ACTION_UP but key was not down." 就出自此处。接下来,进入到AccessibilityInputFilter 的 onInputEvent(),这里将把输入事件(主要是MotionEvent)进行处理,根据须要变成另一个Event,而后经过sendInputEvent()将事件发回给InputDispatcher。最终调用到injectInputEvent() 将这个事件送入 mInBoundQueue.
- 这个时候,InputDispather 还在Looper中睡眠等待,injectInputEvent()经过wake() 将其唤醒。这是进入Input Dispatcher 线程。
- InputDispatcher 大部分的工做在 dispatcherOnce 里完成。首先从mInBoundQueue 中读出队列头部的事件 mPendingEvent, 而后调用 pokeUserActivity(). poke的英文意思是"搓一下, 捅一下“, 这个函数的目的也就是”捅一下“PowerManagerService 提醒它”别睡眠啊,我还活着呢“,最终调用到PowerManagerService 的 updatePowerStateLocked(),防止手机进入休眠状态。须要注意的是,上述动做不会立刻执行,而是存储在命令队列,mCommandQueue里,这里面的命令会在后面依次被执行。
- 接下来是dispatchKeyLocked(), 第一次进去这个函数的时候,先检查Event是否已通过处理(interceptBeforeDispatching), 若是没有,则生成一个命令,一样放入mCommandQueue里。
- runCommandsLockedInterruptible() 依次执行mCommandQueue 里的命令,前面说过,pokeUserActivity 会调用PowerManagerService 的 updatePowerStateLocked(), 而 interceptKeyBeforeDispatching() 则最终调用到PhoneWindowManager的同名函数。咱们在interceptBeforeQueuing 里面提到的一些系统按键在这个被执行,好比 HOME/MENU/SEARCH 等。
- 接下来,处理前面提过GlobalKey,GlobalKeyManager 经过broadcast将这些全局的Event发送给感兴趣的应用。最终,interceptKeyBeforeDispatching 将返回一个Int值,-1 表明Skip,这个Event将不会发送给应用程序。0 表明 Continue, 将进入下一步的处理。1 则代表还须要后续的Event才能作出决定。
- 命令运行完以后,退出 dispatchOnce, 而后调用pollOnce 进入下一轮等待。但这里不会被阻塞,由于timeout值被设成了0.
- 第二次进入dispatchKeyLocked(), 这是Event的状态已经设为”已处理“,这时候才真正进入了发射阶段。
- 接下来调用 findFocusedWindowTargetLocked() 获取当前的焦点窗口,这里面会作一件很是重要的事情,就是检测目标应用是否有ANR发生,若是下诉条件知足,则说明可能发生了ANR:
- 目标应用不会空,而目标窗口为空。说明应用程序在启动过程当中出现了问题。
- 目标 Activity 的状态是Pause,即再也不是Focused的应用。
- 目标窗口还在处理上一个事件。这个咱们下面会说到。
- 若是目标窗口处于正常状态,调用dispatchEventLocked() 进入真正的发送程序。
- 这里,事件又换了一件马甲,从EventEntry 变成 DispatchEntry, 并送人mOutBoundQueue。而后调用startDispatchCycle() 开始发送。
- 最终的发送发生在InputPublish的sendMessage()。这里就用到了咱们前面提到的SocketPair, 一旦sendMessage() 执行,目标窗口所在进程的Looper线程就会被唤醒,而后读取键值并进行处理,这个过程咱们下面立刻就会谈到。
- 乖乖,还没走完啊?是的,工做还差最后一步,Input Dispatcher给这个窗口发送下一个命令以前,必须等待该窗口的回复,若是超过5s没有收到,就会经过Input Manager Service 向Activity Manager 汇报,后者会弹出咱们熟知的 "Application No Response" 窗口。因此,事件会放入mWaitQueue进行暂存。若是窗口一切正常,完成按键处理后它会调用InputConsumer的sendFinishedSignal() 往SocketPair 里写入完成信号,Input Dispatcher 从 Loop中醒来,并从Socket中读取该信号,而后从mWaitQueue 里清除该事件标志其处理完毕。
- 并不是全部的事件应用程序都会处理,若是没有处理,窗口程序返回的完成消息里的 msg.body.finished.handled 会等于false,InputDispatcher 会调用dispatchKeyUnhandled() 将其交给PhoneWindowManager。Android 在这里提供了一个Fallback机制,若是在 /system/usr/keychars/ 下面的kcm文件里定义了 fallback关键字,Android就识别它为一个Fallback Keycode。当它的Parent Keycode没有被应用程序处理,InputDispatcher 会把 Fallback Keycode 当成一个新的Event,从新发给应用程序。下面是一个定义Fallback Key 的例子。若是按了小键盘的0且应用程序不受理它,InputDispatcher 会再发送一个'INSERT' event 给应用程序。
#/system/usr/keychars/generic.kcm
...
key NUMPAD_0 {
label: '0' //打印字符
base: fallback INSERT //behavior
numlock: '0' //在一个textView里输出的字符
}
- 经历了重重关卡,一个按键发送的流程终于完成了,无论有没有Fallback Key存在,调用startDispatcherCycle() 开始下一轮征程。。。
史上最长的流程图终于介绍完了,有点迷糊了?好吧,再看看下面这张图总结一下:
- InputDispatcher 是一个异步系统,里面用到3个Queue(队列)来保存中间任务和事件,分别是 mInBoundQueue, mOutBoundQueue,mWaitQueue不一样队列的进出划分了按键的不一样处理阶段。
- InputReader 采集的输入实现首先通过InterceptBeforeQueuing处理,Android 系统会将这些按键分类(System/Global/User), 这个过程是在InputReader线程里完成。
- 若是是Motion Event, filterEvent()可能会将其转换成其余的Event。而后经过InjectKeyEvent 将这个按键发给InputDispatcher。这个过程是在System Process的ServerThread里完成。
- 在进入mOutBoundQueue 以前,首先要通过 interceptBeforeDispatching() 的处理,System 和 Global 事件会在这个处理,而不会发送给用户程序。
- 经过以前生成的Socket Pair, InputPublish 将 Event发送给当前焦点窗口,而后InputDispatcher将Event放入mWaitQueue 等待窗口的回复。
- 若是窗口回复,该对象被移出mWaitQueue, 一轮事件处理结束。若是窗口没有处理该事件,从kcm文件里搜寻Fallback 按键,若是有,则从新发送一个新的事件给用户。
- 若是超过5s没有收到用户回复,则说明用户窗口出现阻塞,InputDispather 会经过Input Manager Service发送ANR给ActivityManager。
Key processing
前面咱们说过,NativeInputEventReceiver() 经过addFd() 将SocketPair的一个FD 加入到UI线程的loop里,这样,当Input Dispatcher在Socket的另一端写入Event数据,应用程序的UI线程就会从睡眠中醒来,开始事件的处理流程。时序图以下所示:

- 收到的时间首先会送到队列中,ViewRootImpl 经过 deliverInputEvent() 向InputStage传递消息。
- InputStage 是 Android 4.3 新推出的实现,它将输入事件的处理分红若干个阶段(Stage), 若是当前有输入法窗口,则事件处理从 NativePreIme 开始,不然的话,从EarlyPostIme 开始。事件会依次通过每一个Stage,若是该事件没有被标识为 “Finished”, 该Stage就会处理它,而后返回处理结果,Forward 或 Finish, Forward 运行下一Stage继续处理,而Finished事件将会简单的Forward到下一级,直到最后一级 Synthetic InputStage。流程图和每一个阶段完成的事情以下图所示。

- 最后 经过finishInputEvent() 回复InputDispatcher。