1 概述
Android的input系统获取用户输入, 分发给特定的接收者(Framework或应用程序)进行处理, 这个流程涉及到以下一些模块:
InputReader.cpp : 负责从硬件获取输入, 转换成事件(Event), 并分发给Input Dispatcher.
InputDispatcher.cpp : 将Input Reader传送过来的Events 通过socket分发给合适的窗口, 并监控ANR.
InputManagerService.java : 负责Input Reader 和 Input Dispatcher的创建, 并提供Policy 用于Events的预处理.
WindowManagerService.java : 作为应用与IMS的通信桥梁, 也作为InputDispatcher向AMS报告ANR的通信桥梁.
ActivityManagerService.java :ANR 处理.
Activity & Views :接收输入事件并处理.
整个过程涉及到2个进程: system_server 和 APK 进程.
涉及到3个主要线程: InputReaderThread, 负责读取事件; InputDispatcherThread, 负责分发事件; APK main thread, 负责处理事件.
还涉及到1个次要线程 : ActivityManager, 它在这里的作用是处理ANR事件, 收集相关信息.
2 Input系统与Apk&Activity的关系
一个APK一般有一个或多个Activity, 每个Activity对应一个Window(一般是PhoneWindow)、一个RootView(一般是DecorView)、一个ViewRootImpl, 每个RootView上面可以有一个或多个Views. 因此一个典型的APK如下图示:
Input事件的分发对象是window(也可以说是Activity或PhoneWinow或DecorView, 后文统一用window代指一个分发对象, 我们可以把window想象成应用的一个界面).
系统中可能有多个APK, 每个APK还可能有多个window, 那Input事件到底应该被送往哪个window? 答案是FocusedWindow, 在任意时刻, 系统中只会存在一个FocusedWindow(一般是正在显示的那个window), Focuse的切换是WMS负责的, WMS会把当前的FocusedWindow告知InputDispatcher.
2.1 Apk & Window 注册
前面说过input事件只会被送往FocusedWindow, 当某个APK的某个Activity被启动时(此时它处于前台, 用户可见), WMS会把这个APK和APK.Activity对应的Window注册到InputDispatcher里面, 使之成为FocusedWindow. 随后的input事件(例如按键或触摸)就会被送往这个该APK.Activity进行处理.
InputDispatcher提供了2个API用于注册:
setFocusedApplication(const sp<InputApplicationHandle>& inputApplicationHandle) : 注册当前Focus的APK, 只会存在一个.
setInputWindows(const Vector<sp<InputWindowHandle> >& inputWindowHandles) : 注册系统中所有的Windows(这些windows可能隶属于不同的应用), 可能存在很多个. 每个window里面都存储了自身的一些状态, 包括自己当前是否处于focuse状态!因此通过轮询这些Windows, 我们就能找出当前的FocusedWindow.
InputDispatcher内部用3个变量来存储这些注册的APK & Windows:
mFocusedApplicationHandle : 存储当前Focus的APK
mWindowHandles : 存储所有的Windows
mFocusedWindowHandle : 存储当前Focus的Window
注册API的调用时机是什么时候呢? 我们用一张图示表示
两个API已经在图中用红色字体标注出来了
从流程上可以看出, 当某个APK的Activity被启动时, handleLaunchActivity最终会被调用(四大组件一文详细介绍了Activity的启动流程). 在Activity被创建的过程中, WMS中会创建一个对应的WindowState, 在WindowState的构造函数中, 会创建一个mInputWindowHandle. 这个mInputWindowHandle其实就代表APK的一个Window(或者说一个Activity/PhoneWindow/DecorView). 这个mInputWindowHandle最终会作为inputWindowHandles的一员被注册到InputDispatcher里面.
另外, Activity启动的过程中会事先创建APK进程, 这个进程会作为FocusedApplication被注册到InputDispatcher里面.
2.2 建立socketpair通信通道
在2.1节描述的注册流程中, 有如下一段:
红框中代码建立了一条socketpair管道, 管道的一端(inputChannels[1])传递给了APK; 另一端(inputChannels[0])通过registerInputChannel传递给了InputDispatcher. 这样当InputDispatcher收到input事件后, 就可以通过这条管道把事件传递给APK; 当APK处理完事情后, 也可通过这条管道回馈ACK给InputDispatcher.
APK与Input系统通信的模型如下:
在APK侧, 一个Window(或者是Activity/PhoneWindow/DecorView, 它们都是指的同一个意思)对应一个ViewRootImpl. ViewRootImpl里面包含一个mInputChannel, 它最终被初始化为socketpair的一端(inputChannels[1]). 在APK的主线程里面, 会轮询mInputChannel, 如果发现有数据过来, 则会让WindowInputEventReceiver处理这些数据.
在InputDispatcher侧, 当registerInputChannel被调用时, 会创建一个Connection对象. 一个Connection就对应APK侧的一个Window.
mConnectionsByFd存储着所有的Connections.
每个Connection对象包含如下几个主要元素:
InputWindowHandle : 它就代表APK侧的一个Window, 是注册时AMS传递过来的参数
InputPublisher.mChannel : 它被赋值为socketpair的另一端(inputChannels[0]), 当InputDispatcher想往APK传送input事件时, 会写入这个mChannel ; 另外InputDispatcherThread线程会轮询这个mChannel, 如果有数据过来, 则代表APK发消息过来了, 此时会让InputDispatcher处理这些消息.
另外还包含两个Queue: outboundQueue用于存储将要送给APK的events; waitQueue则代表已经送给APK处理但还收到APK反馈ACK的events, 也就是代表正在被APK处理的events.
在这里介绍这两个Queue是想让大家注意到, 通过connection对象, 我们能知道一个Window有多少events需要处理、多次events正在处理. 更进一步来说, 一个connection对象可以包含一个窗口与Input系统打交道的所有内容.
关于建立通信通道的代码细节, 有兴趣可以阅读这篇文章.
2.3 寻找FocusedWindow
当Input系统收到事件后, 它要先找到焦点窗口, 然后把事件分发给这个窗口.
寻找焦点窗口由findFocusedWindowTargetsLocked函数完成. 到底谁才是焦点窗口? 这个逻辑很简单 : 首先mFocusedApplicationHandle != NULL(也就是要有焦点应用存在), 其次, mFocusedWindowHandle != NULL(也就是要有焦点窗口存在). 这两者是在《Apk & Window注册》阶段被赋值的.
找到焦点窗口后还需要检查这个窗口是否有处理Input事件的权限(checkInjectionPermission); 然后还要看该窗口是否有处理input事件的能力(checkWindowReadyForMoreInputLocked), 例如假设该窗口正在处理之前分发给它的事件, 那它此时就没有能力处理当前的input事件, 需要等待前一个事件处理完毕后才能分发新的事件给它.
如果一切检查都没有问题, 接下来就要从mConnectionsByFd中找到与该窗口对应的Connection对象(这个对象是在《建立socketpair通信通道》时创建的), 继而可以拿到对应的InputPublisher, 通过InputPublisher就能把事件传送到该窗口进行处理.
这一块的细节将在《分发事件 : Input Dispatcher》中详细讨论, 这里只是从宏观上给大家一个印象.
3 Input系统初始化流程
整个输入系统的初始化可以划分为Java 和 Native两个部分, 可以用两张时序图分别描述.
3.1 Java侧初始化流程
1 在SystemServer的初始化过程中,InputManagerService 被创建出来,它做的第一件事情就是初始化Native层,包括EventHub, InputReader 和 InputDispatcher,这一部分我们将在后面详细介绍。
2 当InputManager Service 以及其他的System Service 初始化完成之后,应用程序就开始启动。如果一个应用程序有Activity(只有Activity能够接受用户输入),它要将自己的Window(ViewRoot)通过setView()注册到Window Manager Service 中。
3 用户输入的捕捉和处理发生在不同的进程里(生产者:Input Reader 和 Input Dispatcher 在System Server 进程里,而消耗者,应用程序运行在自己的进程里),因此用户输入事件(Event)的传递需要跨进程。在这里,Android使用了Socket 而不是 Binder来完成。OpenInputChannelPair 生成了两个Socket的FD, 代表一个双向通道的两端,向一端写入数据,另外一端便可以读出,反之依然,如果一端没有写入数据,另外一端去读,则陷入阻塞等待。OpenInputChannelPair() 发生在WindowManager Service 内部。
4 通过RegisterInputChannel, Window Manager Service 将刚刚创建的一个Socket FD,封装在InputWindowHandle(代表一个WindowState) 里传给InputManagerService。
5 InputManagerService 通过JNI(NativeInputManager)最终调用到了InputDispatcher 的 RegisterInputChannel()方法,这里,一个Connection 对象被创建出来,代表与远端某个窗口(InputWindowHandle)的一条用户输入数据通道。一个Dispatcher可能有多个Connection(多个Window)同时存在。为了监听来自于Window的消息,InputDispater 通过AddFd 将这些个FD 加入到Looper中(InputDispatcherThread线程),这样,只要某个Window在Socket的另一端写入数据,Looper就会马上从睡眠中醒来,进行处理。
6 到这里,ViewRootImpl 的 AddWindow 返回,WMS 将SocketPair的另外一个FD 放在返回参数 OutputChannel 里。
7 接着ViewRootImpl 创建了WindowInputEventReceiver 用于接受InputDispatcher 传过来的事件,后者同样通过AddFd() 将读端的Socket FD 加入到Looper中(Apk主线程),这样一旦InputDispatcher发送Event,Looper就会立即醒来处理。
3.2 Native侧初始化流程
1 NativeInit 是 NativeInputManager类的一个方法,在InputManagerService的构造函数中被调用。代码在 frameworks/base/services/jni/com_android_server_input_inputManagerService.cpp.
2 首先创建一个EventHub, 用来监听所有的event输入。
3 创建一个InputDispatcher对象。
4 创建一个InputReader对象,他的输入是EventHub, 输出是InputDispatcher。
5 然后分别为InputReader 和 InputDispatcher 创建各自的线程。注意,当前运行在System Server 的 WMThread线程里。
6 接着,InputManagerService 调用NativeStart 通知InputReader 和 InputDispatcher 开始工作。
7 InputDispatcher是InputReader的消费者,它的线程首先启动,进入Looper等待状态。
8 接着 InputReader 线程启动,等待用户输入的发生。
至此,一切准备工作就绪,万事具备,之欠用户一击了。
4 获取事件 : Eventhub 和 Input Reader
4.1 Linux Input Subsystem简述
Android设备可以同时连接多个输入设备,比如说触摸屏,键盘,鼠标等等。用户在任何一个设备上的输入就会产生一个中断,经由Linux内核的中断处理以及设备驱动转换成一个Event,并传递给用户空间的应用程序进行处理。每个输入设备都有自己的驱动程序,数据接口也不尽相同,如何在一个线程里(上面说过只有一个InputReader Thread)把所有的用户输入都给捕捉到? 这首先要归功于Linux 内核的输入子系统(Input Subsystem), 它在各种各样的设备驱动程序上加了一个抽象层,只要底层的设备驱动程序按照这层抽象接口来实现,上层应用就可以通过统一的接口来访问所有的输入设备。这个抽象层有三个重要的概念,input handler, input handle 和 input_dev,它们的关系如下图所示:
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)进行处理,然后返回给应用程序,流程如下图中红色箭头所示。
所以,只要打开 /dev/input/ 下的所有 event* 设备文件,我们就可以有办法获取所有输入设备的输入事件,不管它是触摸屏,还是一个USB 设备,还是一个红外遥控器。Android中完成这个工作的就是EventHub。
4.2 EventHub & InputReader
EventHub实现在 framework/base/services/input/EventHub.cpp, 它和InputReader 的工作流程如下图所示:
1 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。
2 紧接着,InputManagerService启动InputReader 线程,进入无限的循环,每次循环调用loopOnce(). 第一次循环,会主动扫描 "/dev/input/" 目录,并打开下面的所有文件,通过ioctl()从底层驱动获取设备信息,并判断它的设备类型。这里处理的设备类型有:
INPUT_DEVICE_CLASS_KEYBOARD
INPUT_DEVICE_CLASS_TOUCH
INPUT_DEVICE_CLASS_DPAD
INPUT_DEVICE_CLASS_JOYSTICK 等
3 找到每个设备对应的键值映射文件,读取并生产一个KeyMap 对象。一般来说,设备对应的键值映射文件是 "/system/usr/keylayout/Vendor_%04x_Product_%04x".
4 将刚才扫描到的/dev/input 下所有文件的FD 加到epool等待队列中,调用epool_wait() 开始等待事件的发生。
5 某个时间发生,可能是用户按键输入,也可能是某个设备插入,亦或用户调整了设备属性,epoll_wait() 返回,将发生的Event 存放在mPendingEventItems 里。如果这是一个用户输入,系统调用Read() 从驱动读到这个按键的信息,存放在rawEvents里。
6 getEvents() 返回,进入InputReader的processEventLocked函数。
7 通过rawEvent 找到产生时间的Device,再找到这个Device对应的InputMapper对象,最终生成一个NotifyArgs对象,将其放到NotifyArgs的队列中。
8 第一次循环,或者后面发生设备变化的时候(比如说设备拔插),调用 NativeInputManager 提供的回调,通过JNI通知Java 层的Input Manager Service 做设备变化的相应处理,比如弹出一个提示框提示新设备插入。这部分细节会在后面介绍。
9 调用NotifyArgs里面的Notify()方法,最终调用到InputDispatcher 对应的Notify接口(比如NotifyKey) 将接下来的处理交给InputDispatcher,EventHub 和 InputReader 工作结束,但马上又开始新的一轮等待,重复6~9的循环。
5 分发事件 : Input Dispatcher
接下来看看目前为止最长一张时序图,通过下面18个步骤,事件将发送到应用程序进行处理。
1 接上节的最后一步,NotifyKey() 的实现在Input Dispatcher 内部,他首先做简单的校验,对于按键事件,只有Action 是 AKEY_EVENT_ACTION_DOWN 和 AKEY_EVENT_ACTION_UP,即按下和弹起这两个Event被接受。
2 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, 它最终会传递到当前的应用窗口。
3 phoneWindowManager的interceptKeyBeforeQueuing() 最后返回了wmActions,里面包含若干个flags。NativeInputManager在handleInterceptActions(), 假如用户按了Power键,这里会通知Android睡眠或唤醒。最后,返回一个 policyFlags,结束第一次的intercept 过程。
4 接下来,按键马上进入第二轮处理。如果用户在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.
5 这个时候,InputDispather 还在Looper中睡眠等待,injectInputEvent()通过wake() 将其唤醒。这时进入Input Dispatcher 线程。
6 InputDispatcher 大部分的工作在 dispatcherOnce 里完成。首先从mInBoundQueue 中读出队列头部的事件 mPendingEvent, 然后调用 pokeUserActivity(). poke的英文意思是"搓一下, 捅一下“, 这个函数的目的也就是”捅一下“PowerManagerService 提醒它”别睡眠啊,我还活着呢“,最终调用到PowerManagerService 的 updatePowerStateLocked(),防止手机进入休眠状态。需要注意的是,上述动作不会马上执行,而是存储在命令队列,mCommandQueue里,这里面的命令会在后面依次被执行。
7 接下来是dispatchKeyLocked(), 第一次进去这个函数的时候,先检查Event是否已经过处理(interceptBeforeDispatching), 如果没有,则生成一个命令,同样放入mCommandQueue里。
8 runCommandsLockedInterruptible() 依次执行mCommandQueue 里的命令,前面说过,pokeUserActivity 会调用PowerManagerService 的 updatePowerStateLocked(), 而 interceptKeyBeforeDispatching() 则最终调用到PhoneWindowManager的同名函数。我们在interceptBeforeQueuing 里面提到的一些系统按键在这个被执行,比如 HOME/MENU/SEARCH 等。
9 接下来,处理前面提过GlobalKey,GlobalKeyManager 通过broadcast将这些全局的Event发送给感兴趣的应用。最终,interceptKeyBeforeDispatching 将返回一个Int值,-1 代表Skip,这个Event将不会发送给应用程序。0 代表 Continue, 将进入下一步的处理。1 则表明还需要后续的Event才能做出决定。
10 命令运行完之后,退出 dispatchOnce, 然后调用pollOnce 进入下一轮等待。但这里不会被阻塞,因为timeout值被设成了0.
11 第二次进入dispatchKeyLocked(), 这时Event的状态已经设为”已处理“,这时候才真正进入了发射阶段。
12 接下来调用 findFocusedWindowTargetLocked() 获取当前的焦点窗口,这里面会做一件非常重要的事情,就是检测目标应用是否有ANR发生,如果下诉条件满足,则说明可能发生了ANR:
目标应用不为空,而目标窗口为空。说明应用程序在启动过程中出现了问题。
目标 Activity 的状态是Pause,即不再是Focused的应用。
目标窗口还在处理上一个事件。这个我们下面会说到。
13 如果目标窗口处于正常状态,调用dispatchEventLocked() 进入真正的发送程序。
14 这里,事件又换了一件马甲,从EventEntry 变成 DispatchEntry, 并送人mOutBoundQueue。然后调用startDispatchCycle() 开始发送。
15 最终的发送发生在InputPublish的sendMessage()。这里就用到了我们前面提到的SocketPair, 一旦sendMessage() 执行,目标窗口所在进程的Looper线程就会被唤醒,然后读取键值并进行处理,这个过程我们下面马上就会谈到。
16 乖乖,还没走完啊?是的,工作还差最后一步,Input Dispatcher给这个窗口发送下一个命令之前,必须等待该窗口的回复,如果超过5s没有收到,就会通过Input Manager Service 向Activity Manager 汇报,后者会弹出我们熟知的 "Application No Response" 窗口。所以,事件会放入mWaitQueue进行暂存。如果窗口一切正常,完成按键处理后它会调用InputConsumer的sendFinishedSignal() 往SocketPair 里写入完成信号,Input Dispatcher 从 Loop中醒来,并从Socket中读取该信号,然后从mWaitQueue 里清除该事件标志其处理完毕。
17 并非所有的事件应用程序都会处理,如果没有处理,窗口程序返回的完成消息里的 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 给应用程序。
18 经历了重重关卡,一个按键发送的流程终于完成了,不管有没有Fallback Key存在,调用startDispatcherCycle() 开始下一轮征程。。。
史上最长的流程图终于介绍完了,有点迷糊了?好吧,那我们从一个事件是如何被传递的角度给出几张精简示意图。
5.1 事件分发简图
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。
再以另外一种形式展现一下:
6 处理事件
第三章提到过,WindowInputEventReceiver最终会通过addFd() 将SocketPair的一个FD 加入到UI线程的loop里,这样,当Input Dispatcher在Socket的另外一端写入Event数据,应用程序的UI线程就会从睡眠中醒来,开始事件的处理流程。时序图如下所示:
收到的事件首先会送到队列中,ViewRootImpl 通过 deliverInputEvent() 向InputStage传递消息。deliverInputEvent可以理解为处理事件的入口。
InputStage 是 Android 4.3 新推出的实现,它将输入事件的处理分成若干个阶段(Stage), 如果当前有输入法窗口,则事件处理从 NativePreIme 开始,否则的话,从EarlyPostIme 开始。后文《InputStage流程》会展现事件传递的细节.
最后,通过finishInputEvent() 回复InputDispatcher,开始下一轮事件处理。详见第7章。
6.1 InputStage流程
InputStage 是 Android 4.3 新推出的实现,它将输入事件的处理分成若干个阶段(Stage), 如果当前有输入法窗口,则事件处理从 NativePreIme 开始,否则的话,从EarlyPostIme 开始。
事件会依次经过每个Stage. 第一个Stage先尝试处理它: 如果成功处理则将该事件标记为”Finish”; 否则如果该Stage无法处理此事件, 则将它标记为”Forward”.
不管事件被标记为何种状态, 它都会被送入下一个Stage. 下一个Stage会检查事件状态: 如果是”Forward”, 则尝试处理该事件并设置状态; 如果是”Finish”, 则代表该事件已经被处理, 该Stage就不会处理它, 直接将事件传递到下一个Stage.
具体的图示如下:
6.2 按键事件的处理流程
按照图中标注的优先级依次处理, 任一环节如果成功处理, 则不在继续下发, 直接返回:
1 首先尝试给Viewgroup的View.java.dispatchKeyEvent处理
2 然后尝试给Viewgroup上的焦点View处理, 这个View是展现在用户眼前的那个界面
3 然后尝试给Viewgroup所属的Activity处理
4 然后尝试给Activity所依附的那个PhoneWindow处理
5 然后尝试给PhoneFallbackEventHandler处理
6 最后在ViewRootImpl里面会判断当前是否为NavigationKey, 如果是则处理相应的焦点切换动作
从整个流程来看, 事件最先会分发到PhoneWindow.java :: DecorView.dispatchKeyEvent, 在这个函数中, 会尝试获取final Callback cb = getCallback(). 这个getCallback的返回结果代表什么意思呢?
在启动某个APK的Activity时, handleLaunchActivity-> performLaunchActivity过程会先创建一个Activity对象, 然后会执行Activity.attach().
在attach流程中会先创建一个phonewindow, 然后调用mWindow.setCallback(this), 意思就是把Activity自己作为Callback设置进去.
所以, 这里的getCallback返回的其实就是一个Activity对象. 因此cb.dispatchKeyEvent调用的实际就是Activity对象的dispatchKeyEvent.
如果想了解这一块的代码细节, 可以参考这篇文章 :Android按键事件处理流程.
6.3 触屏事件的处理流程
按照图中标注的优先级依次处理, 任一环节如果成功处理, 则不在继续下发, 直接返回:
1 首先遍历ViewGroup的各个子View, 调用它们的onTouch回调
2 然后调用各个子View的onTouchEvent
3 如果所有的子View都没有处理此事件, 则调用ViewGroup自身的onTouchEvent
4 最后调用Activity自身的onTouchEvent
需要说明的是, 上述的流程只是针对Acition_Down事件, 而Aciton_UP和Action_MOVE却不会走这套流程. 事实上, 一次完整的Touch事件, 应该是由“一个Down”、“一个Up”和“0或多个Move”组成的. Down方式通过dispatchTouchEvent分发, 分发的目的是为了找到真正需要处理完整Touch请求的View. 当某个View或者ViewGroup的onTouchEvent事件返回true时, 便表示它是真正要处理这次请求的View, 之后的Aciton_UP和Action_MOVE将由它处理.
ViewGroup还有个onInterceptTouchEvent, 看名字便知道这是个拦截事件。 这个拦截事件需要分两种情况来说明:
假如我们在某个ViewGroup的onInterceptTouchEvent中,将Action为Down的Touch事件返回true,那便表示将该ViewGroup的所有下发操作拦截掉,这种情况下,mTarget会一直为null,因为mTarget是在Down事件中赋值的。由于mTarge为null,该ViewGroup的onTouchEvent事件被执行。这种情况下可以把这个ViewGroup直接当成View来对待。
假如我们在某个ViewGroup的onInterceptTouchEvent中,将Acion为Down的Touch事件都返回false,其他的都返回True,这种情况下,Down事件能正常分发,若子View都返回false,那mTarget还是为空,无影响。若某个子View返回了true,mTarget被赋值了,在Action_Move和Aciton_UP分发到该ViewGroup时,便会给mTarget分发一个Action_Delete的MotionEvent,同时清空mTarget的值,使得接下去的Action_Move(如果上一个操作不是UP)将由ViewGroup的onTouchEvent处理。
情况一用到的比较多,情况二个人还未找到使用场景。
如果想了解这一块的代码细节, 可以参考这篇文章 :ViewRootImpl & ViewGroup & View 触摸事件派发机制源码分析.
7 事件完毕, 开始下一轮事件处理
当事件被应用层处理完毕后, 应用层会调用finishInputEvent, 通过socketpair告知InputDispatcher.
InputDispatcher收到socket传递过来的消息后, 会标记当前事件的处理状态, 并触发下一轮的事件处理. 这一块的代码流程可以参考InputDispatcher线程.
标记当前事件的处理状态是在finishDispatchCycleLocked中完成的, 它会将connection->inputPublisherBlocked设置为false, 意味着这个connection可以处理新的事件了.
这个标记动作很重要, 否则将会引发ANR. 在应用层处理事件的过程中, 如果有新的事件到来, InputDispatcher不会分发该事件(因为它发现connection->inputPublisherBlocked的值为true), 而是把该事件先放入队列, 然后等待应用层处理完毕上有一个事件.
如果应用层在5s内没有通知InputDispatcher上一个事件已经处理完毕了, 则InputDispatcher就会抛出一个ANR.
触发下一轮事件处理则是在onDispatchCycleFinishedLocked中完成的, 它会向InputDispatcher的命令队列中加入一个新的命令, 随后InputDispatcher的loop会取出该命令并运行它.
8 模拟事件
Input.java会被编译为input命令, 通过input命令, 我们可以模拟发送按键或触屏事件.
9 RefLink : 代码细节
Reflink |
Comment |
本文撰写的主要参考网站 |
|
Apk & Window 注册 建立socketpair通信通道 |
|
Input系统初始化流程 |
|
获取事件 : Eventhub 和 Input Reader |
|
分发事件 : Input Dispatcher & 回传事件处理完毕通知 : finishInputEvent |