反思|Android 事件分发机制的设计与实现
Posted 却把清梅嗅
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了反思|Android 事件分发机制的设计与实现相关的知识,希望对你有一定的参考价值。
反思 系列博客是我的一种新学习方式的尝试,该系列起源和目录请参考 这里 。
概述
android
体系本身非常宏大,源码中值得思考和借鉴之处众多。以整体事件分发机制为例,其整个流程涉及到了 系统启动流程(SystemServer
)、输入管理(InputManager
)、系统服务和UI的通信(ViewRootImpl
+ Window
+ WindowManagerService
)、事件分发 等等一系列的环节。
对于 事件分发 环节而言,不可否认非常重要,但Android
系统 整体事件分发机制 也是一名优秀Android
工作者需要去了解的,本文笔者将针对Android
事件分发整体机制和设计思路 进行描述,其整体结构如下图:
整体思路
1.架构设计
Android
系统中将输入事件定义为InputEvent
,而InputEvent
根据输入事件的类型又分为了KeyEvent
和MotionEvent
,前者对应键盘事件,后者则对应屏幕触摸事件,这些事件统一由系统输入管理器InputManager
进行分发。
在系统启动的时候,SystemServer
会启动窗口管理服务WindowManagerService
,WindowManagerService
在启动的时候就会通过启动系统输入管理器InputManager
来负责监控键盘消息。
InputManager
负责从硬件接收输入事件,并将事件分发给当前激活的窗口(Window
)处理,这里我们将前者理解为 系统服务,将后者理解为应用层级的 UI, 因此需要有一个中介负责 服务 和 UI 之间的通信,于是ViewRootImpl
类应运而生。
2.建立通信
ActivityThread
负责控制Activity
的启动过程,在performLaunchActivity()
流程中,ActivityThread
会针对Activity
创建对应的PhoneWindow
和DecorView
实例,而之后的handleResumeActivity()
流程中则会将PhoneWindow
( 应用 )和系统硬件层级的InputManagerService
( 系统服务 )通信建立对应的连接,保证UI可见并能够对输入事件进行正确的分发,这之后Activity
就会成为可见的。
如何在应用程序和系统服务之间建立通信?Android
中Window
和InputManagerService
之间的通信实际上使用的InputChannel
,InputChannel
是一个pipe
,底层实际是通过socket
进行通信:
在ActivityThread
的handleResumeActivity()
流程中, 会通过WindowManagerImpl.addView()
为当前的Window
创建一个ViewRootImpl
实例,当InputManager
监控到硬件层级的输入事件时,会通知ViewRootImpl
对输入事件进行底层的事件分发。
3.事件分发
与View
的 布局流程 和 测量流程 相同,Android
事件分发处理机制也使用了 递归 的思想,因为一个事件最多只有一个消费者,所以通过责任链的方式将事件自顶向下进行传递,找到事件的消费者(这里是指一个View
)之后,再自底向上返回结果。
读到这里,读者应该觉得非常熟悉了,但实际上这里描述的事件分发流程为UI层级的事件分发——它只是事件分发流程整体的一部分。读者需要理解,ViewRootImpl
从InputManager
获取到新的输入事件时,会针对输入事件通过一个复杂的 责任链 进行底层的递归,将不同类型的输入事件(比如 屏幕触摸事件 和 键盘输入事件 )进行不同策略的分发,而只有部分符合条件的 屏幕触摸事件 最终才有可能进入到UI层级的事件分发:
如图所示,蓝色箭头描述的流程才是UI层级的事件分发。
为了方便理解,本文使用以下两个词汇对上文两个斜体词汇进行描述:应用整体的事件分发 和 UI层级的事件分发 ——需要重申的是,这两个词汇虽然会被分开讲解,但其本质仍然属于一个完整 事件分发的责任链,后者只是前者的一小部分而已。
架构设计
1.InputEvent:输入事件分类概述
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
是有必要的。
2.InputManager:系统输入管理器
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
跨进程通信并非重点,读者仅需了解 系统服务的启动流程 和 层级关系 即可,参考下图:
3.ViewRootImpl:窗口服务与窗口的纽带
InputManager
并将事件分发给当前激活的窗口(Window
)处理,这里我们将前者理解为系统层级的 (窗口)服务,将后者理解为应用层级的 窗口, 因此需要有一个中介负责 服务 和 窗口 之间的通信,于是ViewRootImpl
类应运而生。
ViewRootImpl
作为链接WindowManager
和DecorView
的纽带,同时实现了ViewParent
接口,ViewRootImpl
作为整个控件树的根部,它是View Tree
正常运作的动力所在,控件的测量、布局、绘制以及输入事件的分发都由ViewRootImpl
控制。
那么ViewRootImpl
是如何被创建和初始化的,而 (窗口)服务 和 窗口 之间的通信又是如何建立的呢?
建立通信
1.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
开始了绘制流程的三个步骤。
2.通信的建立
完成了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
因此,设计者针对事件分发的整个责任链设计了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.链接
WindowManager
和DecorView
的纽带,更广一点可以说是Window
和View
之间的纽带; - 2.完成
View
的绘制过程,包括measure、layout、draw
过程; - 3.向DecorView分发收到的用户发起的
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);
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
实际上就是Activity
中Window
的根布局,它是一个FrameLayout
。
现在DecorView
执行了dispatchPointerEvent(event)
函数,这是不是就意味着开始了View
的事件分发?
DecorView的双重职责
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个职责:
- 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层级事件分发 的一个环节,但却是最重要的一个环节,也是本文的重点,上文所有内容都是为本节做系统性的铺
以上是关于反思|Android 事件分发机制的设计与实现的主要内容,如果未能解决你的问题,请参考以下文章
反思|Android LayoutInflater机制的设计与实现
反思 | 事件总线的局限性,组件化开发流程中通信机制的设计与实现