反思 系列博客是個人一種新學習方式的嘗試,該系列起源和目錄請參考 這裏 。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。
若是您以爲文章還差了那麼點東西,也請經過關注督促我寫出更好的文章——萬一哪天我進步了呢?