Android 应用整体的事件分发体系——源码探究

参考:
反思 | Android 事件分发机制的设计与实现 【掘金】(本文其实相当于是对它的读后感)
Android 全面解析之window机制【掘金】(主要是讲window的)
Android 之window机制token验证 (wms和token相关)
Android 系统源码分析 - 事件分发(这里对Native层整个过程讲的比较细)
输入系统:进程间双向通信(socketpair+binder) (这个对事件分发Native层用的原理讲的很清楚)

一、概述

反思 | Android 事件分发机制的设计与实现 【掘金】里说

Android 系统中将输入事件定义为 InputEvent,而 InputEvent 根据输入事件的类型又分为了KeyEventMotionEvent,前者对应键盘事件,后者则对应屏幕触摸事件,这些事件统一由系统输入管理器InputManager 进行分发。
UI层级的事件分发,只是应用层级的事件分发的一小部分。ViewRootImpl InputManager 获取到新的输入事件时,会针对输入事件通过一个复杂的 **责任链 **进行底层的递归,将不同类型的输入事件(比如 屏幕触摸事件 和 键盘输入事件 )进行不同策略的分发,而只有部分符合条件的 屏幕触摸事件 最终才有可能进入到UI层级的事件分发

event.png

二、基础概念

InputEvent

  InputEvent是一个基类,它有两个子类:KeyEventMotionEvent,后者对应的就是触摸事件。实际的触摸方式可能有很多,手指、笔、甚至华为的隔空触屏,它们全都抽象成了 MotionEvent

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

public class KeyEvent extends InputEvent implements Parcelable { }

public final class MotionEvent extends InputEvent implements Parcelable { }

InputManager(Native)

  InputManager 是 Android 系统的输入管理器,它负责分发 InputEvent 。这是一个 native 层级的类,注意不要跟 java 层的同名类搞混了。

InputManager::InputManager(
        const sp<EventHubInterface>& eventHub,
        const sp<InputReaderPolicyInterface>& readerPolicy,
        const sp<InputDispatcherPolicyInterface>& dispatcherPolicy) {
    mDispatcher = new InputDispatcher(dispatcherPolicy);
    mReader = new InputReader(eventHub, readerPolicy, mDispatcher);
    initialize();
}

InputManagerService

  java 层初始化和访问这个 native 层InputManager 是通过InputManagerService,它也是运行在 SystemServer 进程里的系统级 Service,它的初始化时机是 WindowManagerService 是一起的,同时后者会持有前者的引用 。

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();
  }
}


public class InputManagerService extends IInputManager.Stub {

  public InputManagerService(Context context) {
    // ...通知native层初始化 InputManager
    // 这个 ptr 应该跟MessageQueue里面的那个一样,是指向Native层对象的指针
    mPtr = nativeInit(this, mContext, mHandler.getLooper().getQueue());
  }

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

Window(Java)

  这里指的是 Java 层中的类 window,它的唯一子类是 phoneWindow,实际上它并不是我们常说的 window 机制中的 window。
  同时它有一个管理类(管理的意思是管理PhoneWindow内部的一些逻辑)也就是 WindowManager,它的唯一实现是 WindowManagerImpl 。但是实际上这个类只是一个代理,它实际的逻辑是在 WindowManagerGlobal中完成的,这是一个单例类,里面统一存放了本进程所有的 ViewrRootImpl等。当然,对于外部来说,它们是只知道 WindowManagerImpl的,这就是面向对象。

这些概念这篇文章讲的很好:Android 全面解析之window机制【掘金】

window 本身并不存在,他只是一个概念。… 因他不存在,所以也很难从源码中找到他的痕迹,window机制的操作单位都是view,如果要说他在源码中的存在形式,笔者目前的认知就是在WindowManagerService中每一个view对应一个windowStatus。

总结一下:
PhoneWindow本身不是真正意义上的window,他更多可以认为是辅助Activity操作window的工具类。
windowManagerImpl并不是管理window的类,而是管理PhoneWindow的类。真正管理window的是WMS。
PhoneWindow可以配合DecorView可以给其中的window按照一定的逻辑提供标准的UI策略。
PhoneWindow限制了不同的组件添加window的权限。

ViewRootImpl

  这个类非常关键,它是连接 WindowManagerdecorView的桥梁,事件经过它实现从 window 到 view 的传递。同时它继承了 ViewParent ,是整个 view 树的根部,除了事件分发,view 测量绘制都由它来控制,重要性不言而喻。

三、建立通信

ViewRootImpl 的创建

  根据上面的概念陈述,我们已经可以知道 ViewRootImpl 是非常重要的一个类,那么它是什么时候创建并进行初始化的呢?这个问题对于理解整个事件和绘制体系都非常关键。

PhoneWindow decorView 的创建
  在 Activity 的启动过程中,AMS会通过Binder 调用 ApplicationThread 的相关方法(Api28之前是scheduleLaunchActivity,之后全部生命周期都是回调scheduleTransaction ),然后调用到 AcitivityThread.performLaunchActivity ,在这个方法里面,会生成 Activity的实例对象,同时生成对应的 PhoneWindowWindowManagerImpl decorView 对象。

ViewRootImpl的创建
  然后在回调 onResume 方法的 AcitivityThread.performResumeActivity 方法里,会调用 WindowManagerImpl.addview 方法,传入decorView (onCreate 中已经添加了 ContentView 进去了) 和 WindowManager.LayoutParams 参数。
  在这个方法里,完成了对 ViewRootImpl 的初始化,并调用 ViewRootImpl.setView,这个方法非常重要。

// 1.WindowManager 的本质实际上是 WindowManagerImpl
public final class ActivityThread {
  
  //...
  @Override
  public void handleResumeActivity(...){
    //...
    windowManager.addView(decorView, windowManagerLayoutParams);
  }
}

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建立通信,这个下文再提
  }
}

通信的建立

  ViewRootImpl 创建完成了,但是系统输入服务(InputManagerService)和用户进程之间的连接是如何做到的呢?这里就会用到 InputChannel 了,这是一个 Pipe ,实际是一个 Socket。
Android 系统源码分析 - 事件分发 中的图:

  ViewRootImpl.setView 在执行的过程中,会创建 InputChannel(java),它实现了 Parcelable,所以它是可以通过 Binder 进程跨进程传输的。
  mWindowSession是一个binder 代理对象,它的真实实现是 SystemServer进程里面的 Session类,这里会调用它的addToDisplay 方法。
  最终这个方法会走到 WMS.addWindow ,实际上:每一个应用程序进程都会对应一个 Session,它在调用WMS.addWindow时,会将自己作为参数传入方法中, WMS 会用 一个List 来保存这些 Session

//ViewRootImpl.java
 public void setView(View view, WindowManager.LayoutParams attrs, View panelParentView) {
    synchronized (this) {
     //...
      try {
            mOrigWindowType = mWindowAttributes.type;
            mAttachInfo.mRecomputeGlobalAttributes = true;
            collectViewAttributes();
            res = mWindowSession.addToDisplay(mWindow, mSeq, mWindowAttributes,
                    getHostVisibility(), mDisplay.getDisplayId(),
                    mAttachInfo.mContentInsets, mAttachInfo.mStableInsets,
                    mAttachInfo.mOutsets, mInputChannel);
           } catch (RemoteException e) {
              //...
           }  
     //...
    }
 }

//Session.java
public class Session extends IWindowSession.Stub implements IBinder.DeathRecipient {
  
  final WindowManagerService mService;
  //...
        @Override
    public int addToDisplay(IWindow window, int seq, WindowManager.LayoutParams attrs,
            int viewVisibility, int displayId, Rect outContentInsets, Rect outStableInsets,
            Rect outOutsets, InputChannel outInputChannel) {
        return mService.addWindow(this, window, seq, attrs, viewVisibility, displayId,
                outContentInsets, outStableInsets, outOutsets, outInputChannel);
    }
  //...
}

Window 的 token:
  Activity的 token 是在 attach 的时候,由 AMS 传给Activity的,它会把它保存在 PhoneWindowmWindowAttribute变量中。
  而子窗口(例如PopupWindow)的token是父窗口的ViewRootImpl中的 w(IWindow对应的 Binder 对象)对象。所以需要父窗口对应的 ViewRootImpl调用完 setView方法(onResume之后),将 w 存入 WMS 的 mWindowMap了,才能展示。

//Activity
@Override
protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    //不会报错
    Dialog dialog = new Dialog(this);
    dialog.addContentView(new FrameLayout(this),
            new ViewGroup.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT,
                    ViewGroup.LayoutParams.MATCH_PARENT));
    dialog.show();

    //报错:Unable to add window -- token null is not valid; is your activity running?
    PopupWindow popupWindow = new PopupWindow(this);
    popupWindow.setContentView(new FrameLayout(this));
    popupWindow.showAsDropDown(LayoutInflater.from(this).inflate(R.layout.activity_main,null));
}

  需要注意的是Dialog实际上不是子窗口,它有自己的PhoneWindow,其类型是TYPE_APPLICATION,它跟 Activity是共用的 WindowManager,同时其中的 parentWindow变量正是 ActivityPhoneWindow,所以它在显示的过程中其实用的是 Activity的 token 。
  另外按常规流程来说,Activity的 token,是在 attach 之后在ActivityStack.startActivityLocked中, 才存入 WMS 的 mWindowMap的。

//dialog
Dialog(@NonNull Context context, @StyleRes int themeResId,
       boolean createContextThemeWrapper) {
    //..

    //这里实际拿到的是activity的windowManager
    mWindowManager = (WindowManager) context.getSystemService(Context.WINDOW_SERVICE);

    final Window w = new PhoneWindow(mContext);
    mWindow = w;
    w.setCallback(this);
    w.setOnWindowDismissedCallback(this);
    w.setOnWindowSwipeDismissedCallback(() -> {
        if (mCancelable) {
            cancel();
        }
    });
  
    //这里实际会走到下面那个方法
    w.setWindowManager(mWindowManager, null, null);
    w.setGravity(Gravity.CENTER);

    mListenersHandler = new ListenersHandler(this);
}

//WindowManagerImpl
public WindowManagerImpl createLocalWindowManager(Window parentWindow) {
    //这个parentWindow实际就是activity的phoneWindow
    return new WindowManagerImpl(mContext, parentWindow);
}

//WindowManagerGlobal
//dialog在show的时候也会走到这,具体就不说了,应该都知道了
public void addView(View view, ViewGroup.LayoutParams params,
            Display display, Window parentWindow) {
    //..
  final WindowManager.LayoutParams wparams = (WindowManager.LayoutParams) params;
  
    //这里是关键,会用parentWindow去初始化子window的LayoutParams
  if (parentWindow != null) {
      parentWindow.adjustLayoutParamsForSubWindow(wparams);
  } else {
      //...
  }
}

//window
//注意这个方法是 parentWindow 调用,传入的是window的LayoutParams
void adjustLayoutParamsForSubWindow(WindowManager.LayoutParams wp) {
  CharSequence curTitle = wp.getTitle();
  if (wp.type >= WindowManager.LayoutParams.FIRST_SUB_WINDOW &&
          wp.type <= WindowManager.LayoutParams.LAST_SUB_WINDOW) {
      
      //这里是判断子window的,例如PopupWindow ...
      if (wp.token == null) {
         View decor = peekDecorView();
         if (decor != null) {
             wp.token = decor.getWindowToken();
         }
      }
      //...
  }else if (wp.type >= WindowManager.LayoutParams.FIRST_SYSTEM_WINDOW &&
                wp.type <= WindowManager.LayoutParams.LAST_SYSTEM_WINDOW) {
      //...
  }else{
      //这里是 dialog 的,使用跟 activity 一样的token
      if (wp.token == null) {
         wp.token = mContainer == null ? mAppToken : mContainer.mAppToken;
      }
       //...
  }
     //...
}

-----注意!下面的内容笔者并未完全搞懂,所以可能会有错漏的地方---------------------

系统进程:
  WMS.addWindow 通过一系列的调用,会执行 InputChannel.nativeOpenInputChannelPair ,这是一个Native方法,它会创建两个 InputChannel(Nativie),其中一个是与Client通信的。InputChannel 会创建两个 Socket ,组成一个 SocketPair(为啥要建两个?个人觉得是因为常规Pipe 的一端一般只能负责读或者只负责写,也就是一个 Server 一个 Client,而这里要双向通信),由此就可以进行双向通信了。

有关 SocketPair可以看看这篇文章:输入系统:进程间双向通信(socketpair+binder)

为什么是Socket而不是Binder?
参考:Android Input子系统为什么要使用socket,而不是binder?
个人理解有以下三点:
  1、binder 是同步的调用,如果要实现 socket 异步读和写,那么需要在用户进程和系统进程都新增两个线程去做处理,这样开销当然会更大。
  2、即使两端都新开了线程,由于binder调用是有内置的binder线程池的,两次调用不一定是在同一个线程,这就无法保证事件分发的有序性。
  3、同样的,binder线程池还有一个问题就是它是有大小限制的,高频操作下,可能会出现创建大量binder线程的情况,那线程池的线程资源必然是不够用的。

  总结一下,笔者感觉,次数频繁切需要异步进行的操作,不适合binder

  到这里已经,大概知道系统进程是怎么建立通信和发送事件的了,那么用户进程又做了什么处理,如何去接收事件呢?

用户进程:
  ViewRootImpl.setViewmWindowSession.addToDisplay 调用之后,会初始化一个 WindowInputEventReceiver对象。
  这是 ViewRootImpl的一个内部类,初始化时候会调用 nativeInit 方法,它根据传入的 InputChannelMessageQueue,会在 Native 层创建一个NativeInputEventReceiver。后者将会拿 InputChannel对象的 fd(用户进程和系统进程会是同一个fd) 去添加 epoll监控,监听系统进程端是否有事件产生并且写入 socket 了。
有的话就开启 socket 去读出来之后,再经过一系列的调用,最后会通过JNI回调到 WindowInputEventReceiveronInputEvent方法,这个方法又会调用 ViewRootImpl的方法enqueueInputEvent。至此,事件终于到了 ViewRootImpl中了。
  在 ViewRootImpl 中处理完成之后,会调用InputEventReceivernativeFinishInputEvent 方法,结束应用层的事件分发,逻辑再度进入 native 层中进行。

public final class ViewRootImpl {

  final class WindowInputEventReceiver extends InputEventReceiver {
    @Override
     public void onInputEvent(InputEvent event, int displayId) {
         // 将输入事件加入队列
         enqueueInputEvent(event, this, 0, true);
     }
      
     // 这实际它的父类 InputEventReceiver 的方法,放一起好看一些
     public final void finishInputEvent(InputEvent event, boolean handled) {
        //...
        //调用native层方法,结束应用层的本次事件分发
        nativeFinishInputEvent(mReceiverPtr, seq, handled);
     }
  }
}

四、应用层的事件分发

  ViewRootImpl.enqueueInputEvent 从名字就能看出这是一个队列,当事件加入队列之后,就是事件的分发了。对于一个输入事件来说,有资格处理这个事件的成员必然是有多个的,这里用了 责任链 模式去实现判断和处理事件。单一功能、输入输出确定、多步骤或角色,毫无疑问这个地方是适合使用责任链的。

InputStage

  这个类是事件分发责任链的节点基类,它有很多个子类,分别承担不同的功能。

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

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

final class ViewPreImeInputStage extends InputStage {}

final class EarlyPostImeInputStage extends InputStage {}

// 通常讲的 UI 层事件分发
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 {}

  然后再简要说明一下各个 InputStage 的逻辑和作用,内容引用自:Android Framework 输入子系统 (09)InputStage解读

NativePreImeInputStage:
分发早于IME的InputEvent到NativeActivity中去处理, NativeActivity和普通acitivty的功能一致,不过是在native层实现,这样执行效率会更高,同时NativeActivity在游戏开发中很实用(不支持触摸事件)。

ViewPreIMEInputStage:
分发早于IME的InputEvent到View框架处理,会调用view(输入焦点)的onkeyPreIme方法,同时会给View在输入法处理key事件之前先得到消息并优先处理,View系列控件可以直接复写onKeyPreIme( 不支持触摸事件)。

ImeInputStage:
分发InputEvent到IME处理调用 ImeInputStage 的onProcess,InputMethodManager 的dispatchInputEvent 方法处理消息(不支持触摸事件)。

EarlyPostImeInputStage:
与touchmode相关,比如你的手机有方向键,按方向键会退出touchmode,这个事件被消费,有可能会有view的背景变化,但不确定(支持触摸事件)。

NativePostImeInputStage:
分发InputEvent事件到NativeActivity,IME处理完消息后能先于普通Activity处理消息(此时支持触摸事件)。

ViewPostImeInputStage:
分发InputEvent事件到View框架,view的事件分发(支持触摸事件)。

SyntheticInputStage:
未处理的 InputEvent 最后的综合处理。

  这里可以看到有一个 ViewPostImeInputStage,这个就是我们通常所说的“事件分发”的入口,当然通过前面的知识我们可以知道,那只是 UI 层级的事件分发,它的分发对象是 View ,后面我们会详细分析它。
  同时,我们也能明白一个道理,如果一个 InputEvent在到达 ViewPostImeInputStage之前就被处理了,那么 View 层级上的那套事件分发,当然也就无从谈起了。

final class ViewPostImeInputStage extends InputStage {
   public ViewPostImeInputStage(InputStage next) {
       super(next);
   }

   @Override
   protected int onProcess(QueuedInputEvent q) {
       if (q.mEvent instanceof KeyEvent) {
           return processKeyEvent(q);
       } else {
           final int source = q.mEvent.getSource();
           if ((source & InputDevice.SOURCE_CLASS_POINTER) != 0) {
               // 就是这里了
               return processPointerEvent(q);
           } else if ((source & InputDevice.SOURCE_CLASS_TRACKBALL) != 0) {
               return processTrackballEvent(q);
           } else {
               return processGenericMotionEvent(q);
           }
       }
   }
}

组装责任链

  前面讲了责任链里面有哪些 InputStage 节点,那么这些节点又是在哪个时机,如何组装起来的呢?答案还是ViewRootImpl.setView 方法,这个方法出现了很多次了,重要性可想而知,我们整体来看一下它的逻辑:

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;
     // ...
  }
}

ViewPostImeInputStage

  这是 View 层级的事件分发体系的入口,但是别急,它不会直接进入到我们一般熟知的三件套方法:dispatchTouchEvent、onInterceptTouchEvent、onTouchEvent,中间还有再绕一圈。

final class ViewPostImeInputStage extends InputStage {

  private int processPointerEvent(QueuedInputEvent q) {
    final MotionEvent event = (MotionEvent)q.mEvent;

    mAttachInfo.mUnbufferedDispatchRequested = false;
    mAttachInfo.mHandlingPointerEvent = true;
    // 看起来直接到View的事件分发三件套了?其实还没有
    boolean handled = mView.dispatchPointerEvent(event);
    maybeUpdatePointerIcon(event);
    maybeUpdateTooltip(event);
    mAttachInfo.mHandlingPointerEvent = false;
    if (mAttachInfo.mUnbufferedDispatchRequested && !mUnbufferedInputDispatch) {
        mUnbufferedInputDispatch = true;
        if (mConsumeBatchedInputScheduled) {
            scheduleConsumeBatchedInputImmediately();
        }
    }
    return handled ? FINISH_HANDLED : FORWARD;
  }
}

DecorView

  代码里面的 mView.dispatchPointerEvent,这个mView我们跟一下就可以知道它是 decorView,也就是 View 树的根节点。
  调用了此方法之后,并不会立即进入 View 层级的事件分发,它会将事件传给 Activity,然后 Activity将事件传给 WindowPhoneWindow),然后 Window再将事件分发给 decorView(调用其 superDispatchTouchEvent方法,其实也就是 dispatchTouchEvent),由此才真正进入 View 层级的事件分发逻辑。
  为什么要这样绕一圈呢?这里我觉得 反思 | Android 事件分发机制的设计与实现 【掘金】里说的非常好,直接引用过来:

事件绕了一个圈子最终回到了DecorView这里,对于初次阅读这段源码的读者来说,这里的设计平淡无奇,似乎说它莫名其妙也不过分。事实上这里是 面向对象程序设计 中灵活运用 多态 这一特征的有力体现——对于DecorView而言,它承担了2个职责:
1.在接收到输入事件时,DecorView不同于其它View,它需要先将事件转发给最外层的 Activity,使得开发者可以通过重写Activity.onTouchEvent() 函数以达到对当前屏幕触摸事件拦截控制的目的,这里DecorView履行了自身(根节点)特殊的职责;
2.从Window接收到事件时,作为View树的根节点,将事件分发给子View,这里DecorView履行了一个普通的View的职责。

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

四、总结

  这篇文章基本将Android 的 Window 体系和 一个事件从 native 到 java 层的传递流程讲了一遍,当然中间很多很多概念和实现都没有深入,事实上目前我也没有能力去深入搞懂整个流程中的每一个环节,所以也只能用这种“不求甚解”的方式,先梳理完成整个事件分发的结构了。
  这样梳理下来,其实要理解 Android的事件分发体系,衔接 Window 和 View 的 ViewRootImpl 是关键中的关键,一定要充分的去理解它的作用。
  最后其实没有讲 View 层的事件分发体系了,因为在另外一篇文章《Android View 层级的事件分发体系——源码探究》有细致梳理过一遍,那篇文章虽然写的是比较烂的,但是也是跟着 《Android 开发艺术探索》这本书的内容走的,此书对 View 层的事件分发差不多讲到极致了。
  最后,本文一开始列出来的参考文章,每一篇都是挺值得一看的,本文也要很多内容是来自于这些文章,如果有原作者觉得侵权,可以联系笔者删除。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值