反思 系列博客是个人一种新学习方式的尝试,该系列起源和目录请参考 这里 。html
Android
体系自己很是宏大,源码中值得思考和借鉴之处众多。以总体事件分发机制为例,其整个流程涉及到了 系统启动流程(SystemServer
)、输入管理(InputManager
)、系统服务和UI的通讯(ViewRootImpl
+ Window
+ WindowManagerService
)、事件分发 等等一系列的环节。java
对于 事件分发 环节而言,不能否认很是重要,但Android
系统完整的 事件分发机制 也是一名优秀Android
工做者须要去了解的,本文笔者将针对Android
事件分发机制及设计思路 进行描述,其总体结构以下图:android
Android
系统中将输入事件定义为InputEvent
,而InputEvent
根据输入事件的类型又分为了KeyEvent
和MotionEvent
,前者对应键盘事件,后者则对应屏幕触摸事件,这些事件统一由系统输入管理器InputManager
进行分发。git
在系统启动的时候,SystemServer
会启动窗口管理服务WindowManagerService
,WindowManagerService
在启动的时候就会经过启动系统输入管理器InputManager
来负责监控键盘消息。github
InputManager
负责从硬件接收输入事件,并将事件分发给当前激活的窗口(Window
)处理,这里咱们将前者理解为 系统服务,将后者理解为应用层级的 UI, 所以须要有一个中介负责 服务 和 UI 之间的通讯,因而ViewRootImpl
类应运而生。算法
ActivityThread
负责控制Activity
的启动过程,在performLaunchActivity()
流程中,ActivityThread
会针对Activity
建立对应的PhoneWindow
和DecorView
实例,而以后的handleResumeActivity()
流程中则会将PhoneWindow
( 应用 )和InputManagerService
( 系统服务 )通讯以创建对应的链接,保证UI可见并可以对输入事件进行正确的分发,这以后Activity
就会成为可见的。架构
如何在应用程序和系统服务之间创建通讯?Android
中Window
和InputManagerService
之间的通讯实际上使用的InputChannel
,InputChannel
是一个pipe
,底层实际是经过socket
进行通讯:app
在ActivityThread
的handleResumeActivity()
流程中, 会经过WindowManagerImpl.addView()
为当前的Window
建立一个ViewRootImpl
实例,当InputManager
监控到硬件层级的输入事件时,会通知ViewRootImpl
对输入事件进行底层的事件分发。socket
与View
的 布局流程 和 测量流程 相同,Android
事件分发机制也使用了 递归 的思想,由于一个事件最多只有一个消费者,因此经过责任链的方式将事件自顶向下进行传递,找到事件的消费者(这里是指一个View
)以后,再自底向上返回结果。ide
读到这里,读者应该以为很是熟悉了,但实际上这里描述的事件分发流程为UI层级的事件分发——它只是事件分发流程总体的一部分。读者须要理解,ViewRootImpl
从InputManager
获取到新的输入事件时,会针对输入事件经过一个复杂的 责任链 进行底层的递归,将不一样类型的输入事件(好比 屏幕触摸事件 和 键盘输入事件 )进行不一样策略的分发,而只有部分符合条件的 屏幕触摸事件 最终才有可能进入到UI层级的事件分发:
如图所示,蓝色箭头描述的流程才是UI层级的事件分发。
为了方便理解,本文使用了如下两个词汇:应用总体的事件分发 和 UI层级的事件分发 ——须要重申的是,这两个词汇虽然被分开讲解,但其本质仍然属于一个完整 事件分发的责任链,后者只是前者的一小部分而已。
Android
系统中将输入事件定义为InputEvent
,而InputEvent
根据输入事件的类型又分为了KeyEvent
和MotionEvent
:
// 输入事件的基类
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
是有必要的。
Android
系统的设计中,InputEvent
统一由系统输入管理器InputManager
进行分发。在这里InputManager
是native
层级的一个类,负责与硬件通讯并接收输入事件。
那么InputManager
是如何初始化的呢?这里就要涉及到Java
层级的SystemServer
了,咱们知道SystemServer
进程中包含着各类各样的系统服务,好比ActivityManagerService
、WindowManagerService
等等,SystemServer
由zygote
进程启动, 启动过程当中对WindowManagerService
和InputManagerService
进行了初始化:
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
会启动窗口管理服务WindowManagerService
,WindowManagerService
在启动的时候就会经过InputManagerService
启动系统输入管理器InputManager
来负责监控键盘消息。
对于本文而言,framework
层级相关如WindowManagerService
(窗口管理服务)、native
层级的源码、SystemServer
亦或者 Binder
跨进程通讯并不是重点,读者仅需了解 系统服务的启动流程 和 层级关系 便可,参考下图:
InputManager
将事件分发给当前激活的窗口(Window
)处理,这里咱们将前者理解为系统层级的 (窗口)服务,将后者理解为应用层级的 窗口, 所以须要有一个中介负责 服务 和 窗口 之间的通讯,因而ViewRootImpl
类应运而生。
ViewRootImpl
做为连接WindowManager
和DecorView
的纽带,同时实现了ViewParent
接口,ViewRootImpl
做为整个控件树的根部,它是View
树正常运做的动力所在,控件的测量、布局、绘制以及输入事件的分发都由ViewRootImpl
控制。
那么ViewRootImpl
是如何被建立和初始化的,而 (窗口)服务 和 窗口 之间的通讯又是如何创建的呢?
既然Android
系统将 (窗口)服务 与 窗口 的通讯创建交给了ViewRootImpl
,那么ViewRootImpl
必然持有了二者的依赖,所以了解ViewRootImpl
是如何建立的就很是重要。
咱们知道,ActivityThread
负责控制Activity
的启动过程,在ActivityThread.performLaunchActivity()
流程中,ActivityThread
会针对Activity
建立对应的PhoneWindow
和DecorView
实例,而在ActivityThread.handleResumeActivity()
流程中,ActivityThread
会将获取当前Activity
的WindowManager
,并将DecorView
和WindowManager.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
开始了绘制流程的三个步骤。
完成了ViewRootImpl
的建立以后,如何完成系统输入服务和应用程序进程的链接呢?
Android
中Window
和InputManagerService
之间的通讯实际上使用的InputChannel
,InputChannel
是一个pipe
,底层实际是经过socket
进行通讯。在ViewRootImpl.setView()
过程当中,也会同时注册InputChannel
:
public final class InputChannel implements Parcelable { }
复制代码
上文中,咱们提到了ViewRootImpl.setView()
函数,在该函数的执行过程当中,会在ViewRootImpl
中建立InputChannel
,InputChannel
实现了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);
}
}
复制代码
这里涉及到了WindowManagerService
和Binder
跨进程通讯,读者不须要纠结于详细的细节,只需了解最终在SystemServer
进程中,WindowManagerService
根据当前的Window
建立了SocketPair
用于跨进程通讯,同时并对App
进程中传过来的InputChannel
进行了注册,这以后,ViewRootImpl
里的InputChannel
就指向了正确的InputChannel
, 做为Client
端,其fd
与SystemServer
进程中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
类做为基类,做为责任链中的模版,并实现了若干个子类,为输入事件按顺序分阶段进行分发处理:
// 事件分发不一样阶段的基类
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
自己职责的体现:
WindowManager
和DecorView
的纽带,更广一点能够说是Window
和View
之间的纽带;View
的绘制过程,包括measure、layout、draw
过程;InputEvent
事件。最终总体事件分发流程由以下责任链构成:
SyntheticInputStage --> ViewPostImeStage --> NativePostImeStage --> EarlyPostImeStage --> ImeInputStage --> ViewPreImeInputStage --> NativePreImeInputStage
上文说到,真正从Native
层的InputManager
接收输入事件的是ViewRootImpl
的WindowInputEventReceiver
对象,既然负责输入事件的分发,天然也负责将事件分发的结果反馈给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);
}
}
复制代码
上文已经提到,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
实际上就是Activity
中Window
的根布局,它是一个FrameLayout
。
如今DecorView
执行了dispatchPointerEvent(event)
函数,这是否是就意味着开始了View
的事件分发?
DecorView
做为View
树的根节点,接收到屏幕触摸事件MotionEvent
时,应该经过递归的方式将事件分发给子View
,这彷佛理所固然。但实际设计中,设计者将DecorView
接收到的事件首先分发给了Activity
,Activity
又将事件分发给了其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个职责:
DecorView
不一样于其它View
,它须要先将事件转发给最外层的Activity
,使得开发者能够经过重写Activity.onTouchEvent()
函数以达到对当前屏幕触摸事件拦截控制的目的,这里DecorView
履行了自身(根节点)特殊的职责;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
层和应用层之间的事件分发机制的一部分了。
虽然View
层级之间的事件分发只是 UI层级事件分发 的一个环节,但倒是最重要的一个环节,也是本文的重点,上文全部内容都是为本节作系统性的铺垫 ——为了方便阅读,本小节接下来的内容中,事件分发 统一泛指 View层级的事件分发。
了解 事件分发 的代码流程细节,首先须要了解整个流程的最终目的,那就是 获知事件是否被消费 ,至于事件被哪一个角色消费了,怎么被消费的,在外层责任链中的ViewPostImeInputStage
不关心,其更上层ViewRootImpl.WindowInputEventReceiver
不关心,native
层级的InputManager
天然更不会关心了。
所以,设计者设计出了这样一个函数:
// 对事件进行分发
public boolean dispatchTouchEvent(MotionEvent event);
复制代码
对于事件分发结果的接收者而言,其只关心事件是否被消费,所以返回值被定义为了boolean
类型:当返回值为true
,事件被消费,反之则事件未被消费。
上文中咱们一样提到了,在ViewGroup
的事件分发过程当中,其自己的dispatchTouchEvent(event)
和super.dispatchTouchEvent(event)
彻底是两个彻底不一样的函数,前者履行的是ViewGroup
的职责,负责将事件分发给子View
;后者履行的是View
的职责,负责处理决定事件是否被消费(参见 应用总体的事件分发-DecorView的双重职责 小节)。
所以,对于事件分发总体流程,咱们能够进行以下定义:
ViewGroup
将事件分发给子View
,当子View
从ViewGroup
中接收到事件,若其有child
,则经过dispatchTouchEvent(event)
再将事件分发给child
...以此类推,直至将事件分发到底部的View
,这也是事件分发的 递流程;View
接收到事件时,经过View
自身的dispatchTouchEvent(event)
函数判断是否消费事件:true
向上层的ViewGroup
返回,ViewGroup
接收到true
,意味着事件已经被消费,所以跳过了是否要消费该事件的判断,直接向上一级继续返回true
,以此类推直到将true
结果通知到最上层的View
节点;false
,ViewGroup
接收到false
,意味着事件未被消费,所以其自己执行super.dispatchTouchEvent(event)
——即执行View
自己的dispatchTouchEvent(event)
函数,并将结果向上级返回,以此类推直到将true
结果通知到最上层的View
节点。对于初次了解事件分发机制或者不熟悉递归思想的读者而言,上述文字彷佛晦涩难懂,实际上用代码实现却惊人的简单:
// 伪代码实现
// ViewGroup.dispatchTouchEvent
public boolean dispatchTouchEvent(MotionEvent event) {
boolean consume = false;
// 1.将事件分发给Child
if (hasChild) {
consume = child.dispatchTouchEvent();
}
// 2.若Child不消费该事件,或者没有child,判断自身是否消费该事件
if (!consume) {
consume = super.dispatchTouchEvent();
}
// 3.将结果向上层传递
return consume;
}
复制代码
上述代码中已经将 事件分发 最核心的流程表现的淋漓尽致,读者需认真理解和揣摩。View
层级的事件传递的真正实现虽然复杂,但其本质却和上述代码并不不一样,理解了这个基本的流程,接下来对于额外功能扩展的设计与实现也只是时间问题了。
在上一小节中,读者已经了解事件分发的本质原理就是递归,而目前其实现方式是,每接收一个新的事件,都须要进行一次递归才能找到对应消费事件的View
,并依次向上返回事件分发的结果。
每一个事件都对View
树进行一次遍历递归?这对性能的影响显而易见,所以这种设计是有改进空间的。
如何针对这个问题进行改进?首先,设计者根据用户的行为对MotionEvent
中添加了一个Action
的属性以描述该事件的行为:
ACTION_CANCEL
...定义了这些行为的同时,设计者定义了一个叫作 事件序列 的概念:针对用户的一次触摸操做,必然对应了一个 事件序列,从用户手指接触屏幕,到移动手指,再到抬起手指 ——单个事件序列必然包含ACTION_DOWN
、ACTION_MOVE
... ACTION_MOVE
、ACTION_UP
等多个事件,这其中ACTION_MOVE
的数量不肯定,ACTION_DOWN
和ACTION_UP
的数量则为1。
定义了 事件序列 的概念,设计者就能够着手对现有代码进行设计和改进,其思路以下:当接收到一个ACTION_DOWN
时,意味着一次完整事件序列的开始,经过递归遍历找到View
树中真正对事件进行消费的Child
,并将其进行保存,这以后接收到ACTION_MOVE
和ACTION_UP
行为时,则跳过遍历递归的过程,将事件直接分发给Child
这个事件的消费者;当接收到ACTION_DOWN
时,则重置整个事件序列:
如图所示,其表明了一个
View
树,若序号为4的View
是实际事件的消费者,那么当接收到ACTION_DOWN
事件时,上层的ViewGroup
则会经过递归找到它,接下来该事件序列中的其它事件到来时,也交给4号View
去处理。
这个思路彷佛没有问题,可是目前的设计中咱们还缺乏一把关键的钥匙,那就是如何在ViewGroup
中保存实际消费事件的View
?
为此设计者根据View
的树形结构,设计了一个TouchTarget
类,为做为一个成员属性,描述ViewGroup
下一级事件分发的目标:
public abstract class ViewGroup extends View {
// 指向下一级事件分发的`View`
private TouchTarget mFirstTouchTarget;
private static final class TouchTarget {
public View child;
public TouchTarget next;
}
}
复制代码
这里应用到了树的 深度优先搜索算法(Depth-First-Search,简称DFS算法),正如代码所描述的,每一个ViewGroup
都持有一个mFirstTouchTarget
, 当接收到一个ACTION_DOWN
时,经过递归遍历找到View
树中真正对事件进行消费的Child
,并保存在mFirstTouchTarget
属性中,依此类推组成一个完整的分发链。
好比上文的树形图中,序号为1的
ViewGroup
中的mFirstTouchTarget
指向序号为2的ViewGroup
,后者的mFirstTouchTarget
指向序号为3的ViewGroup
,依此类推,最终组成了一个 1 -> 2 -> 3 -> 4 事件的分发链。
对于一个 事件序列 而言,第一次接收到ACTION_DOWN
事件时,经过DFS算法为View
树事件的 分发链 进行初始化,在这以后,当接收到同一事件序列的其它事件如ACTION_MOVE
、ACTION_UP
时,则会跳过递归流程,将事件直接分发给 分发链 下一级的Child
中:
// ViewGroup.dispatchTouchEvent
public boolean dispatchTouchEvent(MotionEvent event) {
boolean consume = false;
// ...
if (event.isActionDown()) {
// 1.第一次接收到Down事件,递归寻找分发链的下一级,即消费该事件的View
// 这里能够看到,递归深度搜索的算法只执行了一次
mFirstTouchTarget = findConsumeChild(this);
}
// ...
if (mFirstTouchTarget == null) {
// 2.分发链下一级为空,说明没有子`View`消费该事件
consume = super.dispatchTouchEvent(event);
} else {
// 3.mFirstTouchTarget不为空,必然有消费该事件的`View`,直接将事件分发给下一级
consume = mFirstTouchTarget.child.dispatchTouchEvent(event);
}
// ...
return consume;
}
复制代码
至此,本小节一开始提到的问题获得了解决。
读者应该都有了解,为了增长 事件分发 过程当中的灵活性,Android
为ViewGroup
层级设计了onInterceptTouchEvent()
函数并向外暴露给开发者,以达到让ViewGroup
跳过子View
的事件分发,提早结束 递流程 ,并自身决定是否消费事件,并将结果反馈给上层级的ViewGroup
处理。
额外设计这样一个接口是否有必要?读者认真思考能够得知,这是有必要的,最经典的使用场景就是经过重写onInterceptTouchEvent()
函数以解决开发中常见的 滑动冲突 事件,这里咱们再也不进行引伸,仅探讨设计者是如何设计事件拦截机制的。
实际上事件拦截机制的实现很是简单,咱们仅须要在正式的事件分发以前,经过条件分支判断是否须要拦截当前事件的分发便可:
// 伪代码实现
// ViewGroup.dispatchTouchEvent
public boolean dispatchTouchEvent(MotionEvent event) {
// 1.若须要对事件进行拦截,直接停止事件向下分发,让自身决定是否消费事件,并将结果返回
if (onInterceptTouchEvent(event)) {
return super.dispatchInputEvent(event);
}
// ...
// 2.若不拦截当前事件,开始事件分发流程
}
复制代码
此外,为了不额外的开销,设计者根据 事件序列 为 事件拦截机制 作出了额外的优化处理,保证了 事件拦截的判断在一个事件序列中只处理一次,伪代码简单实现以下:
// ViewGroup.dispatchTouchEvent
public boolean dispatchTouchEvent(MotionEvent event) {
if (mFirstTouchTarget != null) {
// 1.若须要对事件进行拦截,直接停止事件向下分发,让自身决定是否消费事件,并将结果返回
if (onInterceptTouchEvent(event)) {
// 2.肯定对该事件序列拦截后,所以就没有了下一级要分发的Child
mFirstTouchTarget = null;
// 下一个事件传递过来时,最外层的if判断就会为false,不会再重复执行onInterceptTouchEvent()了
return super.dispatchInputEvent(event);
}
}
// ...
// 3.若不拦截当前事件,开始事件分发流程
}
复制代码
为了令代码便于理解,上述伪代码中逻辑其实是有瑕疵的,读者没必要纠结于细节,详细实现请参考源码。
至此,事件分发 中 事件拦截机制 的设计初衷、流程的实现,以及性能的优化也阐述完毕。
在一步步对细节的填充过程当中,事件分发 体系的设计已初显峥嵘,但回归本质,这些细节犹如血肉,而核心的思想(即递归)才是骨架,只有骨架搭建起来,细节的血肉才能一点点覆于其上,最终演变为成为生机勃勃的 事件分发 完总体系。
Android
总体的事件分发机制十分复杂,单就一篇文章来讲,本文也仅仅只能站在巨人的肩膀上,对总体的轮廓进行一个简略的描述,强烈建议参考本文开篇的思惟导图并结合源码进行总体小结。
这一篇文章就能让我理解Android事件分发机制吗?
固然不能,即便是笔者对此也只是初窥门径而已,在撰写本文的过程当中,笔者参考了许多优秀的学习资料,一样笔者也不认为本文比这些资料讲解的更透彻,读者能够参考这些资料 ——一千我的有一千个哈姆雷特,也许这些优秀的资料相比本文更适合你呢?
源码永远是学习过程当中最好的老师,RTFSC。
神书,书中 View的事件分发机制 一节将源码分析到了极致,讲解的很是透彻,强烈建议 建议读者源码阅读时参考这本书。
framework
层原理分析的神文,懂得天然懂。本文中的部分图片也引自该文。
很是好的博客系列。
对
ViewRootImpl
讲解很是透彻的一篇博客,本文对于ViewRootImpl
的主要职责的描述也是参考了此文。
很是欣赏 @KunMinX 老师博文的风格,大道至简,此文对事件消费过程当中的 消费 二字的讲解很是透彻,给予了笔者不少启示——另,本文不是黑车(笑)。
Hello,我是 却把清梅嗅 ,若是您以为文章对您有价值,欢迎 ❤️,也欢迎关注个人 博客 或者 Github。
若是您以为文章还差了那么点东西,也请经过关注督促我写出更好的文章——万一哪天我进步了呢?