首页 > 技术文章 > Android Input Subsystem

jliuxin 2020-12-13 17:26 原文

1       概述

Androidinput系统获取用户输入, 分发给特定的接收者Framework或应用程序)进行处理, 这个流程涉及到以下一些模块:

         InputReader.cpp : 负责从硬件获取输入, 转换成事件(Event), 并分发给Input Dispatcher.

         InputDispatcher.cpp : Input Reader传送过来的Events 通过socket分发给合适的窗口, 并监控ANR.

         InputManagerService.java : 负责Input Reader Input Dispatcher的创建, 并提供Policy 用于Events的预处理.

         WindowManagerService.java : 作为应用与IMS的通信桥梁, 也作为InputDispatcherAMS报告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(也可以说是ActivityPhoneWinowDecorView, 后文统一用window代指一个分发对象, 我们可以把window想象成应用的一个界面).

 

系统中可能有多个APK, 每个APK还可能有多个window, Input事件到底应该被送往哪个window? 答案是FocusedWindow, 在任意时刻, 系统中只会存在一个FocusedWindow(一般是正在显示的那个window), Focuse的切换是WMS负责的, WMS会把当前的FocusedWindow告知InputDispatcher.

2.1    Apk & Window 注册

前面说过input事件只会被送往FocusedWindow, 当某个APK的某个Activity被启动时(此时它处于前台, 用户可见), WMS会把这个APKAPK.Activity对应的Window注册到InputDispatcher里面, 使之成为FocusedWindow. 随后的input事件(例如按键或触摸)就会被送往这个该APK.Activity进行处理.

 

InputDispatcher提供了2API用于注册:

         setFocusedApplication(const sp<InputApplicationHandle>& inputApplicationHandle) : 注册当前FocusAPK, 只会存在一个.

         setInputWindows(const Vector<sp<InputWindowHandle> >& inputWindowHandles) : 注册系统中所有的Windows(这些windows可能隶属于不同的应用), 可能存在很多个. 每个window里面都存储了自身的一些状态, 包括自己当前是否处于focuse状态!因此通过轮询这些Windows, 我们就能找出当前的FocusedWindow.

 

InputDispatcher内部用3个变量来存储这些注册的APK & Windows:

         mFocusedApplicationHandle : 存储当前FocusAPK

         mWindowHandles : 存储所有的Windows

         mFocusedWindowHandle : 存储当前FocusWindow

 

注册API的调用时机是什么时候呢? 我们用一张图示表示

         两个API已经在图中用红色字体标注出来了

         从流程上可以看出, 当某个APKActivity被启动时, 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处理完事情后, 也可通过这条管道回馈ACKInputDispatcher.

 

APKInput系统通信的模型如下:

         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用于存储将要送给APKevents; waitQueue则代表已经送给APK处理但还收到APK反馈ACKevents, 也就是代表正在被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 生成了两个SocketFD 代表一个双向通道的两端,向一端写入数据,另外一端便可以读出,反之依然,如果一端没有写入数据,另外一端去读,则陷入阻塞等待。OpenInputChannelPair() 发生在WindowManager Service 内部。

4           通过RegisterInputChannel, Window Manager Service 将刚刚创建的一个Socket FD,封装在InputWindowHandle(代表一个WindowState) 里传给InputManagerService

5           InputManagerService 通过JNINativeInputManager)最终调用到了InputDispatcher RegisterInputChannel()方法,这里,一个Connection 对象被创建出来,代表与远端某个窗口(InputWindowHandle)的一条用户输入数据通道。一个Dispatcher可能有多个Connection(多个Window)同时存在。为了监听来自于Window的消息,InputDispater 通过AddFd 将这些个FD 加入到LooperInputDispatcherThread线程,这样,只要某个WindowSocket的另一端写入数据,Looper就会马上从睡眠中醒来,进行处理。

6           到这里,ViewRootImpl AddWindow 返回,WMS SocketPair的另外一个FD 放在返回参数 OutputChannel 里。

7           接着ViewRootImpl 创建了WindowInputEventReceiver 用于接受InputDispatcher 传过来的事件,后者同样通过AddFd() 将读端的Socket FD 加入到LooperApk主线程),这样一旦InputDispatcher发送EventLooper就会立即醒来处理。

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           InputDispatcherInputReader的消费者,它的线程首先启动,进入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 evdevhandle就对应与 /dev/input/下的四个文件"event0~3". 通过input handle, 可以找到对应的input_handler input_dev.

 

简单说来,input_dev对应于底层驱动,而input_handler是个上层驱动,而input_handle 提供给应用程序标准的文件访问接口来打通这条上下通道。通过Linux input system获取用户输入的流程简单如下:

    1. 设备通过input_register_dev 将自己的驱动注册到Input 系统。
    1. 各种Handler 通过 input_register_handler将自己注册到Input系统中。
    1. 每一个注册进来的input_dev Input_handler 都会通过input_connect() 寻找对方,生成对应的 input_handle,并在/dev/input/下产成一个设备节点文件.
  1. 应用程序通过打开(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() 返回,进入InputReaderprocessEventLocked函数。

7           通过rawEvent 找到产生时间的Device,再找到这个Device对应的InputMapper对象,最终生成一个NotifyArgs对象,将其放到NotifyArgs的队列中。

8           第一次循环,或者后面发生设备变化的时候(比如说设备拔插),调用 NativeInputManager 提供的回调,通过JNI通知Java 层的Input Manager Service 做设备变化的相应处理,比如弹出一个提示框提示新设备插入。这部分细节会在后面介绍。

9           调用NotifyArgs里面的Notify()方法,最终调用到InputDispatcher 对应的Notify接口(比如NotifyKey) 将接下来的处理交给InputDispatcherEventHub InputReader 工作结束,但马上又开始新的一轮等待,重复69的循环。

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           phoneWindowManagerinterceptKeyBeforeQueuing() 最后返回了wmActions,里面包含若干个flagsNativeInputManagerhandleInterceptActions() 假如用户按了Power键,这里会通知Android睡眠或唤醒。最后,返回一个 policyFlags,结束第一次的intercept 过程。

4           接下来,按键马上进入第二轮处理。如果用户在Setting->Accessibility 中选择打开某些功能,比如说手势识别,AndroidAccessbilityManagerService(辅助功能服务) 会创建一个 InputFilter 对象,它会检查输入的事件,根据需要可能会转换成新的Event,比如说两根手指头捏动的手势最终会变成ZOOMevent. 目前,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           接下来,处理前面提过GlobalKeyGlobalKeyManager 通过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       最终的发送发生在InputPublishsendMessage()。这里就用到了我们前面提到的SocketPair, 一旦sendMessage() 执行,目标窗口所在进程的Looper线程就会被唤醒,然后读取键值并进行处理,这个过程我们下面马上就会谈到。

16       乖乖,还没走完啊?是的,工作还差最后一步,Input Dispatcher给这个窗口发送下一个命令之前,必须等待该窗口的回复,如果超过5s没有收到,就会通过Input Manager Service Activity Manager 汇报,后者会弹出我们熟知的 "Application No Response" 窗口。所以,事件会放入mWaitQueue进行暂存。如果窗口一切正常,完成按键处理后它会调用InputConsumersendFinishedSignal() SocketPair 里写入完成信号,Input Dispatcher Loop中醒来,并从Socket中读取该信号,然后从mWaitQueue 里清除该事件标志其处理完毕。

17       并非所有的事件应用程序都会处理,如果没有处理,窗口程序返回的完成消息里的 msg.body.finished.handled 会等于falseInputDispatcher 会调用dispatchKeyUnhandled() 将其交给PhoneWindowManagerAndroid 在这里提供了一个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 是一个异步系统,里面用到3Queue(队列)来保存中间任务和事件,分别是 mInBoundQueue, mOutBoundQueuemWaitQueue不同队列的进出划分了按键的不同处理阶段。

         InputReader 采集的输入实现首先经过InterceptBeforeQueuing处理,Android 系统会将这些按键分类(System/Global/User) 这个过程是在InputReader线程里完成。

         如果是Motion Event, filterEvent()可能会将其转换成其他的Event。然后通过InjectKeyEvent 将这个按键发给InputDispatcher。这个过程是在System ProcessServerThread里完成。

         在进入mOutBoundQueue 之前,首先要经过 interceptBeforeDispatching() 的处理,System Global 事件会在这个处理,而不会发送给用户程序。

         通过之前生成的Socket Pair, InputPublish Event发送给当前焦点窗口,然后InputDispatcherEvent放入mWaitQueue 等待窗口的回复。

         如果窗口回复,该对象被移出mWaitQueue 一轮事件处理结束。如果窗口没有处理该事件,从kcm文件里搜寻Fallback 按键,如果有,则重新发送一个新的事件给用户。

         如果超过5s没有收到用户回复,则说明用户窗口出现阻塞,InputDispather 会通过Input Manager Service发送ANRActivityManager

 

再以另外一种形式展现一下:

6       处理事件

第三章提到过,WindowInputEventReceiver最终会通过addFd() SocketPair的一个FD 加入到UI线程的loop里,这样,当Input DispatcherSocket的另外一端写入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           首先尝试给ViewgroupView.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的返回结果代表什么意思呢?

 

在启动某个APKActivity, 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           然后调用各个子ViewonTouchEvent

3           如果所有的子View都没有处理此事件, 则调用ViewGroup自身的onTouchEvent

4           最后调用Activity自身的onTouchEvent

 

需要说明的是, 上述的流程只是针对Acition_Down事件, Aciton_UPAction_MOVE却不会走这套流程. 事实上, 一次完整的Touch事件, 应该是由一个Down一个Up0或多Move组成的. Down方式通过dispatchTouchEvent分发, 分发的目的是为了找到真正需要处理完整Touch请求的View. 当某个View或者ViewGrouponTouchEvent事件返回true, 便表示它是真正要处理这次请求的View, 之后的Aciton_UPAction_MOVE将由它处理.

 

ViewGroup还有个onInterceptTouchEvent 看名字便知道这是个拦截事件 这个拦截事件需要分两种情况来说明

         假如我们在某个ViewGrouponInterceptTouchEvent中,将ActionDownTouch事件返回true,那便表示将该ViewGroup的所有下发操作拦截掉,这种情况下,mTarget会一直为null,因为mTarget是在Down事件中赋值的。由于mTargenull,该ViewGrouponTouchEvent事件被执行。这种情况下可以把这个ViewGroup直接当成View来对待。

         假如我们在某个ViewGrouponInterceptTouchEvent中,将AcionDownTouch事件都返回false,其他的都返回True,这种情况下,Down事件能正常分发,若子View都返回false,那mTarget还是为空,无影响。若某个子View返回了truemTarget被赋值了,在Action_MoveAciton_UP分发到该ViewGroup时,便会给mTarget分发一个Action_DeleteMotionEvent,同时清空mTarget的值,使得接下去的Action_Move(如果上一个操作不是UP)将由ViewGrouponTouchEvent处理。

情况一用到的比较多,情况二个人还未找到使用场景。

 

如果想了解这一块的代码细节, 可以参考这篇文章 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的命令队列中加入一个新的命令, 随后InputDispatcherloop会取出该命令并运行它.

8       模拟事件

Input.java会被编译为input命令, 通过input命令, 我们可以模拟发送按键或触屏事件.

9       RefLink : 代码细节

Reflink

Comment

http://www.cnblogs.com/samchen2009/p/3368158.html

本文撰写的主要参考网站

http://gityuan.com/2016/12/24/input-ui/

Apk & Window 注册

建立socketpair通信通道

http://gityuan.com/2016/12/10/input-manager/

Input系统初始化流程

http://gityuan.com/2016/12/11/input-reader/

获取事件 : Eventhub Input Reader

 http://gityuan.com/2016/12/17/input-dispatcher/

 http://gityuan.com/2016/12/31/input-ipc/

分发事件 : Input Dispatcher & 回传事件处理完毕通知 : finishInputEvent

 

推荐阅读