反思|Android 事件分发机制的设计与实现

Posted 却把清梅嗅

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了反思|Android 事件分发机制的设计与实现相关的知识,希望对你有一定的参考价值。

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

概述

android体系本身非常宏大,源码中值得思考和借鉴之处众多。以整体事件分发机制为例,其整个流程涉及到了 系统启动流程SystemServer)、输入管理(InputManager)、系统服务和UI的通信ViewRootImpl + Window + WindowManagerService)、事件分发 等等一系列的环节。

对于 事件分发 环节而言,不可否认非常重要,但Android系统 整体事件分发机制 也是一名优秀Android工作者需要去了解的,本文笔者将针对Android 事件分发整体机制和设计思路 进行描述,其整体结构如下图:

整体思路

1.架构设计

Android系统中将输入事件定义为InputEvent,而InputEvent根据输入事件的类型又分为了KeyEventMotionEvent,前者对应键盘事件,后者则对应屏幕触摸事件,这些事件统一由系统输入管理器InputManager进行分发。

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

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

2.建立通信

ActivityThread负责控制Activity的启动过程,在performLaunchActivity()流程中,ActivityThread会针对Activity创建对应的PhoneWindowDecorView实例,而之后的handleResumeActivity()流程中则会将PhoneWindow应用 )和系统硬件层级的InputManagerService( 系统服务 )通信建立对应的连接,保证UI可见并能够对输入事件进行正确的分发,这之后Activity就会成为可见的。

如何在应用程序和系统服务之间建立通信?AndroidWindowInputManagerService之间的通信实际上使用的InputChannel,InputChannel是一个pipe,底层实际是通过socket进行通信:

ActivityThreadhandleResumeActivity()流程中, 会通过WindowManagerImpl.addView()为当前的Window创建一个ViewRootImpl实例,当InputManager监控到硬件层级的输入事件时,会通知ViewRootImpl对输入事件进行底层的事件分发。

3.事件分发

View布局流程测量流程 相同,Android事件分发处理机制也使用了 递归 的思想,因为一个事件最多只有一个消费者,所以通过责任链的方式将事件自顶向下进行传递,找到事件的消费者(这里是指一个View)之后,再自底向上返回结果。

读到这里,读者应该觉得非常熟悉了,但实际上这里描述的事件分发流程为UI层级的事件分发——它只是事件分发流程整体的一部分。读者需要理解,ViewRootImplInputManager获取到新的输入事件时,会针对输入事件通过一个复杂的 责任链 进行底层的递归,将不同类型的输入事件(比如 屏幕触摸事件键盘输入事件 )进行不同策略的分发,而只有部分符合条件的 屏幕触摸事件 最终才有可能进入到UI层级的事件分发:

如图所示,蓝色箭头描述的流程才是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 Tree正常运作的动力所在,控件的测量、布局、绘制以及输入事件的分发都由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层级事件分发 的一个环节,但却是最重要的一个环节,也是本文的重点,上文所有内容都是为本节做系统性的铺

以上是关于反思|Android 事件分发机制的设计与实现的主要内容,如果未能解决你的问题,请参考以下文章

反思|Android 事件拦截机制的设计与实现

反思|Android View机制设计与实现:测量流程

反思|Android LayoutInflater机制的设计与实现

反思 | 事件总线的局限性,组件化开发流程中通信机制的设计与实现

反思 | 事件总线的局限性,组件化开发流程中通信机制的设计与实现

反思 | 事件总线的局限性,组件化开发流程中通信机制的设计与实现