反思 系列博客是个人一种新学习方式的尝试,该系列起源和目录请参考 这里 。html
对于Android
开发者而言,ANR
是一个老生常谈的问题,站在面试者的角度,彷佛说出 「不要在主线程作耗时操做」 就算合格了。java
可是,ANR
机制究竟是什么,其背后的原理究竟如何,为何要设计出这样的机制?这些问题时时刻刻会萦绕脑海,而想搞清楚这些,就不得不提到Android
自身的 输入系统 (Input System
)。android
Android
自身的 输入系统 又是什么?一言以蔽之,任何与Android
设备的交互——咱们称之为 输入事件,都须要经过 输入系统 进行管理和分发;这其中最靠近上层,而且最典型的一个小环节就是View
的 事件分发 流程。git
这样看来,输入系统 自己确实是一个很是庞大复杂的命题,而且,越靠近底层细节,越容易有一种 只见树木不见树林 之感,反复几回,直至迷失在细节代码的较真中,一次学习的努力尝试付诸东流。github
所以,控制住原理分析的粒度,在宏观的角度,系统地了解输入系统自己的设计理念,并引伸到实际开发中的ANR
现象的原理和解决思路 ,是一个很是不错的理论与实践相结合的学习方式,这也正是笔者写做本文的初衷。面试
本文篇幅较长,思惟导图以下:安全
谈到Android
系统自己,首先,必须将 应用进程 和 系统进程 有一个清晰的认知,前者通常表明开发者依托Android
平台自己创造开发的应用;后者则表明 Android
系统自身建立的核心进程。markdown
这里咱们抛开 应用进程 ,先将视线转向 系统进程,由于 输入系统 自己是由后者初始化和管理调度的。数据结构
Android
系统在启动的时候,会初始化zygote
进程和由zygote
进程fork
出来的SystemServer
进程;做为 系统进程 之一,SystemServer
进程会提供一系列的系统服务,而接下来要讲到的InputManagerService
也正是由 SystemServer
提供的。多线程
在SystemServer
的初始化过程当中,InputManagerService
(下称IMS
)和WindowManagerService
(下称WMS
)被建立出来;其中WMS
自己的建立依赖IMS
对象的注入:
// SystemServer.java
private void startOtherServices() {
// ...
InputManagerService inputManager = new InputManagerService(context);
// inputManager做为WindowManagerService的构造参数
WindowManagerService wm = WindowManagerService.main(context,inputManager, ...);
}
复制代码
在 输入系统 中,WMS
很是重要,其负责管理IMS
、Window
与ActivityManager
之间的通讯,这里点到为止,后文再进行补充,咱们先来看IMS
。
顾名思义,IMS
服务的做用就是负责输入模块在Java
层级的初始化,并经过JNI
调用,在Native
层进行更下层输入子系统相关功能的建立和预处理。
在JNI
的调用过程当中,IMS
建立了NativeInputManager
实例,NativeInputManager
则在初始化流程中又建立了EventHub
和InputManager
:
NativeInputManager::NativeInputManager(jobject contextObj, jobject serviceObj, const sp<Looper>& looper) : mLooper(looper), mInteractive(true) {
// ...
// 建立一个EventHub对象
sp<EventHub> eventHub = new EventHub();
// 建立一个InputManager对象
mInputManager = new InputManager(eventHub, this, this);
}
复制代码
此时咱们已经处于Native
层级。读者须要注意,对于整个Native
层级而言,其向下负责与Linux
的设备节点中获取输入,向上则与靠近用户的Java
层级相通讯,能够说是很是重要。而在该层级中,EventHub
和InputManager
又是最核心的两个角色。
这两个角色的职责又是什么呢?首先来讲EventHub
,它是底层 输入子系统 中的核心类,负责从物理输入设备中不断读取事件(Event
),而后交给InputManager
,后者内部封装了InputReader
和InputDispatcher
,用来从EventHub
中读取事件和分发事件:
InputManager::InputManager(...) {
mDispatcher = new InputDispatcher(dispatcherPolicy);
mReader = new InputReader(eventHub, readerPolicy, mDispatcher);
initialize();
}
复制代码
简单来看,EventHub
创建了Linux
与输入设备之间的通讯,InputManager
中的InputReader
和InputDispatcher
负责了输入事件的读取和分发,在 输入系统 中,二者的确很是重要。
这里借用网上的图对此进行一个简单的归纳:
对于EventHub
的具体实现,绝大多数App
开发者也许并不须要去花太多时间深刻——简单了解其职责,而后一笔带过彷佛是笔划算的买卖。
可是在EventHub
的实现细节中笔者发现,其对epoll
机制的利用是一个很是经典的学习案例,所以,花时间稍微深刻了解也绝对是一箭双雕。
上文说到,EventHub
创建了Linux
与输入设备之间的通讯,其实这种描述是不许确的,那么,EventHub
是为了解决什么问题而设计的呢,其具体又是如何实现的?
咱们知道,Android
设备能够同时链接多个输入设备,好比 屏幕 、 键盘 、 鼠标 等等,用户在任意设备上的输入都会产生一个中断,经由Linux
内核的中断处理及设备驱动转换成一个Event
,最终交给用户空间的应用程序进行处理。
Linux
内核提供了一个便于将不一样设备不一样数据接口统一转换的抽象层,只要底层输入设备驱动程序按照这层抽象接口实现,应用就能够经过统一接口访问全部输入设备,这即是Linux
内核的 输入子系统。
那么 输入子系统 如何是针对接收到的Event
进行的处理呢?这就不得不提到EventHub
了,它是底层Event
处理的枢纽,其利用了epoll
机制,不断接收到输入事件Event
,而后将其向上层的InputReader
传递。
这是常见于面试Handler
相关知识点时的一道进阶题,变种问法是:「既然Handler
中的Looper
中经过一个死循环不断轮询,为何程序没有由于无限死循环致使崩溃或者ANR
?」
读者应该知道,Handler
简单的利用了epoll
机制,作到了消息队列的阻塞和唤醒。关于epoll
机制,这里有一篇很是经典的解释,不了解其设计理念的读者 有必要 了解一下:
参考上文,这里咱们对epoll
机制进行一个简单的总结:
epoll
能够理解为event poll
,不一样于忙轮询和无差异轮询,在 多个输入流 的状况下,epoll
只会把哪一个流发生了怎样的I/O事件通知咱们。此时咱们对这些流的操做都是有意义的。
EventHub
中使用epoll
的恰到好处——多个物理输入设备对应了多个不一样的输入流,经过epoll
机制,在EventHub
初始化时,分别建立mEpollFd
和mINotifyFd
;前者用于监听设备节点是否有设备文件的增删,后者用于监听是否有可读事件,建立管道,让InputReader
来读取事件:
本章节将对InputReader
和InputDispatcher
进行系统性的介绍。
InputReader
是什么?简单理解InputReader
的做用,经过从EventHub
获取事件后,将事件进行对应的处理,而后将事件进行封装并添加到InputDispatcher
的队列中,最后唤醒InputDispatcher
进行下一步的事件分发。
乍得一看,在 输入系统 的Native
层中,InputReader
彷佛平凡无奇,但越是看似朴实无华的事物,在整个流程中每每占据绝对重要的做用。
首先,EventHub
传过来的Event
除了普通的 输入事件 外,还包含了设备自己的增、删、扫描 等事件,这些额外的事件处理并无直接交给InputDispatcher
去分发,而是在InputReader
中进行了处理。
当某个时间发生——多是用户 按键输入,或者某个 设备插入,亦或 设备属性被调整 ,epoll_wait()
返回并将Event
存入。
这以后,InputReader
对输入事件进行了一次读取,由于不一样设备对事件的处理逻辑又各自不一样,所以InputReader
内部持有一系列的Mapper
对事件进行 匹配 ,若是不匹配则忽略事件,反之则将Event
封装成一个新的NotifyArgs
数据对象,准备存入队列中,即唤醒InputDispatcher
进行分发。
巧妙的是,在唤醒InputDispatcher
进行分发以前,InputReader
在本身的线程中先执行了一个很特殊的 拦截操做 环节。
读者知道,在应用开发中,一些特殊的输入事件是没法经过普通的方式进行拦截的;好比音量键,Power
键,电话键,以及一些特殊的组合键,这里咱们通称为 系统按键。
这点无可厚非,虽然Android
系统对于开发者足够的开放,可是一切都是有限制的,绝大多数的 用户按键 一般能够被应用拦截处理,可是 系统按键 绝对不行——这种限制每每可以给予用户设备安全最后的保障。
所以,在InputReader
唤醒InputDispatcher
进行事件分发以前,InputReader
在本身的线程中进行了两轮拦截处理。
首先的第一轮拦截操做就是对 系统按键 级别的 输入事件 进行处理,对于手机而言,这个工做是在PhoneWindowManager
中完成;举例来讲,当用户按了Power
(电源)键,Android
设备自己会切唤醒或睡眠——即亮屏和息屏。
这也正是「在技术论坛中,一般对 系统按键 拦截处理的技术方案,基本都是须要修改PhoneWindowManager
的源码」的缘由。
接下来输入事件进入到第二轮的处理中,若是用户在Setting->Accessibility
中选择打开某些功能,以 手势识别 为例,Android
的AccessbilityManagerService
(辅助功能服务) 可能会根据须要转换成新的Event
,好比说两根手指头捏动的手势最终会变成ZoomEvent
。
须要注意的是,这里的拦截处理并不会真正将事件 消费 掉,而是经过特殊的方式将事件进行标记(policyFlags
),而后在InputDispatcher
中处理。
至此,InputReader
对 输入事件 完整的一轮处理到此结束,这以后,InputReader
又进入了新一轮等待。
当wake()
函数将在Looper
中睡眠等待的InputDispatcher
唤醒时,InputDispatcher
开始新一轮事件的分发。
准确来讲,
InputDispatcher
被唤醒时,wake()
函数实际是在InputManagerService
的线程中执行的,即整个流程的线程切换顺序为InputReaderThread
->InputManagerServiceThread
->InputDispatcherThread
。
InputDispatcher
的线程负责将接收到的 输入事件 分发给 目标应用窗口,在这个过程当中,InputDispatcher
首先须要对上个环节中标记了须要拦截的 系统按键 相关事件进行拦截,被拦截的事件至此再也不向下分发。
这以后,InputDispatcher
进入了本文最关键的一个环节——调用 findFocusedWindowTargetLocked()
获取当前的 焦点窗口 ,同时检测目标应用是否有ANR
发生。
若是检测到目标窗口处于正常状态,即ANR
并未发生时,InputDispatcher
进入真正的分发程序,将事件对象进行新一轮的封装,经过SocketPair
唤醒目标窗口所在进程的Looper
线程,即咱们应用进程中的主线程,后者会读取相应的键值并进行处理。
表面来看,整个分发流程彷佛干净简洁且便于理解,但实际上InputDispatcher
整个流程的逻辑十分复杂,试想一次事件分发要横跨3个线程的流程又怎会简单?
此外,InputDispatcher
还负责了 ANR 的处理,这又致使整个流程的复杂度又上升了一个层级,这个流程咱们在后文的ANR
章节中进行更细致的分析,所以先按住不提。
接下来,咱们来看看整个 输入事件 的分发流程中, 应用进程 是如何与 系统进程 创建相应的通讯连接的。
关于 跨进程通讯的创建 这一节,笔者最初打算做为一个大的章节来说,可是对于整个 输入系统 而言,其彷佛又只是一个 重要非必需 的知识点。最终,笔者将其放在一个小节中进行简单的描述,有兴趣的读者能够在文末的参考连接中查阅更详尽的资料。
咱们知道,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
呢,关于这个问题的解释,笔者找到了一个很好的版本:
Socket
能够实现异步的通知,且只须要两个线程参与(Pipe
两端各一个),假设系统有N
个应用程序,跟输入处理相关的线程数目是N+1
(1
是Input Dispatcher
线程)。然而,若是用Binder
实现的话,为了实现异步接收,每一个应用程序须要两个线程,一个Binder
线程,一个后台处理线程(不能在Binder
线程里处理输入,由于这样太耗时,将会堵塞住发送端的调用线程)。在发送端,一样须要两个线程,一个发送线程,一个接收线程来接收应用的完成通知,因此,N
个应用程序须要2(N+1)
个线程。相比之下,Socket
仍是高效多了。
如今,应用进程 可以收到由InputDispatcher
处理完成并分发过来的 输入事件 了。至此,咱们来到了最熟悉的应用层级事件分发流程。对于这以后 应用层级的事件分发,能够阅读下述笔者的另外两篇文章,本文不赘述。
对 输入系统 有了更初步总体的认知以后,接下来本文将针对ANR
机制进行更深一步的探索。
一般来说,ANR
的来源分为Service、Broadcast、Provider
以及Input
两种。
这样区分的缘由是,首先,前者发生在 应用进程 组件中的ANR
问题一般是相对好解决的,若ANR
自己容易复现,开发者一般仅须要肯定组件的代码中是否在 主线程中作了耗时处理;然后者ANR
发生的缘由为 输入事件 分发超时,包括按键和屏幕的触摸事件,经过阅读上一章节,读者知道 输入系统 中负责处理ANR
问题的是处于 系统进程 中的InputDispatcher
,其整个流程相比前者而言逻辑更加复杂。
简单理解了以后,读者须要知道,「组件类ANR
发生缘由一般是因为 主线程中作了耗时处理」这种说法其实是笼统的,更准确的讲,其本质的缘由是 组件任务调度超时,而在设备资源紧凑的状况下,ANR
的发生更可能是综合性的缘由。
而Input
类型的ANR
相对于Service、Broadcast、Provider
,其内部的机制又大相径庭。
具体不一样在哪里呢,对于Service、Broadcast、Provider
组件类的ANR
而言,Gityuan 在 这篇文章 中作了一个很是精妙的解释:
ANR
是一套监控Android
应用响应是否及时的机制,能够把发生ANR
比做是 引爆炸弹,那么整个流程包含三部分组成:
- 埋定时炸弹:中控系统(
system_server
进程)启动倒计时,在规定时间内若是目标(应用进程)没有干完全部的活,则中控系统会定向炸毁(杀进程)目标。- 拆炸弹:在规定的时间内干完工地的全部活,并及时向中控系统报告完成,请求解除定时炸弹,则幸免于难。
- 引爆炸弹:中控系统当即封装现场,抓取快照,搜集目标执行慢的罪证(
traces
),便于后续的案件侦破(调试分析),最后是炸毁目标。
将组件的ANR
机制比喻为 定时炸弹 很是贴切,以Service
为例,对于Android
系统而言,启动一个服务其本质是进程间的异步通讯,那么,如何判断Service
是否启动成功,若是一直没有成功,那么如何处理?
所以Android
设计了一个 置之死地然后生 的机制,在尝试启动Service
时,让中控系统system_server
埋下一个 定时炸弹 ,当Service
完成启动,拆掉炸弹;不然在system_server
的ActivityManager
线程中引爆炸弹,这就是组件类ANR
机制的原理:
接下来简单了解一下 输入系统 流程中ANR
机制的原理。
Input
类型的ANR
在平常开发中更为常见且更复杂,好比用户或者测试反馈,点击屏幕中的UI元素致使「卡死」。
少数状况下开发者可以很快定位到问题,但更常见的状况是,该问题是 随机 且 难以复现 的,致使该问题的缘由也更具备综合性,好比低端设备的系统自己资源已很是紧张,或者多线程相互持有彼此须要的资源致使 死锁 ,亦或其它复杂的状况,所以处理这类型问题就须要开发者对 输入系统 中的ANR
机制有必定的了解。
与组件类ANR
不一样的是,Input
类型的超时机制并不是时间到了必定就会爆炸,而是处理后续上报事件的过程才会去检测是否该爆炸,因此更像是 扫雷 的过程。
什么叫作 扫雷 呢,对于 输入系统 而言,即便某次事件执行时间超过预期的时长,只要用户后续没有再生成输入事件,那么也不须要ANR
。
而只有当新一轮的输入事件到来,此时正在分发事件的窗口(即App
应用自己)迟迟没法释放资源给新的事件去分发,这时InputDispatcher
才会根据超时时间,动态的判断是否须要向对应的窗口提示ANR
信息。
这也正是用户在第一次点击屏幕,即便事件处理超时,也没有弹出ANR
窗口,而当用户下意识再次点击屏幕时,屏幕上才提示出了ANR
信息的缘由。
因而可知,组件类ANR
和Input ANR
原理上确实有所不一样;除此以外,前者是在ActivityManager
线程中处理的ANR
信息,后者则是在InputDispatcher
线程中处理的ANR
,这里经过一张图简单了解一下后者的总体流程:
如今咱们对Input
类型的ANR
机制有了一个简单的了解,下文将针对其更深刻性的细节实现进行探讨。
咱们再次将目光转回到InputDispatcher
的实现细节。
先抛出一个新的问题,对处于system_server
进程Native
层级的 事件分发 而言,其向下与 应用进程 的通讯的过程应该是同步仍是异步的?
对于读者而言,不可贵出答案是异步的,由于二者之间双向通讯的创建是经过SocketPair
,而且,由于system_server
中InputDispatcher
对事件的分发其实是一对多的,若是是同步的,那么一旦其中一个应用分发超时,那么InputDispatcher
线程天然被卡住,其永远都不可能进入到下一轮的事件分发中,扫雷 机制更是无从谈起。
所以,与应用进程中事件分发不一样的是,后者咱们一般能够认为是在主线程中同步的,而对于整个 输入系统 而言,由于涉及到 系统进程 与多个 应用进程 之间异步的通讯,所以其内部的实现更为复杂。
由于事件分发涉及到异步回调机制,所以InputDispatcher
须要对事件进行维护和管理,那么问题就变成了,使用什么样的数据结构去维护这些输入事件比较合适。
InputDispatcher
的源码实现中,总体的事件分发流程共使用到3个事件队列:
InputReader
发送过来的输入事件;下文,笔者经过2轮事件分发的示例,对三个队列的做用进行简单的梳理。
首先InputReader
线程经过EventHub
监听到底层的输入事件上报,并将其放入了mInBoundQueue
中,同时唤醒了InputDispatcher
线程。
而后InputDispatcher
开始了第一轮的事件分发,此时并无正在处理的事件,所以InputDispatcher
从mInBoundQueue
队列头部取出事件,并重置ANR
的计时,并检查窗口是否就绪,此时窗口准备就绪,将该事件转移到了outBoundQueue
队列中,由于应用管道对端链接正常,所以事件从outBoundQueue
取出,而后放入了waitQueue
队列,由于Socket
双向通讯已经创建,接下来就是 应用进程 接收到新的事件,而后对其进行分发。
若是 应用进程 事件分发正常,那么会经过Socket
向system_server
通知完成,则对应的事件最终会从waitQueue
队列中移除。
若是第一轮事件分发还没有接收到回调通知,第二轮事件分发抵达又是如何处理的呢?
第二轮事件到达InputDispatcher
时,此时InputDispatcher
发现有事件正在处理,所以不会从mInBoundQueue
取出新的事件,而是直接检查窗口是否就绪,若未就绪,则进入ANR
检测状态。
如下几种状况会致使进入ANR
检测状态:
一、目标应用不会空,而目标窗口为空。说明应用程序在启动过程当中出现了问题; 二、目标
Activity
的状态是Pause
,即再也不是Focused
的应用; 三、目标窗口还在处理上一个事件。
读者须要理解,并不是全部「目标窗口还在处理上一个事件」都会抛出ANR
,而是须要经过检测时间,若是未超时,那么直接停止本轮事件分发,反之,若是事件分发超时,那么才会肯定ANR
的发生。
这也正是将Input
类型的ANR
描述为 扫雷 的缘由:这里的扫雷是指当前输入系统中正在处理着某个耗时事件的前提下,后续的每一次input
事件都会检测前一个正在处理的事件是否超时(进入扫雷状态),检测当前的时间距离上次输入事件分发时间点是否超时。若是前一个输入事件,则会重置ANR
的timeout
,从而不会爆炸。
至此,输入系统 检测到了ANR
的发生,并向上层抛出了本次ANR
的相关信息。
本文旨在对Android
输入系统 进行一个系统性的概述,读者不该将本文做为惟一的学习资料,而应该经过本文对该知识体系进行初步的了解,并根据自身要求进行单个方向细节性的突破。而已经掌握了骨骼架构的读者而言,更细节性的知识点也不过是待丰富的血肉而已。
本文从立题至发布,整个流程耗时近1个半月,在这个过程当中,笔者参考了较本文内容数十倍的资料,受益颇深,也深感以 举重若轻 为写文目标之艰难——内容铺展容易,但经过 简洁 且 连贯 的语言来对一个庞大复杂的知识体系进行收拢,须要极强的 克制力 ,在这种严苛的要求下,每一句的描述都须要极高的 精确性 ,这对笔者而言是一个挑战,但真正完成以后,对整个知识体系的理解程度一样也是极高的。
而这也正是 反思 系列的初衷,但愿你能喜欢。
正如上文所言,输入系统 和 ANR 自己都是一个很是大的命题,除了宽广的知识体系,还须要亲身去实践和总结,下文列出若干相关参考资料,读者可根据自身需求选择性进行扩展阅读:
一、完全理解安卓应用无响应机制 @Gityuan
二、Input系统—ANR原理分析 @Gityuan
三、理解Android ANR的触发原理 @Gityuan
深刻学习ANR
机制资料,Gityuan
的ANR
博客系列绝对是先驱级别的,尤为是第1篇文章中,其对于 定时炸弹 和 扫雷 的形容,贴切且易理解,这种 举重若轻 的写做风格体现了做者自己对整个知识体系的深度掌握;然后两篇文章则针对两种类型的ANR
分别进行了源码级别的分析,很是下饭。
四、图解Android-Android的 Event Input System @漫天尘沙
笔者曾经想写一个 图解Android 系列,后来由于种种缘由放弃了,没想到若干年前已经有先驱进行过了这样的尝试,而且,内容质量极高。笔者相信,可以花费很是大精力总结的文章必定不会被埋没,而这篇文章,注定会成为经典中的经典。
一个笔者最近关注很是优秀的做者,文章很是具备深度,其Input
系列针对整个输入系统进行了更细致源码级别的分析,很是值得收藏。
六、Android 信号处理面面观 之 信号定义、行为和来源 @rambo2188
若是读者对「Android
系统信号处理的行为」感兴趣,那么这篇文章绝对不能错过。
七、Android开发高手课 @张绍文
实战中的经典之做,该课程每一小结都极具深度,价值不可估量。因或涉及到利益相关,并且推荐了也从张老师那里拿不到钱,所以本文不加连接并放在最下面(笑)。
Hello,我是 却把清梅嗅 ,若是您以为文章对您有价值,欢迎 ❤️,也欢迎关注个人 博客 或者 GitHub。
若是您以为文章还差了那么点东西,也请经过 关注 督促我写出更好的文章——万一哪天我进步了呢?