经验总结---Android事件分发机制及设计思路

如图所示,蓝色箭头描述的流程才是UI层级的事件分发。

为了方便理解,本文使用了以下两个词汇:应用整体的事件分发UI层级的事件分发 ——需要重申的是,这两个词汇虽然被分开讲解,但其本质仍然属于一个完整 事件分发的责任链,后者只是前者的一小部分而已。

架构设计

1.InputEvent:输入事件分类概述

Android系统中将输入事件定义为InputEvent,而InputEvent根据输入事件的类型又分为了KeyEventMotionEvent

// 输入事件的基类
public abstract class InputEvent implements Parcelable { }

public class KeyEvent extends InputEvent implements Parcelable { }

public final class MotionEvent extends InputEvent implements Parcelable { }
复制代码

KeyEvent对应了键盘的输入事件,那么什么是MotionEvent?顾名思义,MotionEvent就是移动事件,鼠标、笔、手指、轨迹球等相关输入设备的事件都属于MotionEvent,本文我们简单地将其视为 屏幕触摸事件

用户的输入种类繁多,由此可见,Android输入系统的设计中,将 输入事件 抽象为InputEvent是有必要的。

2.InputManager:系统输入管理器

Android系统的设计中,InputEvent统一由系统输入管理器InputManager进行分发。在这里InputManagernative层级的一个类,负责与硬件通信并接收输入事件。

那么InputManager是如何初始化的呢?这里就要涉及到Java层级的SystemServer了,我们知道SystemServer进程中包含着各种各样的系统服务,比如ActivityManagerServiceWindowManagerService等等,SystemServerzygote进程启动, 启动过程中对WindowManagerServiceInputManagerService进行了初始化:

public final class SystemServer {

private void startOtherServices() {
// 初始化 InputManagerService
InputManagerService inputManager = new InputManagerService(context);
// WindowManagerService 持有了 InputManagerService
WindowManagerService wm = WindowManagerService.main(context, inputManager,…);

inputManager.setWindowManagerCallbacks(wm.getInputMonitor());
inputManager.start();
}
}
复制代码

InputManagerService的构造器中,通过调用native函数,通知native层级初始化InputManager:

public class InputManagerService extends IInputManager.Stub {

public InputManagerService(Context context) {
// …通知native层初始化 InputManager
mPtr = nativeInit(this, mContext, mHandler.getLooper().getQueue());
}

// native 函数
private static native long nativeInit(InputManagerService service, Context context, MessageQueue messageQueue);
}
复制代码

SystemServer会启动窗口管理服务WindowManagerServiceWindowManagerService在启动的时候就会通过InputManagerService启动系统输入管理器InputManager来负责监控键盘消息。

对于本文而言,framework层级相关如WindowManagerService(窗口管理服务)、native层级的源码、SystemServer 亦或者 Binder跨进程通信并非重点,读者仅需了解 系统服务的启动流程层级关系 即可,参考下图:

3.ViewRootImpl:窗口服务与窗口的纽带

InputManager将事件分发给当前激活的窗口(Window)处理,这里我们将前者理解为系统层级的 (窗口)服务,将后者理解为应用层级的 窗口, 因此需要有一个中介负责 服务窗口 之间的通信,于是ViewRootImpl类应运而生。

ViewRootImpl作为链接WindowManagerDecorView的纽带,同时实现了ViewParent接口,ViewRootImpl作为整个控件树的根部,它是View树正常运作的动力所在,控件的测量、布局、绘制以及输入事件的分发都由ViewRootImpl控制。

那么ViewRootImpl是如何被创建和初始化的,而 (窗口)服务窗口 之间的通信又是如何建立的呢?

建立通信

1.ViewRootImpl的创建

既然Android系统将 (窗口)服务窗口 的通信建立交给了ViewRootImpl,那么ViewRootImpl必然持有了两者的依赖,因此了解ViewRootImpl是如何创建的就非常重要。

我们知道,ActivityThread负责控制Activity的启动过程,在ActivityThread.performLaunchActivity()流程中,ActivityThread会针对Activity创建对应的PhoneWindowDecorView实例,而在ActivityThread.handleResumeActivity()流程中,ActivityThread会将获取当前ActivityWindowManager,并将DecorViewWindowManager.LayoutParams(布局参数)作为参数调用addView()函数:

// 伪代码
public final class ActivityThread {

@Override
public void handleResumeActivity(…){
//…
windowManager.addView(decorView, windowManagerLayoutParams);
}
}
复制代码

WindowManager.addView()实际上就是对ViewRootImpl进行了初始化,并执行了setView()函数:

// 1.WindowManager 的本质实际上是 WindowManagerImpl
public final class WindowManagerImpl implements WindowManager {

@Override
public void addView(@NonNull View view, @NonNull ViewGroup.LayoutParams params) {
// 2.实际上调用了 WindowManagerGlobal.addView()
WindowManagerGlobal.getInstance().addView(…);
}
}

public final class WindowManagerGlobal {

public void addView(…) {
// 3.初始化 ViewRootImpl,并执行setView()函数
ViewRootImpl root = new ViewRootImpl(view.getContext(), display);
root.setView(view, wparams, panelParentView);
}
}

public final class ViewRootImpl {

public void setView(View view, WindowManager.LayoutParams attrs, View panelParentView) {
// 4.该函数就是控测量(measure)、布局(layout)、绘制(draw)的开始
requestLayout();
// …
// 5.此外还有通过Binder建立通信,这个下文再提
}
}
复制代码

Android系统的Window机制并非本文重点,读者可简单理解为ActivityThread.handleResumeActivity()流程中最终创建了ViewRootImpl,并通过setView()函数对DecorView开始了绘制流程的三个步骤。

2.通信的建立

完成了ViewRootImpl的创建之后,如何完成系统输入服务和应用程序进程的连接呢?

AndroidWindowInputManagerService之间的通信实际上使用的InputChannel,InputChannel是一个pipe,底层实际是通过socket进行通信。在ViewRootImpl.setView()过程中,也会同时注册InputChannel

public final class InputChannel implements Parcelable { }
复制代码

上文中,我们提到了ViewRootImpl.setView()函数,在该函数的执行过程中,会在ViewRootImpl中创建InputChannelInputChannel实现了Parcelable, 所以它可以通过Binder传输。具体是通过addDisplay()将当前window加入到WindowManagerService中管理:

public final class ViewRootImpl {

public void setView(View view, WindowManager.LayoutParams attrs, View panelParentView) {
requestLayout();
// …
// 创建InputChannel
mInputChannel = new InputChannel();
// 通过Binder在SystemServer进程中完成InputChannel的注册
mWindowSession.addToDisplay(mWindow, mSeq, mWindowAttributes,
getHostVisibility(), mDisplay.getDisplayId(),
mAttachInfo.mContentInsets, mAttachInfo.mStableInsets,
mAttachInfo.mOutsets, mInputChannel);
}
}
复制代码

这里涉及到了WindowManagerServiceBinder跨进程通信,读者不需要纠结于详细的细节,只需了解最终在SystemServer进程中,WindowManagerService根据当前的Window创建了SocketPair用于跨进程通信,同时并对App进程中传过来的InputChannel进行了注册,这之后,ViewRootImpl里的InputChannel就指向了正确的InputChannel, 作为Client端,其fdSystemServer进程中Server端的fd组成SocketPair, 它们就可以双向通信了。

对该流程感兴趣的读者可以参考 这篇文章

应用整体的事件分发

App端与服务端建立了双向通信之后,InputManager就能够将产生的输入事件从底层硬件分发过来,Android提供了InputEventReceiver类,以接收分发这些消息:

public abstract class InputEventReceiver {
// Called from native code.
private void dispatchInputEvent(int seq, InputEvent event, int displayId) {
// …
}
}
复制代码

InputEventReceiver是一个抽象类,其默认的实现是将接收到的输入事件直接消费掉,因此真正的实现是ViewRootImpl.WindowInputEventReceiver类:

public final class ViewRootImpl {

final class WindowInputEventReceiver extends InputEventReceiver {
@Override
public void onInputEvent(InputEvent event, int displayId) {
// 将输入事件加入队列
enqueueInputEvent(event, this, 0, true);
}
}
}
复制代码

输入事件加入队列之后,接下来就是对事件的分发了,设计者在这里使用了经典的 责任链 模式:对于一个输入事件的分发而言,必然有其对应的消费者,在这个过程中为了使多个对象都有处理请求的机会,从而避免了请求的发送者和接收者之间的耦合关系。将这些对象串成一条链,并沿着这条链一直传递该请求,直到有对象处理它为止。

InputStage

因此,设计者针对事件分发的整个责任链设计了InputStage类作为基类,作为责任链中的模版,并实现了若干个子类,为输入事件按顺序分阶段进行分发处理:

// 事件分发不同阶段的基类
abstract class InputStage {
private final InputStage mNext; // 指向事件分发的下一阶段
}

// InputStage的子类,象征事件分发的各个阶段

final class ViewPreImeInputStage extends InputStage {}

final class EarlyPostImeInputStage extends InputStage {}

final class ViewPostImeInputStage extends InputStage {}

final class SyntheticInputStage extends InputStage {}

abstract class AsyncInputStage extends InputStage {}

final class NativePreImeInputStage extends AsyncInputStage {}

final class ImeInputStage extends AsyncInputStage {}

final class NativePostImeInputStage extends AsyncInputStage {}
复制代码

输入事件整体的分发阶段十分复杂,比如当事件分发至SyntheticInputStage阶段,该阶段为 综合性处理阶段 ,主要针对轨迹球、操作杆、导航面板及未捕获的事件使用键盘进行处理:

final class SyntheticInputStage extends InputStage {
@Override
protected int onProcess(QueuedInputEvent q) {
// 轨迹球
if (…) {
mTrackball.process(event);
return FINISH_HANDLED;
} else if (…) {
// 操作杆
mJoystick.process(event);
return FINISH_HANDLED;
} else if (…) {
// 导航面板
mTouchNavigation.process(event);
return FINISH_HANDLED;
}
// 继续转发事件
return FORWARD;
}
}
复制代码

比如当事件分发至ImeInputStage阶段,即 输入法事件处理阶段 ,会从事件中过滤出用户输入的字符,如果输入的内容无法被识别,则将输入事件向下一个阶段继续分发:

final class ImeInputStage extends AsyncInputStage {

@Override
protected int onProcess(QueuedInputEvent q) {
if (mLastWasImTarget && !isInLocalFocusMode()) {
// 获取输入法Manager
InputMethodManager imm = InputMethodManager.peekInstance();
final InputEvent event = q.mEvent;
// imm对事件进行分发
int result = imm.dispatchInputEvent(event, q, this, mHandler);
if (result == …) {
// imm消费了该输入事件
return FINISH_HANDLED;
} else {
return FORWARD; // 向下转发
}
}
return FORWARD; // 向下转发
}
}
复制代码

当然还有最熟悉的ViewPostImeInputStage,即 视图输入处理阶段 ,主要处理按键、轨迹球、手指触摸及一般性的运动事件,触摸事件的分发对象是View,这也正是我们熟悉的 UI层级的事件分发 流程的起点:

final class ViewPostImeInputStage extends InputStage {

private int processPointerEvent(QueuedInputEvent q) {
// 让顶层的View开始事件分发
final MotionEvent event = (MotionEvent)q.mEvent;
boolean handled = mView.dispatchPointerEvent(event);
//…
}
}
复制代码

读到这里读者应该理解了, UI层级的事件分发只是完整事件分发流程的一部分,当输入事件(即使是MotionEvent)并没有分发到ViewPostImeInputStage(比如在 综合性处理阶段 就被消费了),那么View层的事件分发自然无从谈起,这里再将整体的流程图进行展示以方便理解:

组装责任链

现在我们理解了,新分发的事件会通过一个InputStage的责任链进行整体的事件分发,这意味着,当新的事件到来时,责任链已经组装好了,那么这个责任链是何时进行组装的?

不难得出,对于责任链的组装,最好是在系统服务和Window建立通信成功的时候,而上文中也提到了,通信的建立是执行在ViewRootImpl.setView()方法中的,因此在InputChannel注册成功之后,即可对责任链进行组装:

public final class ViewRootImpl implements ViewParent {

public void setView(View view, WindowManager.LayoutParams attrs, View panelParentView) {
// …
// 1.开始根布局的绘制流程
requestLayout();
// 2.通过Binder建立双端的通信
res = mWindowSession.addToDisplay(…)
mInputEventReceiver = new WindowInputEventReceiver(mInputChannel, Looper.myLooper());
// 3.对责任链进行组装
mSyntheticInputStage = new SyntheticInputStage();
InputStage viewPostImeStage = new ViewPostImeInputStage(mSyntheticInputStage);
InputStage nativePostImeStage = new NativePostImeInputStage(viewPostImeStage,
“aq:native-post-ime:” + counterSuffix);
InputStage earlyPostImeStage = new EarlyPostImeInputStage(nativePostImeStage);
InputStage imeStage = new ImeInputStage(earlyPostImeStage,
“aq:ime:” + counterSuffix);
InputStage viewPreImeStage = new ViewPreImeInputStage(imeStage);
InputStage nativePreImeStage = new NativePreImeInputStage(viewPreImeStage,
“aq:native-pre-ime:” + counterSuffix);
mFirstInputStage = nativePreImeStage;
mFirstPostImeInputStage = earlyPostImeStage;
// …
}
}
复制代码

这说明ViewRootImpl.setView()函数非常重要,该函数也正是ViewRootImpl本身职责的体现:

  • 1.链接WindowManagerDecorView的纽带,更广一点可以说是WindowView之间的纽带;
  • 2.完成View的绘制过程,包括measure、layout、draw过程;
  • 3.向DecorView分发收到的用户发起的InputEvent事件。

最终整体事件分发流程由如下责任链构成:

SyntheticInputStage --> ViewPostImeStage --> NativePostImeStage --> EarlyPostImeStage --> ImeInputStage --> ViewPreImeInputStage --> NativePreImeInputStage

事件分发结果的返回

上文说到,真正从Native层的InputManager接收输入事件的是ViewRootImplWindowInputEventReceiver对象,既然负责输入事件的分发,自然也负责将事件分发的结果反馈给Native层,作为事件分发的结束:

public final class ViewRootImpl {

final class WindowInputEventReceiver extends InputEventReceiver {
@Override
public void onInputEvent(InputEvent event, int displayId) {
// 【开始】将输入事件加入队列,开始事件分发
enqueueInputEvent(event, this, 0, true);
}
}
}

// ViewRootImpl.WindowInputEventReceiver 是其子类,因此也持有finishInputEvent函数
public abstract class InputEventReceiver {
private static native void nativeFinishInputEvent(long receiverPtr, int seq, boolean handled);

public final void finishInputEvent(InputEvent event, boolean handled) {
//…
// 【结束】调用native层函数,结束应用层的本次事件分发
nativeFinishInputEvent(mReceiverPtr, seq, handled);
}
}
复制代码

ViewPostImeInputStage:UI层事件分发的起点

上文已经提到,UI层级的事件分发 作为 完整事件分发流程的一部分,发生在ViewPostImeInputStage.processPointerEvent函数中:

final class ViewPostImeInputStage extends InputStage {

private int processPointerEvent(QueuedInputEvent q) {
// 让顶层的View开始事件分发
final MotionEvent event = (MotionEvent)q.mEvent;
boolean handled = mView.dispatchPointerEvent(event);
//…
}
}
复制代码

这个顶层的View其实就是DecorView(参见上文 建立通信-ViewRootImpl的创建 小节),读者知道,DecorView实际上就是ActivityWindow的根布局,它是一个FrameLayout

现在DecorView执行了dispatchPointerEvent(event)函数,这是不是就意味着开始了View的事件分发?

DecorView的双重职责

DecorView作为View树的根节点,接收到屏幕触摸事件MotionEvent时,应该通过递归的方式将事件分发给子View,这似乎理所当然。但实际设计中,设计者将DecorView接收到的事件首先分发给了ActivityActivity又将事件分发给了其Window,最终Window才将事件又交回给了DecorView,形成了一个小的循环:

// 伪代码
public class DecorView extends FrameLayout {

// 1.将事件分发给Activity
@Override
public boolean dispatchTouchEvent(MotionEvent ev) {
return window.getActivity().dispatchTouchEvent(ev)
}

// 4.执行ViewGroup 的 dispatchTouchEvent
public boolean superDispatchTouchEvent(MotionEvent event) {
return super.dispatchTouchEvent(event);
}
}

// 2.将事件分发给Window
public class Activity {
public boolean dispatchTouchEvent(MotionEvent ev) {
return getWindow().superDispatchTouchEvent(ev);
}
}

// 3.将事件再次分发给DecorView
public class PhoneWindow extends Window {
@Override
public boolean superDispatchTouchEvent(MotionEvent event) {
return mDecor.superDispatchTouchEvent(event);
}
}
复制代码

事件绕了一个圈子最终回到了DecorView这里,对于初次阅读这段源码的读者来说,这里的设计平淡无奇,似乎说它莫名其妙也不过分。事实上这里是 面向对象程序设计 中灵活运用 多态 这一特征的有力体现——对于DecorView而言,它承担了2个职责:

  • 1.在接收到输入事件时,DecorView不同于其它View,它需要先将事件转发给最外层的Activity,使得开发者可以通过重写Activity.onTouchEvent()函数以达到对当前屏幕触摸事件拦截控制的目的,这里DecorView履行了自身(根节点)特殊的职责;
  • 2.从Window接收到事件时,作为View树的根节点,将事件分发给子View,这里DecorView履行了一个普通的View的职责。

实际上,不只是DecorView,接下来View层级的事件分发中也运用到了这个技巧,对于ViewGroup的事件分发来说,其本质是递归思想的体现,在 递流程 中,其本身被视为上游的ViewGroup,需要自定义dispatchTouchEvent()函数,并调用child.dispatchTouchEvent(event)将事件分发给下游的子View;同时,在 归流程 中,其本身被视为一个View,需要调用View自身的方法已决定是否消费该事件(super.dispatchTouchEvent(event)),并将结果返回上游,直至回归到View树的根节点,至此整个UI树事件分发流程结束。

同时,读者应该也已理解,平时所说View层级的事件分发也只是 UI层的事件分发 的一个环节,而 UI层的事件分发 又只是 应用层完整事件分发 的一个小环节,更遑论后者本身又是Native层和应用层之间的事件分发机制的一部分了。

UI层级事件分发

虽然View层级之间的事件分发只是 UI层级事件分发 的一个环节,但却是最重要的一个环节,也是本文的重点,上文所有内容都是为本节做系统性的铺垫 ——为了方便阅读,本小节接下来的内容中,事件分发 统一泛指 View层级的事件分发

1.核心思想

了解 事件分发 的代码流程细节,首先需要了解整个流程的最终目的,那就是 获知事件是否被消费 ,至于事件被哪个角色消费了,怎么被消费的,在外层责任链中的ViewPostImeInputStage不关心,其更上层ViewRootImpl.WindowInputEventReceiver不关心,native层级的InputManager自然更不会关心了。

因此,设计者设计出了这样一个函数:

// 对事件进行分发
public boolean dispatchTouchEvent(MotionEvent event);
复制代码

对于事件分发结果的接收者而言,其只关心事件是否被消费,因此返回值被定义为了boolean类型:当返回值为true,事件被消费,反之则事件未被消费。

上文中我们同样提到了,在ViewGroup的事件分发过程中,其本身的dispatchTouchEvent(event)super.dispatchTouchEvent(event)完全是两个完全不同的函数,前者履行的是ViewGroup的职责,负责将事件分发给子View;后者履行的是View的职责,负责处理决定事件是否被消费(参见 应用整体的事件分发-DecorView的双重职责 小节)。

自我介绍一下,小编13年上海交大毕业,曾经在小公司待过,也去过华为、OPPO等大厂,18年进入阿里一直到现在。

深知大多数初中级Android工程师,想要提升技能,往往是自己摸索成长或者是报班学习,但对于培训机构动则近万的学费,着实压力不小。自己不成体系的自学效果低效又漫长,而且极易碰到天花板技术停滞不前!

因此收集整理了一份《2024年Android移动开发全套学习资料》,初衷也很简单,就是希望能够帮助到想自学提升又不知道该从何学起的朋友,同时减轻大家的负担。

img

img

img

img

既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,基本涵盖了95%以上Android开发知识点,真正体系化!

由于文件比较大,这里只是将部分目录截图出来,每个节点里面都包含大厂面经、学习笔记、源码讲义、实战项目、讲解视频,并且会持续更新!

如果你觉得这些内容对你有帮助,可以扫码获取!!(备注:Android)

学习福利

【Android 详细知识点思维脑图(技能树)】

其实Android开发的知识点就那么多,面试问来问去还是那么点东西。所以面试没有其他的诀窍,只看你对这些知识点准备的充分程度。so,出去面试时先看看自己复习到了哪个阶段就好。

虽然 Android 没有前几年火热了,已经过去了会四大组件就能找到高薪职位的时代了。这只能说明 Android 中级以下的岗位饱和了,现在高级工程师还是比较缺少的,很多高级职位给的薪资真的特别高(钱多也不一定能找到合适的),所以努力让自己成为高级工程师才是最重要的。

这里附上上述的面试题相关的几十套字节跳动,京东,小米,腾讯、头条、阿里、美团等公司19年的面试题。把技术点整理成了视频和PDF(实际上比预期多花了不少精力),包含知识脉络 + 诸多细节。

由于篇幅有限,这里以图片的形式给大家展示一小部分。

网上学习 Android的资料一大堆,但如果学到的知识不成体系,遇到问题时只是浅尝辄止,不再深入研究,那么很难做到真正的技术提升。希望这份系统化的技术体系对大家有一个方向参考。

《Android学习笔记总结+移动架构视频+大厂面试真题+项目实战源码》,点击传送门即可获取!

复习到了哪个阶段就好。

虽然 Android 没有前几年火热了,已经过去了会四大组件就能找到高薪职位的时代了。这只能说明 Android 中级以下的岗位饱和了,现在高级工程师还是比较缺少的,很多高级职位给的薪资真的特别高(钱多也不一定能找到合适的),所以努力让自己成为高级工程师才是最重要的。

这里附上上述的面试题相关的几十套字节跳动,京东,小米,腾讯、头条、阿里、美团等公司19年的面试题。把技术点整理成了视频和PDF(实际上比预期多花了不少精力),包含知识脉络 + 诸多细节。

由于篇幅有限,这里以图片的形式给大家展示一小部分。

[外链图片转存中…(img-0BzUjgdS-1712024804969)]

网上学习 Android的资料一大堆,但如果学到的知识不成体系,遇到问题时只是浅尝辄止,不再深入研究,那么很难做到真正的技术提升。希望这份系统化的技术体系对大家有一个方向参考。

《Android学习笔记总结+移动架构视频+大厂面试真题+项目实战源码》,点击传送门即可获取!
  • 24
    点赞
  • 18
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值