Jimmy Chen

A Programmer

(转载)Input系统: 按键事件分发

前言

前面一篇文章分析了 InputReader 对按键事件的流程流程,大致上就是根据配置文件把按键的扫描码(scan code)转换为按键码(key code),并且同时会从配置文件中获取策略标志位(policy flag),用于控制按键的行为,例如亮屏。然后把按键事件进行包装,分发给 InputDispatcher。本文就接着来分析 InputDispatcher 对按键事件的处理。

1. InputDispatcher 收到事件

从前面一篇文章可知,InputDispatcher 收到的按键事件的来源如下

InputReader 把按键事件交给 KeyboardInputMapper 处理,KeyboardInputMapper 把按键事件包装成 NotifyKeyArgs,然后加入到 QueuedInputListener 的缓存队列。

然后,当 InputReader 处理完所有事件后,会刷新 QueuedInputListener 的缓存队列,如下

QueuedInputListener 会把缓存队列中的所有事件,分发给 InputClassifier

InputClassifier 收到 NotifyKeyArgs 事件后,其实什么也没做,就交给了 InputDispatcher

现在明白了按键事件的来源,接下来分析 InputDispatcher 如何处理按键事件

InputDispatcher 处理按键事件的过程如下

  1. 把按键事件包装成 KeyEvent 对象,然后查询截断策略,看看策略是否截断该事件。如果策略不截断,那么会在参数 policyFlags 添加 POLICY_FLAG_PASS_TO_USER 标志位,表示事件要发送给用户。 否则不会添加这个标志位,InputDispatcher 后面会丢弃这个事件,也就是不会分发给用户。参考【1.1 截断策略查询
  2. 把按键事件包装成 KeyEntry 对象,然后加入到 InputDispatcher 的收件箱 InputDispatcher::mInboundQueue。参考【1.2 InputDispatcher 收件箱接收事件
  3. 如有必要,唤醒 InputDispatcher 线程处理事件。通常,InputDispatcher 线程处于休眠状态时,如果收到事件,那么需要唤醒线程来处理事件。

注意,这里的所有操作,不是发生在 InputDispatcher 线程,而是发生在 InputReader 线程,这个线程是负责不断地读取事件,因此这里的查询策略是否截断事件的过程,时间不能太长,否则影响了输入系统读取事件。

另外,在执行完截断策略后,会记录处理的时长,如果时长超过一定的阈值,会收到一个警告信息。我曾经听到其他项目的人在谈论 power 键亮屏慢的问题,那么可以在这里排查下。

1.1 截断策略查询

事件截断策略的查询过程,就是就是通过 JNI 调用上层 InputManagerService 的方法,而这个策略最终是由 PhoneWindowManager 实现的。如果策略不截断,如果策略不截断事件,那么在参数的策略标志位 policyFlags 中添加 POLICY_FLAG_PASS_TO_USER 标志位。

为何需要这个截断策略? 或者这样问,如果没有截断策略,那么会有什么问题呢? 假想我们正在处于通话,此时按下挂断电话按键,如果输入系统还有很多事件没有处理完,或者说,处理事件的时间较长,那么挂断电话的按键事件不能得到及时处理,这就相当影响用户体验。而如果有了截断策略,在输入系统正式处理事件前,就可以处理挂断电话按键事件。

因此,截断策略的作用就是及时处理系统一些重要的功能。这给我们一个什么提示呢?当硬件上添加了一个按键,如果想要快速响应这个按键的事件,那么就在截断策略中处理。

关于截断策略,以及后面的分发策略,是一个比较好的课题,我会在后面一篇文章中详细分析。

下面,理解几个概念

  1. 什么是交互状态?什么是非交互状态?简单理解,亮屏就是交互状态,灭屏就是非交互状态。但是,严格来说,并不准确,如果读者想知道具体的定义,可以查看我写的 PowerManagerService 的文章。
  2. 什么是受信任的事件?来自物理输入设备的事件都是受信任的,另外像 SystemUI ,由于申请了 android.permission.INJECT_EVENTS 权限, 因此它注入的 BACK, HOME 按键事件也都是受信任。
  3. 什么是注入事件?简单来说,不是由物理设备产生的事件。例如,导航栏上的 BACK, HOME 按键,它们的事件都是通过注入产生的,因此它们是注入事件。

1.2 InputDispatcher 收件箱接收事件

InputDispatcher::mInboundQueue 是 InputDispatcher 的事件收件箱,所有的事件,包括注入事件,都会加入这个收件箱。

如果收件箱之前没有”邮件”,当接收到”邮件”后,就需要唤醒 InputDispatcher 线程来处理”邮件”,这个逻辑很合理吧?

另外,聊一下这里提到的 app switch 按键。从上面的代码可知,HOME, RECENT, ENDCALL 按键都是 app switch 按键。当 app switch 按键抬起时,会计算一个超时时间,并且立即唤醒 InputDispatcher 线程来处理事件,因为这个事件很重要,需要及时处理,但是处理时间也不能太长,因此需要设置一个超时时间。

为何要给 app switch 按键设置一个超时时间? 假如我在操作一个界面,此时由于某些原因,例如 CPU 占用率过高,导致界面事件处理比较缓慢,也就是常说的卡顿现象。此时我觉得这个 app 太渣了,想杀掉它,怎么办呢? 按下导航栏的 RECENT 按键,然后干掉它。但是由于界面处理事件比较缓慢,因此 RECENT 按键事件可能不能得到及时处理,这就会让我很恼火,我很可能扔掉这个手机。因此需要给 app switch 按键设置一个超时时间,如果超时了,那么就会丢弃 app switch 按键之前的所有事件,来让 app switch 事件得到及时的处理。

2. InputDispatcher 处理按键事件

现在收件箱已 InputDispatcher::mInboundQueue 已经收到了按键事件,那么来看下 InputDisaptcher 线程如何处理按键事件的。由前面的文章可知,InputDisaptcher 线程循环的代码如下

InputDispatcher 的一次线程循环,做了如下几件事

  1. 执行一次事件分发。其实就是从收件箱中获取一个事件进行分发。注意,此过程只分发一个事件。也就是说,线程循环一次,只处理了一个事件。参考【2.1 分发事件
  2. 执行命令。 这个命令是哪里来的呢?是上一步事件分发中产生的。事件在发送给窗口前,会执行一次分发策略查询,而这个查询的方式就是创建一个命令来执行。
  3. 处理 ANR,并返回下一次线程唤醒的时间。窗口在接收到事件后,需要在规定的时间内处理,否则会发生 ANR。这里利用 ANR 的超时时间计算线程下次唤醒的时间,以便能及时处理 ANR。
  4. 线程休眠。线程被唤醒有很多种可能,例如窗口及时地返回了事件的处理结果,或者窗口处理事件超时了。

2.1 分发事件

InputDispatcher 线程的一次事件分发的过程如下

  1. 从收件箱取出事件。
  2. 分发事件。参考【3. 按键事件的分发
  3. 处理事件分发后的结果。 事件被丢弃或者发送给指定窗口,都会返回 true,表示事件被处理了,因此会重置 mPendingEvent。而如果事件分发的结果返回 false,表示事件没有被处理,这种情况表示系统暂时不知道如何处理,最常见的情况就是组合键的第一个按键被按下,例如截屏键的 power 键按下,此时系统不知道是不是要单独处理这个按键,还要等待组合键的另外一个按键,在超时前按下,因此系统不知道如何处理,需要等等看。

3. 按键事件的分发

按键事件分发的主要过程如下

  1. 执行分发策略。分发策略的作用,一方面是实现组合按键的功能,另外一方面是为了在事件分发给窗口前,给系统一个优先处理的机会。
  2. 寻找处理按键事件的焦点窗口。参考【3.1 寻找焦点窗口
  3. 只有成功寻找到焦点窗口,才进行按键事件分发。参考【3.2 分发按键事件给目标窗口

分发策略涉及到组合按键的实现,因此是一个非常复杂的话题,我们将在后面的文章中,把它和截断策略一起分析。

3.1 寻找焦点窗口

寻找目标窗口的过程其实就是找到焦点窗口,然后根据焦点窗口创建 InputTarget,保存到参数 inputTargets 中。

结合前面的代码分析,寻找焦点窗口返回的结果,会影响事件的处理,总结如下

寻找焦点窗口的结果 结果的说明 如何影响事件的处理
InputEventInjectionResult::SUCCEEDED 成功为事件找到焦点窗口 事件会被分发到焦点窗口
InputEventInjectionResult::FAILED 没有找到可用的焦点窗口 事件会被丢弃
InputEventInjectionResult::PERMISSION_DENIED 没有权限把事件发送到焦点窗口 事件会被丢弃
InputEventInjectionResult::PENDING 有焦点窗口,但是暂时不可用于接收事件 线程会休眠,等待时机被唤醒,发送事件到焦点窗口

在工作中,有时候需要我们分析按键事件为何没有找到焦点窗口,这里列举一下所有的情况,以供大家工作或面试时使用

  1. 没有焦点app,并且没有焦点窗口。这种情况应该比较极端,应该整个surface系统都出问题了。
  2. 窗口的 Feature 表示要丢弃事件。这个丢弃事件的 Feature 是 Surface 系统给窗口设置的,目前我还没有搞清楚这里面的逻辑。
  3. 焦点app启动焦点窗口,超时了。
  4. 对于注入事件,如果注入者的 UID 与焦点窗口的 UID 不同,并且注入者没有申请 android.Manifest.permission.INJECT_EVENTS 权限。

没有找到焦点窗口的所有情况,都有日志对应输出,可帮助我们定位问题。

现在来看一下,找到焦点窗口后,创建并保存 InputTarget 的过程

3.2 分发按键事件给目标窗口

现在,处理按键事件的焦点窗口已经找到,并且已经保存到 inputTargets,是时候来分发按键事件了

焦点窗口只有一个,为何需要一个 inputTargets 集合来保存所有的目标窗口,因为根据前面的分析,除了焦点窗口以外,还有一个全局的监听事件的输入目标。

WindowManagerService 会在创建窗口时,创建一个连接,其中一端给窗口,另外一端给输入系统。当输入系统需要发送事件给窗口时,就会通过这个连接进行发送。至于连接的建立过程,有点小复杂,本分不分析,后面如果写 WMS 的文章,再来细致分析一次。

找到这个窗口的连接后,就准备分发循环 ? 问题来了,什么是分发循环 ? InputDispatcher 把一个事件发送给窗口,窗口处理完事件,然后返回结果为 InputDispatcher,这就是一个循环。但是注意,分发事件给窗口,窗口返回处理事件结果,这两个是互为异步过程。

现在来看下分发循环之前的准备

分发循环前的准备工作,其实就是根据窗口所支持的分发模式(dispatche mode),调用enqueueDispatchEntryLocked() 创建并保存事件到连接的收件箱。前面分析过,焦点窗口的的分发模式为 InputTarget::FLAG_DISPATCH_AS_IS | InputTarget::FLAG_FOREGROUND,而此时只用到了InputTarget::FLAG_DISPATCH_AS_IS。 参考【3.2.1 根据分发模式,添加事件到连接收件箱

如果连接的收件箱之前没有事件,那么证明连接没有处于发送事件的状态中,而现在有事件了,那就启动分发循环来发送事件。参考 【3.2.2 启动分发循环

3.2.1 根据分发模式,添加事件到连接收件箱

根据前面创建 InputTarget 的代码可知,InputTarget::flags 的值为 InputTarget::FLAG_FOREGROUND | InputTarget::FLAG_DISPATCH_AS_IS

InputTarget::FLAG_FOREGROUND 表明事件正在发送给前台应用,InputTarget::FLAG_DISPATCH_AS_IS 表示事件按照原样进行发送。

而参数 dispatchMode 只使用了 InputTarget::FLAG_DISPATCH_AS_IS,因此,对于按键事件,只会创建并添加一个 DispatchEntry 到 Connection::outboundQueue

3.2.2 启动分发循环

现在,焦点窗口连接的发件箱中已经有事件了,此时真的到了发送事件给焦点窗口的时候了

事件分发循环的过程如下

  1. 通过窗口连接,把事件发送给窗口,并从连接的发件箱 Connection::outboundQueue 中移除。
  2. 把刚刚发送的事件,保存到连接的等待队列 Connection::waitQueue。连接在等待什么呢?当然是等到窗口的处理结果。
  3. 用 AnrTracker 记录事件处理的超时时间,如果事件处理超时,会引发 ANR。

既然叫做一个循环,现在事件已经发送出去了,那么如何接收处理结果呢? InputDispatcher 线程使用了底层的 Looper 机制,当窗口与输入系统建立连接时,Looper 通过 epoll 机制监听连接的输入端的文件描述符,当窗口通过连接反馈处理结果时,epoll 就会收到可读事件,因此 InputDispatcher 线程会被唤醒来读取窗口的事件处理结果,而这个过程就是下面的下面的回调函数

如果读者想了解底层的 Looper 机制,可以参考我写的 深入理解Native层消息机制

处理窗口反馈的事件处理结果的过程如下

  1. 根据连接的 token 获取连接。
  2. 从连接中读取窗口返回的事件处理结果。
  3. 完成这个事件的分发循环。此过程会创建一个命令,并加如到命令队列中,然后在第四步执行。
  4. 执行第三步创建的命令,以完成分发循环。

当监听到窗口的连接有事件到来时,会从连接读取窗口对事件的处理结果,然后创建一个即将执行的命令,保存到命令队列中,如下

命令是用来完成事件分发循环的,那么命令什么时候执行呢?这就是第四步执行的,最终调用如下函数来执行命令

分发循环的完成过程如下

  1. 检查连接中是否有对应的正在的等待的事件。
  2. 既然窗口已经反馈的事件的处理结果,那么从连接的等待队列 Connection::waitQueue 中移除。
  3. 既然窗口已经反馈的事件的处理结果,那么就不必处理这个事件的 ANR,因此移除事件的 ANR 超时时间。
  4. 既然此时窗口正在反馈事件的处理结果,那趁热打铁,那么开启下一次分发循环,发送连接发件箱中的事件。当然,如果发件箱没有事件,那么什么也不做。

完成分发循环,其实最主要的就是把按键事件从连接的等待队列中移除,以及解除 ANR 的触发。

总结

本文虽然分析的只是按键事件的分发过程,但是从整体上剖析了所有事件的分发过程。我们将以此为基础去分析触摸事件(motion event)的分发过程。

现在总结下一个按键事件的基本发送流程

  1. InputReader 线程把按键事件加入到 InputDispatcher 的收件箱之前,会询问截断策略,如果策略截断了,那么事件最终不会发送给窗口。
  2. InputDispatcher 通过一次线程循环来发送按键事件
  3. 事件在发送之前,会循环分发策略,主要是为了实现组合按键功能。
  4. 如果截断策略和分发策略都不截断按键事件,那么会寻找能处理按键事件的焦点窗口。
  5. 焦点窗口找到了,那么会把按键事件加入到窗口连接的发件箱中。
  6. 执行分发循环,从窗口连接的发件箱中获取事件,然后发送给窗口。然后把事件从发件箱中移除,并加入到连接的等待队列中。最后,记录 ANR 时间。
  7. 窗口返回事件的处理结果,InputDispatcher 会读取结果,然后把事件从连接的等待队列中移除,然后解除 ANR 的触发。
  8. 继续发送连接中的事件,并重复上述过程,直至连接中没有事件为止。

感想

我是一个注重实际效果的人,我花这么大力气去分析事件的分发流程,是否值得? 从长期的考虑看,肯定是值得的,从短期看,我们可以从 trace log 中分析出 ANR 的原因是否是因为事件处理超时。

最后,说一个题外话。我有个大学同学是这个平台的签约作者,一年下来的,平台给的费用有小几千。我老婆知道这个事情后问我怎么不搞呢? 我解释说,我不是一个喜欢被束缚的人,我觉得我的文章没写好,我就不会发出去,我觉得我心情不好,也不会把文章发出去,所以我更新文章喜欢断断续续。如果大家觉得我的文章的不错,可以关注我,如果觉得我的文章有什么建议,欢迎留言,bye~

本文转自 https://juejin.cn/post/7189181855596281913,如有侵权,请联系删除。

发表回复

您的电子邮箱地址不会被公开。 必填项已用 * 标注