反思|Android 输入系统 & ANR机制的设计与实现

反思 系列博客是个人一种新学习方式的尝试,该系列起源和目录请参考 这里html

概述

对于Android开发者而言,ANR是一个老生常谈的问题,站在面试者的角度,彷佛说出 「不要在主线程作耗时操做」 就算合格了。java

可是,ANR机制究竟是什么,其背后的原理究竟如何,为何要设计出这样的机制?这些问题时时刻刻会萦绕脑海,而想搞清楚这些,就不得不提到Android自身的 输入系统Input System)。android

Android自身的 输入系统 又是什么?一言以蔽之,任何与Android设备的交互——咱们称之为 输入事件,都须要经过 输入系统 进行管理和分发;这其中最靠近上层,而且最典型的一个小环节就是View事件分发 流程。git

这样看来,输入系统 自己确实是一个很是庞大复杂的命题,而且,越靠近底层细节,越容易有一种 只见树木不见树林 之感,反复几回,直至迷失在细节代码的较真中,一次学习的努力尝试付诸东流。github

所以,控制住原理分析的粒度,在宏观的角度,系统地了解输入系统自己的设计理念,并引伸到实际开发中的ANR现象的原理和解决思路 ,是一个很是不错的理论与实践相结合的学习方式,这也正是笔者写做本文的初衷。面试

本文篇幅较长,思惟导图以下:安全

1、自顶向下探索

谈到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很是重要,其负责管理IMSWindowActivityManager之间的通讯,这里点到为止,后文再进行补充,咱们先来看IMS

顾名思义,IMS服务的做用就是负责输入模块在Java层级的初始化,并经过JNI调用,在Native层进行更下层输入子系统相关功能的建立和预处理。

JNI的调用过程当中,IMS建立了NativeInputManager实例,NativeInputManager则在初始化流程中又建立了EventHubInputManager:

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层级相通讯,能够说是很是重要。而在该层级中,EventHubInputManager又是最核心的两个角色。

这两个角色的职责又是什么呢?首先来讲EventHub,它是底层 输入子系统 中的核心类,负责从物理输入设备中不断读取事件(Event),而后交给InputManager,后者内部封装了InputReaderInputDispatcher,用来从EventHub中读取事件和分发事件:

InputManager::InputManager(...) {
    mDispatcher = new InputDispatcher(dispatcherPolicy);
    mReader = new InputReader(eventHub, readerPolicy, mDispatcher);
    initialize();
}
复制代码

简单来看,EventHub创建了Linux与输入设备之间的通讯,InputManager中的InputReaderInputDispatcher负责了输入事件的读取和分发,在 输入系统 中,二者的确很是重要。

这里借用网上的图对此进行一个简单的归纳:

2、EventHub 与 epoll 机制

对于EventHub的具体实现,绝大多数App开发者也许并不须要去花太多时间深刻——简单了解其职责,而后一笔带过彷佛是笔划算的买卖。

可是在EventHub的实现细节中笔者发现,其对epoll机制的利用是一个很是经典的学习案例,所以,花时间稍微深刻了解也绝对是一箭双雕。

上文说到,EventHub创建了Linux与输入设备之间的通讯,其实这种描述是不许确的,那么,EventHub是为了解决什么问题而设计的呢,其具体又是如何实现的?

一、多输入设备与输入子系统

咱们知道,Android设备能够同时链接多个输入设备,好比 屏幕键盘鼠标 等等,用户在任意设备上的输入都会产生一个中断,经由Linux内核的中断处理及设备驱动转换成一个Event,最终交给用户空间的应用程序进行处理。

Linux内核提供了一个便于将不一样设备不一样数据接口统一转换的抽象层,只要底层输入设备驱动程序按照这层抽象接口实现,应用就能够经过统一接口访问全部输入设备,这即是Linux内核的 输入子系统

那么 输入子系统 如何是针对接收到的Event进行的处理呢?这就不得不提到EventHub了,它是底层Event处理的枢纽,其利用了epoll机制,不断接收到输入事件Event,而后将其向上层的InputReader传递。

二、什么是epoll机制

这是常见于面试Handler相关知识点时的一道进阶题,变种问法是:「既然Handler中的Looper中经过一个死循环不断轮询,为何程序没有由于无限死循环致使崩溃或者ANR?」

读者应该知道,Handler简单的利用了epoll机制,作到了消息队列的阻塞和唤醒。关于epoll机制,这里有一篇很是经典的解释,不了解其设计理念的读者 有必要 了解一下:

知乎:epoll或者kqueue的原理是什么?

参考上文,这里咱们对epoll机制进行一个简单的总结:

epoll能够理解为event poll,不一样于忙轮询和无差异轮询,在 多个输入流 的状况下,epoll只会把哪一个流发生了怎样的I/O事件通知咱们。此时咱们对这些流的操做都是有意义的。

EventHub中使用epoll的恰到好处——多个物理输入设备对应了多个不一样的输入流,经过epoll机制,在EventHub初始化时,分别建立mEpollFdmINotifyFd;前者用于监听设备节点是否有设备文件的增删,后者用于监听是否有可读事件,建立管道,让InputReader来读取事件:

3、事件的读取和分发

本章节将对InputReaderInputDispatcher进行系统性的介绍。

一、InputReader:读取事件

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中选择打开某些功能,以 手势识别 为例,AndroidAccessbilityManagerService(辅助功能服务) 可能会根据须要转换成新的Event,好比说两根手指头捏动的手势最终会变成ZoomEvent

须要注意的是,这里的拦截处理并不会真正将事件 消费 掉,而是经过特殊的方式将事件进行标记(policyFlags),而后在InputDispatcher中处理。

至此,InputReader输入事件 完整的一轮处理到此结束,这以后,InputReader又进入了新一轮等待。

三、InputDispatcher:分发事件

wake()函数将在Looper中睡眠等待的InputDispatcher唤醒时,InputDispatcher开始新一轮事件的分发。

准确来讲,InputDispatcher被唤醒时,wake()函数实际是在InputManagerService的线程中执行的,即整个流程的线程切换顺序为InputReaderThread -> InputManagerServiceThread -> InputDispatcherThread

InputDispatcher的线程负责将接收到的 输入事件 分发给 目标应用窗口,在这个过程当中,InputDispatcher首先须要对上个环节中标记了须要拦截的 系统按键 相关事件进行拦截,被拦截的事件至此再也不向下分发。

这以后,InputDispatcher进入了本文最关键的一个环节——调用 findFocusedWindowTargetLocked()获取当前的 焦点窗口 ,同时检测目标应用是否有ANR发生。

若是检测到目标窗口处于正常状态,即ANR并未发生时,InputDispatcher进入真正的分发程序,将事件对象进行新一轮的封装,经过SocketPair唤醒目标窗口所在进程的Looper线程,即咱们应用进程中的主线程,后者会读取相应的键值并进行处理。

表面来看,整个分发流程彷佛干净简洁且便于理解,但实际上InputDispatcher整个流程的逻辑十分复杂,试想一次事件分发要横跨3个线程的流程又怎会简单?

此外,InputDispatcher还负责了 ANR 的处理,这又致使整个流程的复杂度又上升了一个层级,这个流程咱们在后文的ANR章节中进行更细致的分析,所以先按住不提。

接下来,咱们来看看整个 输入事件 的分发流程中, 应用进程 是如何与 系统进程 创建相应的通讯连接的。

四、经过Socket创建通讯

关于 跨进程通讯的创建 这一节,笔者最初打算做为一个大的章节来说,可是对于整个 输入系统 而言,其彷佛又只是一个 重要非必需 的知识点。最终,笔者将其放在一个小节中进行简单的描述,有兴趣的读者能够在文末的参考连接中查阅更详尽的资料。

咱们知道,InputReaderInputDispatcher运行在system_server 系统进程 中,而用户操做的应用都运行在本身的 应用进程 中;这里就涉及到跨进程通讯,那么 应用进程 是如何与 系统进程 创建通讯的呢?

让咱们回到文章最初WindowManagerService(WMS)InputManagerService(IMS)初始化的流程中来,当IMS以及其余的系统服务初始化完成以后,应用程序开始启动。

若是一个应用程序有Activity(只有Activity可以接受用户输入),那么它要将本身的Window注册到WMS中。

在这里,Android使用了Socket而不是Binder来完成。WMS中经过OpenInputChannelPair生成了两个SocketFD, 表明一个双向通道的两端:向一端写入数据,另一端即可以读出;反之,若是一端没有写入数据,另一端去读,则陷入阻塞等待。

最终InputDispatcher中创建了目标应用的Connection对象,表明与远端应用的窗口创建了连接;一样,应用进程中的ViewRootImpl建立了WindowInputEventReceiver用于接受InputDispatchor传过来的事件:

这里咱们对该次 跨进程通讯创建流程 有了初步的认知,对于Android系统而言,Binder是最普遍的跨进程通讯的应用方式,可是Android系中跨进程通讯就仅仅只用到了Binder吗?答案是否认的,至少在 输入系统 中,除了Binder以外,Socket一样起到了举足轻重的做用。

那么新的问题就来了,这里为何选择Socket而不是选择Binder呢,关于这个问题的解释,笔者找到了一个很好的版本:

Socket能够实现异步的通知,且只须要两个线程参与(Pipe两端各一个),假设系统有N个应用程序,跟输入处理相关的线程数目是 N+1 (1Input Dispatcher线程)。然而,若是用Binder实现的话,为了实现异步接收,每一个应用程序须要两个线程,一个Binder线程,一个后台处理线程(不能在Binder线程里处理输入,由于这样太耗时,将会堵塞住发送端的调用线程)。在发送端,一样须要两个线程,一个发送线程,一个接收线程来接收应用的完成通知,因此,N个应用程序须要 2(N+1)个线程。相比之下,Socket仍是高效多了。

如今,应用进程 可以收到由InputDispatcher处理完成并分发过来的 输入事件 了。至此,咱们来到了最熟悉的应用层级事件分发流程。对于这以后 应用层级的事件分发,能够阅读下述笔者的另外两篇文章,本文不赘述。

4、ANR机制的设计与实现

输入系统 有了更初步总体的认知以后,接下来本文将针对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_serverActivityManager线程中引爆炸弹,这就是组件类ANR机制的原理:

接下来简单了解一下 输入系统 流程中ANR机制的原理。

二、第二类原理概述

Input类型的ANR在平常开发中更为常见且更复杂,好比用户或者测试反馈,点击屏幕中的UI元素致使「卡死」。

少数状况下开发者可以很快定位到问题,但更常见的状况是,该问题是 随机难以复现 的,致使该问题的缘由也更具备综合性,好比低端设备的系统自己资源已很是紧张,或者多线程相互持有彼此须要的资源致使 死锁 ,亦或其它复杂的状况,所以处理这类型问题就须要开发者对 输入系统 中的ANR机制有必定的了解。

与组件类ANR不一样的是,Input类型的超时机制并不是时间到了必定就会爆炸,而是处理后续上报事件的过程才会去检测是否该爆炸,因此更像是 扫雷 的过程。

什么叫作 扫雷 呢,对于 输入系统 而言,即便某次事件执行时间超过预期的时长,只要用户后续没有再生成输入事件,那么也不须要ANR

而只有当新一轮的输入事件到来,此时正在分发事件的窗口(即App应用自己)迟迟没法释放资源给新的事件去分发,这时InputDispatcher才会根据超时时间,动态的判断是否须要向对应的窗口提示ANR信息。

这也正是用户在第一次点击屏幕,即便事件处理超时,也没有弹出ANR窗口,而当用户下意识再次点击屏幕时,屏幕上才提示出了ANR信息的缘由。

因而可知,组件类ANRInput ANR原理上确实有所不一样;除此以外,前者是在ActivityManager线程中处理的ANR信息,后者则是在InputDispatcher线程中处理的ANR,这里经过一张图简单了解一下后者的总体流程:

如今咱们对Input类型的ANR机制有了一个简单的了解,下文将针对其更深刻性的细节实现进行探讨。

三、事件分发的异步机制

咱们再次将目光转回到InputDispatcher的实现细节。

先抛出一个新的问题,对处于system_server进程Native层级的 事件分发 而言,其向下与 应用进程 的通讯的过程应该是同步仍是异步的?

对于读者而言,不可贵出答案是异步的,由于二者之间双向通讯的创建是经过SocketPair,而且,由于system_serverInputDispatcher对事件的分发其实是一对多的,若是是同步的,那么一旦其中一个应用分发超时,那么InputDispatcher线程天然被卡住,其永远都不可能进入到下一轮的事件分发中,扫雷 机制更是无从谈起。

所以,与应用进程中事件分发不一样的是,后者咱们一般能够认为是在主线程中同步的,而对于整个 输入系统 而言,由于涉及到 系统进程 与多个 应用进程 之间异步的通讯,所以其内部的实现更为复杂。

由于事件分发涉及到异步回调机制,所以InputDispatcher须要对事件进行维护和管理,那么问题就变成了,使用什么样的数据结构去维护这些输入事件比较合适。

四、三个队列

InputDispatcher的源码实现中,总体的事件分发流程共使用到3个事件队列:

  • mInBoundQueue:用于记录InputReader发送过来的输入事件;
  • outBoundQueue:用于记录即将分发给目标应用窗口的输入事件;
  • waitQueue:用于记录已分发给目标应用,且应用还没有处理完成的输入事件。

下文,笔者经过2轮事件分发的示例,对三个队列的做用进行简单的梳理。

4.1 第一轮事件分发

首先InputReader线程经过EventHub监听到底层的输入事件上报,并将其放入了mInBoundQueue中,同时唤醒了InputDispatcher线程。

而后InputDispatcher开始了第一轮的事件分发,此时并无正在处理的事件,所以InputDispatchermInBoundQueue队列头部取出事件,并重置ANR的计时,并检查窗口是否就绪,此时窗口准备就绪,将该事件转移到了outBoundQueue队列中,由于应用管道对端链接正常,所以事件从outBoundQueue取出,而后放入了waitQueue队列,由于Socket双向通讯已经创建,接下来就是 应用进程 接收到新的事件,而后对其进行分发。

若是 应用进程 事件分发正常,那么会经过Socketsystem_server通知完成,则对应的事件最终会从waitQueue队列中移除。

4.2 第二轮事件分发

若是第一轮事件分发还没有接收到回调通知,第二轮事件分发抵达又是如何处理的呢?

第二轮事件到达InputDispatcher时,此时InputDispatcher发现有事件正在处理,所以不会从mInBoundQueue取出新的事件,而是直接检查窗口是否就绪,若未就绪,则进入ANR检测状态。

如下几种状况会致使进入ANR检测状态:

一、目标应用不会空,而目标窗口为空。说明应用程序在启动过程当中出现了问题; 二、目标Activity的状态是Pause,即再也不是Focused的应用; 三、目标窗口还在处理上一个事件。

读者须要理解,并不是全部「目标窗口还在处理上一个事件」都会抛出ANR,而是须要经过检测时间,若是未超时,那么直接停止本轮事件分发,反之,若是事件分发超时,那么才会肯定ANR的发生。

这也正是将Input类型的ANR描述为 扫雷 的缘由:这里的扫雷是指当前输入系统中正在处理着某个耗时事件的前提下,后续的每一次input事件都会检测前一个正在处理的事件是否超时(进入扫雷状态),检测当前的时间距离上次输入事件分发时间点是否超时。若是前一个输入事件,则会重置ANRtimeout,从而不会爆炸。

至此,输入系统 检测到了ANR的发生,并向上层抛出了本次ANR的相关信息。

小结

本文旨在对Android 输入系统 进行一个系统性的概述,读者不该将本文做为惟一的学习资料,而应该经过本文对该知识体系进行初步的了解,并根据自身要求进行单个方向细节性的突破。而已经掌握了骨骼架构的读者而言,更细节性的知识点也不过是待丰富的血肉而已。

本文从立题至发布,整个流程耗时近1个半月,在这个过程当中,笔者参考了较本文内容数十倍的资料,受益颇深,也深感以 举重若轻 为写文目标之艰难——内容铺展容易,但经过 简洁连贯 的语言来对一个庞大复杂的知识体系进行收拢,须要极强的 克制力 ,在这种严苛的要求下,每一句的描述都须要极高的 精确性 ,这对笔者而言是一个挑战,但真正完成以后,对整个知识体系的理解程度一样也是极高的。

而这也正是 反思 系列的初衷,但愿你能喜欢。

参考 & 扩展阅读

正如上文所言,输入系统ANR 自己都是一个很是大的命题,除了宽广的知识体系,还须要亲身去实践和总结,下文列出若干相关参考资料,读者可根据自身需求选择性进行扩展阅读:

一、完全理解安卓应用无响应机制 @Gityuan
二、Input系统—ANR原理分析 @Gityuan
三、理解Android ANR的触发原理 @Gityuan

深刻学习ANR机制资料,GityuanANR博客系列绝对是先驱级别的,尤为是第1篇文章中,其对于 定时炸弹扫雷 的形容,贴切且易理解,这种 举重若轻 的写做风格体现了做者自己对整个知识体系的深度掌握;然后两篇文章则针对两种类型的ANR分别进行了源码级别的分析,很是下饭。

四、图解Android-Android的 Event Input System @漫天尘沙

笔者曾经想写一个 图解Android 系列,后来由于种种缘由放弃了,没想到若干年前已经有先驱进行过了这样的尝试,而且,内容质量极高。笔者相信,可以花费很是大精力总结的文章必定不会被埋没,而这篇文章,注定会成为经典中的经典。

五、Android Input系列 @Stan_Z

一个笔者最近关注很是优秀的做者,文章很是具备深度,其Input系列针对整个输入系统进行了更细致源码级别的分析,很是值得收藏。

六、Android 信号处理面面观 之 信号定义、行为和来源 @rambo2188

若是读者对「Android系统信号处理的行为」感兴趣,那么这篇文章绝对不能错过。

七、Android开发高手课 @张绍文

实战中的经典之做,该课程每一小结都极具深度,价值不可估量。因或涉及到利益相关,并且推荐了也从张老师那里拿不到钱,所以本文不加连接并放在最下面(笑)。


关于我

Hello,我是 却把清梅嗅 ,若是您以为文章对您有价值,欢迎 ❤️,也欢迎关注个人 博客 或者 GitHub

若是您以为文章还差了那么点东西,也请经过 关注 督促我写出更好的文章——万一哪天我进步了呢?

相关文章
相关标签/搜索