-
Notifications
You must be signed in to change notification settings - Fork 126
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
反思|Android 输入系统 & ANR机制的设计与实现 #46
Comments
反思|Android 输入系统 & ANR机制的设计与实现四、ANR机制的设计与实现对 输入系统 有了更初步整体的认知之后,接下来本文将针对 通常来讲, 这样区分的原因是,首先,前者发生在 应用进程 组件中的 简单理解了之后,读者需要知道,「组件类 而 1、第一类原理概述具体不同在哪里呢,对于
将组件的 因此 接下来简单了解一下 输入系统 流程中 2、第二类原理概述
少数情况下开发者能够很快定位到问题,但更常见的情况是,该问题是 随机 且 难以复现 的,导致该问题的原因也更具有综合性,比如低端设备的系统本身资源已非常紧张,或者多线程相互持有彼此需要的资源导致 死锁 ,亦或其它复杂的情况,因此处理这类型问题就需要开发者对 输入系统 中的 与组件类 什么叫做 扫雷 呢,对于 输入系统 而言,即使某次事件执行时间超过预期的时长,只要用户后续没有再生成输入事件,那么也不需要 而只有当新一轮的输入事件到来,此时正在分发事件的窗口(即 这也正是用户在第一次点击屏幕,即使事件处理超时,也没有弹出 由此可见,组件类 现在我们对 3、事件分发的异步机制我们再次将目光转回到 先抛出一个新的问题,对处于 对于读者而言,不难得出答案是异步的,因为两者之间双向通信的建立是通过 因此,与应用进程中事件分发不同的是,后者我们通常可以认为是在主线程中同步的,而对于整个 输入系统 而言,因为涉及到 系统进程 与多个 应用进程 之间异步的通信,因此其内部的实现更为复杂。 因为事件分发涉及到异步回调机制,因此 4、三个队列
下文,笔者通过2轮事件分发的示例,对三个队列的作用进行简单的梳理。 4.1 第一轮事件分发首先 然后 如果 应用进程 事件分发正常,那么会通过 4.2 第二轮事件分发如果第一轮事件分发尚未接收到回调通知,第二轮事件分发抵达又是如何处理的呢? 第二轮事件到达 以下几种情况会导致进入
读者需要理解,并非所有「目标窗口还在处理上一个事件」都会抛出 这也正是将 至此,输入系统 检测到了 小结本文旨在对 本文从立题至发布,整个流程耗时近1个半月,在这个过程中,笔者参考了较本文内容数十倍的资料,受益颇深,也深感以 举重若轻 为写文目标之艰难——内容铺展容易,但通过 简洁 且 连贯 的语言来对一个庞大复杂的知识体系进行收拢,需要极强的 克制力 ,在这种严苛的要求下,每一句的描述都需要极高的 精确性 ,这对笔者而言是一个挑战,但真正完成之后,对整个知识体系的理解程度同样也是极高的。 而这也正是 反思 系列的初衷,希望你能喜欢。 参考 & 扩展阅读正如上文所言,输入系统 和 ANR 本身都是一个非常大的命题,除了宽广的知识体系,还需要亲身去实践和总结,下文列出若干相关参考资料,读者可根据自身需求选择性进行扩展阅读: 1、彻底理解安卓应用无响应机制 @Gityuan 深入学习 4、图解Android-Android的 Event Input System @漫天尘沙 笔者曾经想写一个 图解Android 系列,后来因为种种原因放弃了,没想到若干年前已经有先驱进行过了这样的尝试,并且,内容质量极高。笔者相信,能够花费非常大精力总结的文章一定不会被埋没,而这篇文章,注定会成为经典中的经典。 一个笔者最近关注非常优秀的作者,文章非常具有深度,其 6、Android 信号处理面面观 之 信号定义、行为和来源 @rambo2188 如果读者对「 7、Android开发高手课 @张绍文 实战中的经典之作,该课程每一小结都极具深度,价值不可估量。因或涉及到利益相关,而且推荐了也从张老师那里拿不到钱,因此本文不加链接并放在最下面(笑)。 关于我Hello,我是 却把清梅嗅 ,如果您觉得文章对您有价值,欢迎 ❤️,也欢迎关注我的 博客 或者 GitHub。 如果您觉得文章还差了那么点东西,也请通过 关注 督促我写出更好的文章——万一哪天我进步了呢? |
反思|Android 输入系统 & ANR机制的设计与实现
概述
对于
Android
开发者而言,ANR
是一个老生常谈的问题,站在面试者的角度,似乎说出 「不要在主线程做耗时操作」 就算合格了。但是,
ANR
机制到底是什么,其背后的原理究竟如何,为什么要设计出这样的机制?这些问题时时刻刻会萦绕脑海,而想搞清楚这些,就不得不提到Android
自身的 输入系统 (Input System
)。Android
自身的 输入系统 又是什么?一言以蔽之,任何与Android
设备的交互——我们称之为 输入事件,都需要通过 输入系统 进行管理和分发;这其中最靠近上层,并且最典型的一个小环节就是View
的 事件分发 流程。这样看来,输入系统 本身确实是一个非常庞大复杂的命题,并且,越靠近底层细节,越容易有一种 只见树木不见树林 之感,反复几次,直至迷失在细节代码的较真中,一次学习的努力尝试付诸东流。
因此,控制住原理分析的粒度,在宏观的角度,系统地了解输入系统本身的设计理念,并引申到实际开发中的
ANR
现象的原理和解决思路 ,是一个非常不错的理论与实践相结合的学习方式,这也正是笔者写作本文的初衷。本文篇幅较长,思维导图如下:
一、自顶向下探索
谈到
Android
系统本身,首先,必须将 应用进程 和 系统进程 有一个清晰的认知,前者一般代表开发者依托Android
平台本身创造开发的应用;后者则代表Android
系统自身创建的核心进程。这里我们抛开 应用进程 ,先将视线转向 系统进程,因为 输入系统 本身是由后者初始化和管理调度的。
Android
系统在启动的时候,会初始化zygote
进程和由zygote
进程fork
出来的SystemServer
进程;作为 系统进程 之一,SystemServer
进程会提供一系列的系统服务,而接下来要讲到的InputManagerService
也正是由SystemServer
提供的。在
SystemServer
的初始化过程中,InputManagerService
(下称IMS
)和WindowManagerService
(下称WMS
)被创建出来;其中WMS
本身的创建依赖IMS
对象的注入:在 输入系统 中,
WMS
非常重要,其负责管理IMS
、Window
与ActivityManager
之间的通信,这里点到为止,后文再进行补充,我们先来看IMS
。顾名思义,
IMS
服务的作用就是负责输入模块在Java
层级的初始化,并通过JNI
调用,在Native
层进行更下层输入子系统相关功能的创建和预处理。在
JNI
的调用过程中,IMS
创建了NativeInputManager
实例,NativeInputManager
则在初始化流程中又创建了EventHub
和InputManager
:此时我们已经处于
Native
层级。读者需要注意,对于整个Native
层级而言,其向下负责与Linux
的设备节点中获取输入,向上则与靠近用户的Java
层级相通信,可以说是非常重要。而在该层级中,EventHub
和InputManager
又是最核心的两个角色。这两个角色的职责又是什么呢?首先来说
EventHub
,它是底层 输入子系统 中的核心类,负责从物理输入设备中不断读取事件(Event
),然后交给InputManager
,后者内部封装了InputReader
和InputDispatcher
,用来从EventHub
中读取事件和分发事件:简单来看,
EventHub
建立了Linux
与输入设备之间的通信,InputManager
中的InputReader
和InputDispatcher
负责了输入事件的读取和分发,在 输入系统 中,两者的确非常重要。这里借用网上的图对此进行一个简单的概括:
二、EventHub 与 epoll 机制
对于
EventHub
的具体实现,绝大多数App
开发者也许并不需要去花太多时间深入——简单了解其职责,然后一笔带过似乎是笔划算的买卖。但是在
EventHub
的实现细节中笔者发现,其对epoll
机制的利用是一个非常经典的学习案例,因此,花时间稍微深入了解也绝对是一举两得。上文说到,
EventHub
建立了Linux
与输入设备之间的通信,其实这种描述是不准确的,那么,EventHub
是为了解决什么问题而设计的呢,其具体又是如何实现的?1、多输入设备与输入子系统
我们知道,
Android
设备可以同时连接多个输入设备,比如 屏幕 、 键盘 、 鼠标 等等,用户在任意设备上的输入都会产生一个中断,经由Linux
内核的中断处理及设备驱动转换成一个Event
,最终交给用户空间的应用程序进行处理。Linux
内核提供了一个便于将不同设备不同数据接口统一转换的抽象层,只要底层输入设备驱动程序按照这层抽象接口实现,应用就可以通过统一接口访问所有输入设备,这便是Linux
内核的 输入子系统。那么 输入子系统 如何是针对接收到的
Event
进行的处理呢?这就不得不提到EventHub
了,它是底层Event
处理的枢纽,其利用了epoll
机制,不断接收到输入事件Event
,然后将其向上层的InputReader
传递。2、什么是epoll机制
这是常见于面试
Handler
相关知识点时的一道进阶题,变种问法是:「既然Handler
中的Looper
中通过一个死循环不断轮询,为什么程序没有因为无限死循环导致崩溃或者ANR
?」读者应该知道,
Handler
简单的利用了epoll
机制,做到了消息队列的阻塞和唤醒。关于epoll
机制,这里有一篇非常经典的解释,不了解其设计理念的读者 有必要 了解一下:参考上文,这里我们对
epoll
机制进行一个简单的总结:EventHub
中使用epoll
的恰到好处——多个物理输入设备对应了多个不同的输入流,通过epoll
机制,在EventHub
初始化时,分别创建mEpollFd
和mINotifyFd
;前者用于监听设备节点是否有设备文件的增删,后者用于监听是否有可读事件,创建管道,让InputReader
来读取事件:三、事件的读取和分发
本章节将对
InputReader
和InputDispatcher
进行系统性的介绍。1、InputReader:读取事件
InputReader
是什么?简单理解InputReader
的作用,通过从EventHub
获取事件后,将事件进行对应的处理,然后将事件进行封装并添加到InputDispatcher
的队列中,最后唤醒InputDispatcher
进行下一步的事件分发。乍得一看,在 输入系统 的
Native
层中,InputReader
似乎平凡无奇,但越是看似朴实无华的事物,在整个流程中往往占据绝对重要的作用。首先,
EventHub
传过来的Event
除了普通的 输入事件 外,还包含了设备本身的增、删、扫描 等事件,这些额外的事件处理并没有直接交给InputDispatcher
去分发,而是在InputReader
中进行了处理。当某个时间发生——可能是用户 按键输入,或者某个 设备插入,亦或 设备属性被调整 ,
epoll_wait()
返回并将Event
存入。这之后,
InputReader
对输入事件进行了一次读取,因为不同设备对事件的处理逻辑又各自不同,因此InputReader
内部持有一系列的Mapper
对事件进行 匹配 ,如果不匹配则忽略事件,反之则将Event
封装成一个新的NotifyArgs
数据对象,准备存入队列中,即唤醒InputDispatcher
进行分发。巧妙的是,在唤醒
InputDispatcher
进行分发之前,InputReader
在自己的线程中先执行了一个很特殊的 拦截操作 环节。2、输入事件的拦截和转换
读者知道,在应用开发中,一些特殊的输入事件是无法通过普通的方式进行拦截的;比如音量键,
Power
键,电话键,以及一些特殊的组合键,这里我们通称为 系统按键。这点无可厚非,虽然
Android
系统对于开发者足够的开放,但是一切都是有限制的,绝大多数的 用户按键 通常可以被应用拦截处理,但是 系统按键 绝对不行——这种限制往往能够给予用户设备安全最后的保障。因此,在
InputReader
唤醒InputDispatcher
进行事件分发之前,InputReader
在自己的线程中进行了两轮拦截处理。首先的第一轮拦截操作就是对 系统按键 级别的 输入事件 进行处理,对于手机而言,这个工作是在
PhoneWindowManager
中完成;举例来说,当用户按了Power
(电源)键,Android
设备本身会切唤醒或睡眠——即亮屏和息屏。这也正是「在技术论坛中,通常对 系统按键 拦截处理的技术方案,基本都是需要修改
PhoneWindowManager
的源码」的原因。接下来输入事件进入到第二轮的处理中,如果用户在
Setting->Accessibility
中选择打开某些功能,以 手势识别 为例,Android
的AccessbilityManagerService
(辅助功能服务) 可能会根据需要转换成新的Event
,比如说两根手指头捏动的手势最终会变成ZoomEvent
。需要注意的是,这里的拦截处理并不会真正将事件 消费 掉,而是通过特殊的方式将事件进行标记(
policyFlags
),然后在InputDispatcher
中处理。至此,
InputReader
对 输入事件 完整的一轮处理到此结束,这之后,InputReader
又进入了新一轮等待。3、InputDispatcher:分发事件
当
wake()
函数将在Looper
中睡眠等待的InputDispatcher
唤醒时,InputDispatcher
开始新一轮事件的分发。InputDispatcher
的线程负责将接收到的 输入事件 分发给 目标应用窗口,在这个过程中,InputDispatcher
首先需要对上个环节中标记了需要拦截的 系统按键 相关事件进行拦截,被拦截的事件至此不再向下分发。这之后,
InputDispatcher
进入了本文最关键的一个环节——调用findFocusedWindowTargetLocked()
获取当前的 焦点窗口 ,同时检测目标应用是否有ANR
发生。如果检测到目标窗口处于正常状态,即
ANR
并未发生时,InputDispatcher
进入真正的分发程序,将事件对象进行新一轮的封装,通过SocketPair
唤醒目标窗口所在进程的Looper
线程,即我们应用进程中的主线程,后者会读取相应的键值并进行处理。表面来看,整个分发流程似乎干净简洁且便于理解,但实际上
InputDispatcher
整个流程的逻辑十分复杂,试想一次事件分发要横跨3个线程的流程又怎会简单?此外,
InputDispatcher
还负责了 ANR 的处理,这又导致整个流程的复杂度又上升了一个层级,这个流程我们在后文的ANR
章节中进行更细致的分析,因此先按住不提。接下来,我们来看看整个 输入事件 的分发流程中, 应用进程 是如何与 系统进程 建立相应的通信链接的。
4、通过Socket建立通信
我们知道,
InputReader
和InputDispatcher
运行在system_server
系统进程 中,而用户操作的应用都运行在自己的 应用进程 中;这里就涉及到跨进程通信,那么 应用进程 是如何与 系统进程 建立通信的呢?让我们回到文章最初
WindowManagerService(WMS)
和InputManagerService(IMS)
初始化的流程中来,当IMS
以及其他的系统服务初始化完成之后,应用程序开始启动。如果一个应用程序有
Activity
(只有Activity
能够接受用户输入),那么它要将自己的Window
注册到WMS
中。在这里,
Android
使用了Socket
而不是Binder
来完成。WMS
中通过OpenInputChannelPair
生成了两个Socket
的FD
, 代表一个双向通道的两端:向一端写入数据,另外一端便可以读出;反之,如果一端没有写入数据,另外一端去读,则陷入阻塞等待。最终
InputDispatcher
中建立了目标应用的Connection
对象,代表与远端应用的窗口建立了链接;同样,应用进程中的ViewRootImpl
创建了WindowInputEventReceiver
用于接受InputDispatchor
传过来的事件:这里我们对该次 跨进程通信建立流程 有了初步的认知,对于
Android
系统而言,Binder
是最广泛的跨进程通信的应用方式,但是Android
系中跨进程通信就仅仅只用到了Binder
吗?答案是否定的,至少在 输入系统 中,除了Binder
之外,Socket
同样起到了举足轻重的作用。那么新的问题就来了,这里为什么选择
Socket
而不是选择Binder
呢,关于这个问题的解释,笔者找到了一个很好的版本:现在,应用进程 能够收到由
InputDispatcher
处理完成并分发过来的 输入事件 了。至此,我们来到了最熟悉的应用层级事件分发流程。对于这之后 应用层级的事件分发,可以阅读下述笔者的另外两篇文章,本文不赘述。The text was updated successfully, but these errors were encountered: