Android输入系统梳理

Android输入系统梳理

输入系统总体流程与参与角色

在这里插入图片描述
Android最常见的输入设备是触摸屏,然而除了触摸屏,安卓还支持其他输入设备如鼠标、游戏手柄、键盘等。当输入设备可用时,Linux内核会在/dev/input/下创建对应的名为event0~n或其他名称的设备节点。而当输入设备不可用时,则会将对应的节点删除。在用户空间可以通过ioctl的方式从这些设备节点中获取其对应的输入设备的类型、厂商、描述等信息。
当用户操作输入设备时,Linux内核接收到相应的硬件中断,然后将中断加工成原始的输入事件数据并写入其对应的设备节点中,在用户空间可以通过read()函数将事件数据读出。Android输入系统的工作原理概括来说,就是监控/dev/input/下的所有设备节点,当某个节点有数据可读时,将数据读出并进行一系列的翻译加工,然后在所有的窗口中寻找合适的事件接收者,并派发给它。
流程图中设备节点负责承载输出原始输入数据,InputManagerService负责封装加工原始输入数据为KeyEvent、MotionEvent等输入事件,然后通过InputDispatcher分发给WMS管理的Window,最在在APP内部从ViewRootImpl开始,开始输入事件在视图View系统中的消费处理。

设备节点上报数据

linux内核写入到设备节点的数据格式和例子如下,数据都是16进制的

格式时间戳设备节点路径事件类型事件代码事件的值
举例:按下电源键1262.443489/dev.input.event00001007400000001

代码对应结构为RawEvent
在这里插入图片描述

通过adb shell getevent命令可以直接获取某个节点的上报数据
通过adb shell sendevent命令可以模拟linux内核给设备节点写入数据,比如可以模拟点击电源键,然后手机就会有亮屏灭屏等行为,这个给自动化测试中提供了一个切入手段

InputManagerService

在这里插入图片描述
在这里插入图片描述

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

EventHub

EventHub主要通过INofity和Epoll机制监听设备节点的增删以及获取原始输入事件,通过getEvents接口,给InputReader返回RawEvent结构的数据
在这里插入图片描述

InputReader

InputReader运行在InputReaderThread线程中,从EventHub读到RawEvent后,通过合适的InputMapper处理后,把RawEvent加工为三种基本类型:按键类型、手势类型和开关类型。对应的结构体为NofifyKeyArgs、NotifyMotionArgs和NotifySwitchArgs。这三种类型就是InputReader的输出,部分处理完的数据先放入QueueInputListener中缓存,随后再flush到InputDispatcher,避免多次唤醒InputDispatcher线程。另外InputReader还处理设备节点的增删修改,具体也在processEventsLocked中
在这里插入图片描述

代码如下,InputReaderThread的threadLoop方法类似于java中Thread.run()方法,不同的是如果返回true的话,就会一直循环执行threadLoop方法,知道返回false,所以可以看到InputReaderThread这个线程在永不休止地执行mReader->loopOnce()一个方法
在这里插入图片描述
mEventHub->getEvents就是从EventHub中读取RawEvents,处理完后刷新通过flush同步给InputDispatcherThread中
在这里插入图片描述

InputDispatcher

在这里插入图片描述
InputDispatcher工作流程如上。上一节提到InputReader生成NotifyKeyArgs、NotifyMotionArgs和NotifySwitchArgs三种事件类型并传递给InputDispatcher处理,最后会调用到notifyKey、notifyMotion和 notifySwitch。下面以notifyMotion为例,讲解InputDispatcher的工作流程。

由于notifyMotion由InputReader在其线程循环中调用,因此在此函数执行的两个策略相关的动作interceptMotionBeforeQueueing()和InputFilter都发生在InputReader线程,而不是派发线程中。这个需要特别留意下。

void InputDispatcher::notifyMotion(const NotifyMotionArgs* args) {
    // 首先验证参数的有效性。对Motion事件来说,主要是为了验证触控点的数量与Id是否在合理范围
    if (!validateMotionEvent(args->action,
                       args->pointerCount, args->pointerProperties)) { return; }
    // ① 在输入事件进入派发队列之前,首先向DispatcherPolicy获取本事件的派发策略
    uint32_t policyFlags = args->policyFlags;
    policyFlags |= POLICY_FLAG_TRUSTED; // 从InputReader过来的事件都是TRUSTED的
    /* 注意,policyFlag是一个按引用传递的参数,也就是说DispatcherPolicy会更改policyFlags的
       取值 */
    mPolicy->interceptMotionBeforeQueueing(args->eventTime, /*byref*/ policyFlags);
    {
        // ② 在进入派发队列之前还有一件重要的事情,就是把事件交给InputFilter进行过滤
        if (shouldSendMotionToInputFilterLocked(args)) {
            MotionEvent event;
            // 使用NotifyMotionArgs参数中所保存的事件信息填充一个MotionEvent对象
            event.initialize(args->deviceId, ......);
            policyFlags |= POLICY_FLAG_FILTERED; // 通过policyFlags标记此事件已被过滤
            // 由DispatcherPolicy启动过滤动作。注意,当过滤结果为false时,此事件将被忽略
            if (!mPolicy->filterInputEvent(&event, policyFlags)) { return; }
        }
        /* ③ 使用NotifyMotionArgs参数中的事件信息构造一个MotionEntry,并通过
           enqueueInboundEventLocked()函数将其放入派发队列中 */
        MotionEntry* newEntry = new MotionEntry(
                                   args->eventTime, ..., policyFlags, ...);
        // 注意返回值needWake,它指示派发线程是否处于休眠状态
        needWake = enqueueInboundEventLocked(newEntry);
    }
    // ④ 唤醒派发线程
    if (needWake) { mLooper->wake(); }
}

在事件放入派发队列前,有两个地方可以对事件做前置处理:DispatcherPolicy和InputFilter。

  1. DispatcherPolicy对事件的拦截函数interceptXXXBeforeQueueing最终实现是在java层WMS中的PhoneWindowManager的同名函数中,所以WMS是在此处对输入事件做了一层控制,以interceptMotionBeforeQueueing为例,对屏幕的点击有三种策略
  • POLICY_FLAG_WOKE_HERE,PhoneWindowManager可能认为此次点击可以将设备唤醒。虽然此派发策略不会对派发流程产生影响,但是在从Java层的调用返回时,NativeInputManager会检查这个派发策略,并调用PowerManagerService的相关函数将设备唤醒。可以看出,设备唤醒操作发生在输入事件进入派发队列之前,因此它不会被ANR所阻塞。
  • POLICY_FLAG_BRIGHT_HERE,功能同POLICY_FLAG_WOKE_HERE类似。当屏幕变暗时,PhoneWindowManager为输入事件返回此派发策略会使NativeInp-utManager调用PowerManager相关函数促使屏幕恢复正常亮度。
  • POLICY_FLAG_PASS_TO_USER:这个派发策略决定了事件是否会进入正式的派发流程。PhoneWindowManager认为事件不需要派发给用户时,就会从policyFlags中移除这个派发策略,在dispatchOnceInnerLocked()函数中会将事件丢弃。
  1. 使用者可以通过setInputFilter()函数将自己的InputFilter对象设置给IMS,从此便可以开始进行输入事件的监听操作了。不过需要注意的是,IMS认为一旦事件被一个InputFilter截获过,这个事件便被丢弃了(返回false)。如果IInputFilter的使用者不作为,将会导致输入事件无法响应,也不会引发ANR。因此,IInputFilter的使用者截获事件之后,需要以软件的方式将事件重新注入InputDispatcher。使用者可以从IInputFilter的install()回调所提供的IInputFilterHost对象完成事件的重新注入。不过,对重新注入的事件内容是什么输入系统则不做要求,因此InputFilter机制为使用者提供了篡改输入事件的能力。
  2. mInBoundQueue为派发队列,是InputReader和InputDispatcher两者之间的桥梁。InputReader是生产者,往派发对立尾部塞事件,而InputDispatcher是消费者,从队列头部获取事件并处理。往mInBoundQueue中塞入事件之前的流程,都是在InputReaderThread中执行的,当队列不为空时,则唤醒InputDispatcherThread线程消费队列中的事件。
  3. 从mInBoundQueue去除队头元素放入mPendingEvent中,mPendingEvent存储的是当前正在处理的单个事件,处理完成后,mPendingEvnet设置为null,才能处理下一个事件。取出mPendingEvent的DropReason判断是否需要丢弃事件,如果不需要,则通过findXXXWindowTargetsLocked方法找到合适的派发窗口,然后将事件封装成InputTarget,通过Connection的sockets通信到app进程中,由对应目标进程处理事件并在事件处理结束后通过Connection事件处理完成
  4. Connection由三个重要成员
  • mInputPublisher,类型为InputPublisher,它封装InputChannel并直接对其进行读写。InputChannel根据wms提供并维护的窗口信息,通过sockets机制与特定窗口保持双向跨进程通信。
  • outboundQueue,用于保存准备通过此Connection进行发送的事件队列
  • waitQueue,用于保存已经通过此Connection发送到窗口但窗口仍未处理完成的事件

从outboundQueue取出输入事件,通过InputChannel发送到目标窗口(一般是APP进程)处理,把发送后的输入事件存入waitQueue中,当目标窗口处理完事件后,通过InputChannel通过Connection输入事件处理完成,此时可以把处理完成的事件移出waitQueue中。
如果目标窗口(一般是APP进程)无法及时处理输入事件,则waitQueue会保持不空,此时可能anr
如果目标窗口(一般是APP进程)因为主线程繁忙等原因无法接收输入事件,则outboundQueue不为空,此时可能anr。

ViewRootImpl

当一个输入事件被派发到ViewRootImpl所在的窗口时,所在线程的Looper会被唤醒并触发InputEventReciever.onInputEvent回调,控件树的输入事件派发流程就从这回调开始。从InputDispatcher传递过来的InputEvent,在ViewRootImpl处与InputEventReceiver一起封装成QueuedInputEvent,随后根据Event类型做不同分发,分发结束后,调用finishInputEvent,从QueuedInputEvent中取出receiver,调用InputEventReciever.finishInputEvent通知InputDispatcher事件处理完成,InputDispatcher对应地移除waitQueue中的事件
在这里插入图片描述

参考

深入理解Android卷三

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值