反思|Android 事件分發機制的設計與實現

反思 系列博客是個人一種新學習方式的嘗試,該系列起源和目錄請參考 這裏html

概述

Android體系自己很是宏大,源碼中值得思考和借鑑之處衆多。以總體事件分發機制爲例,其整個流程涉及到了 系統啓動流程SystemServer)、輸入管理(InputManager)、系統服務和UI的通訊ViewRootImpl + Window + WindowManagerService)、事件分發 等等一系列的環節。java

對於 事件分發 環節而言,不能否認很是重要,但Android系統完整的 事件分發機制 也是一名優秀Android工做者須要去了解的,本文筆者將針對Android 事件分發機制及設計思路 進行描述,其總體結構以下圖:android

總體思路

1.架構設計

Android系統中將輸入事件定義爲InputEvent,而InputEvent根據輸入事件的類型又分爲了KeyEventMotionEvent,前者對應鍵盤事件,後者則對應屏幕觸摸事件,這些事件統一由系統輸入管理器InputManager進行分發。git

在系統啓動的時候,SystemServer會啓動窗口管理服務WindowManagerServiceWindowManagerService在啓動的時候就會經過啓動系統輸入管理器InputManager來負責監控鍵盤消息。github

InputManager負責從硬件接收輸入事件,並將事件分發給當前激活的窗口(Window)處理,這裏咱們將前者理解爲 系統服務,將後者理解爲應用層級的 UI, 所以須要有一箇中介負責 服務UI 之間的通訊,因而ViewRootImpl類應運而生。算法

2.創建通訊

ActivityThread負責控制Activity的啓動過程,在performLaunchActivity()流程中,ActivityThread會針對Activity建立對應的PhoneWindowDecorView實例,而以後的handleResumeActivity()流程中則會將PhoneWindow應用 )和InputManagerService( 系統服務 )通訊以創建對應的鏈接,保證UI可見並可以對輸入事件進行正確的分發,這以後Activity就會成爲可見的。架構

如何在應用程序和系統服務之間創建通訊?AndroidWindowInputManagerService之間的通訊實際上使用的InputChannel,InputChannel是一個pipe,底層實際是經過socket進行通訊:app

ActivityThreadhandleResumeActivity()流程中, 會經過WindowManagerImpl.addView()爲當前的Window建立一個ViewRootImpl實例,當InputManager監控到硬件層級的輸入事件時,會通知ViewRootImpl對輸入事件進行底層的事件分發。socket

3.事件分發

View佈局流程測量流程 相同,Android事件分發機制也使用了 遞歸 的思想,由於一個事件最多隻有一個消費者,因此經過責任鏈的方式將事件自頂向下進行傳遞,找到事件的消費者(這裏是指一個View)以後,再自底向上返回結果。ide

讀到這裏,讀者應該以爲很是熟悉了,但實際上這裏描述的事件分發流程爲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樹正常運做的動力所在,控件的測量、佈局、繪製以及輸入事件的分發都由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的雙重職責 小節)。

所以,對於事件分發總體流程,咱們能夠進行以下定義:

  • 一、ViewGroup將事件分發給子View,當子ViewViewGroup中接收到事件,若其有child,則經過dispatchTouchEvent(event)再將事件分發給child...以此類推,直至將事件分發到底部的View,這也是事件分發的 遞流程
  • 二、底部的View接收到事件時,經過View自身的dispatchTouchEvent(event)函數判斷是否消費事件:
  • 2.1 若消費事件,則將結果做爲true向上層的ViewGroup返回,ViewGroup接收到true,意味着事件已經被消費,所以跳過了是否要消費該事件的判斷,直接向上一級繼續返回true,以此類推直到將true結果通知到最上層的View節點;
  • 2.2 若不消費事件,則向上層返回falseViewGroup接收到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層級的事件傳遞的真正實現雖然複雜,但其本質卻和上述代碼並不不一樣,理解了這個基本的流程,接下來對於額外功能擴展的設計與實現也只是時間問題了。

2.事件序列與分發鏈

在上一小節中,讀者已經瞭解事件分發的本質原理就是遞歸,而目前其實現方式是,每接收一個新的事件,都須要進行一次遞歸才能找到對應消費事件的View,並依次向上返回事件分發的結果。

每一個事件都對View樹進行一次遍歷遞歸?這對性能的影響顯而易見,所以這種設計是有改進空間的。

如何針對這個問題進行改進?首先,設計者根據用戶的行爲對MotionEvent中添加了一個Action的屬性以描述該事件的行爲:

  • ACTION_DOWN:手指觸摸到屏幕的行爲
  • ACTION_MOVE:手指在屏幕上移動的行爲
  • ACTION_UP:手指離開屏幕的行爲
  • ...其它Action,好比ACTION_CANCEL...

定義了這些行爲的同時,設計者定義了一個叫作 事件序列 的概念:針對用戶的一次觸摸操做,必然對應了一個 事件序列,從用戶手指接觸屏幕,到移動手指,再到擡起手指 ——單個事件序列必然包含ACTION_DOWNACTION_MOVE ... ACTION_MOVEACTION_UP 等多個事件,這其中ACTION_MOVE的數量不肯定,ACTION_DOWNACTION_UP的數量則爲1。

定義了 事件序列 的概念,設計者就能夠着手對現有代碼進行設計和改進,其思路以下:當接收到一個ACTION_DOWN時,意味着一次完整事件序列的開始,經過遞歸遍歷找到View樹中真正對事件進行消費的Child,並將其進行保存,這以後接收到ACTION_MOVEACTION_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_MOVEACTION_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;
}
複製代碼

至此,本小節一開始提到的問題獲得瞭解決。

3.事件攔截機制

讀者應該都有了解,爲了增長 事件分發 過程當中的靈活性,AndroidViewGroup層級設計了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事件分發機制嗎?

固然不能,即便是筆者對此也只是初窺門徑而已,在撰寫本文的過程當中,筆者參考了許多優秀的學習資料,一樣筆者也不認爲本文比這些資料講解的更透徹,讀者能夠參考這些資料 ——一千我的有一千個哈姆雷特,也許這些優秀的資料相比本文更適合你呢?

  • 1.Android源碼

源碼永遠是學習過程當中最好的老師,RTFSC。

神書,書中 View的事件分發機制 一節將源碼分析到了極致,講解的很是透徹,強烈建議 建議讀者源碼閱讀時參考這本書。

framework層原理分析的神文,懂得天然懂。本文中的部分圖片也引自該文。

很是好的博客系列。

ViewRootImpl講解很是透徹的一篇博客,本文對於ViewRootImpl的主要職責的描述也是參考了此文。

很是欣賞 @KunMinX 老師博文的風格,大道至簡,此文對事件消費過程當中的 消費 二字的講解很是透徹,給予了筆者不少啓示——另,本文不是黑車(笑)。


關於我

Hello,我是 卻把清梅嗅 ,若是您以爲文章對您有價值,歡迎 ❤️,也歡迎關注個人 博客 或者 Github

若是您以爲文章還差了那麼點東西,也請經過關注督促我寫出更好的文章——萬一哪天我進步了呢?

相關文章
相關標籤/搜索