前言
很久沒有發表文章了,今天來一篇,大家撒花~~~
本文打算分析下Android中點選事件的來源,順便提及下ViewRootImpl。
Android中點選事件的來源
這個問題,也許你會說“這還用你說嗎?我可是看過藝術探索的人”,我知道藝術探索中的確是詳細介紹了點選事件的傳遞流程,反正大致就是點選事件從Activity傳遞給PhoneWindow,然後PhoneWindow再傳遞給DecorView,接著DecorView就進行後續的遍歷式的傳遞。這都沒錯,但是點選事件是誰傳遞給Activity的呢?這個大家可能不清楚吧?那本文就是分析這個問題的。
首先看Activity的實現,如下,Activity實現了一個特殊的介面:Window.Callback。
1 2 3 4 5 6 7 |
public class Activity extends ContextThemeWrapper implements LayoutInflater.Factory2, Window.Callback, KeyEvent.Callback, OnCreateContextMenuListener, ComponentCallbacks2, Window.OnWindowDismissedCallback { private static final String TAG = "Activity"; private static final boolean DEBUG_LIFECYCLE = false; |
那麼Window.Callback到底是什麼東西呢?如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 |
/** * API from a Window back to its caller. This allows the client to * intercept key dispatching, panels and menus, etc. */ public interface Callback { /** * Called to process key events. At the very least your * implementation must call * {<a href="http://www.jobbole.com/members/57845349" data-mce-href="http://www.jobbole.com/members/57845349">@link</a> android.view.Window#superDispatchKeyEvent} to do the * standard key processing. * * @param event The key event. * * @return boolean Return true if this event was consumed. */ public boolean dispatchKeyEvent(KeyEvent event); /** * Called to process touch screen events. At the very least your * implementation must call * {<a href="http://www.jobbole.com/members/57845349" data-mce-href="http://www.jobbole.com/members/57845349">@link</a> android.view.Window#superDispatchTouchEvent} to do the * standard touch screen processing. * * @param event The touch screen event. * * @return boolean Return true if this event was consumed. */ public boolean dispatchTouchEvent(MotionEvent event); /** * Called to process trackball events. At the very least your * implementation must call * {<a href="http://www.jobbole.com/members/57845349" data-mce-href="http://www.jobbole.com/members/57845349">@link</a> android.view.Window#superDispatchTrackballEvent} to do the * standard trackball processing. * * @param event The trackball event. * * @return boolean Return true if this event was consumed. */ public boolean dispatchTrackballEvent(MotionEvent event); ...(省略若干程式碼,下同) |
然後我們似乎看出了一些端倪,難道這個介面和點選事件的傳遞有關?嗯,你猜對了。在藝術探索這本書中,並沒有描述事件是如何傳遞給Activity的,但是這裡我們可以猜測,如果外界想要傳遞點選事件給Activity,那麼它就必須持有Activity的引用,這沒錯,在Activity的attach方法中,有如下一段:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
final void attach(Context context, ActivityThread aThread, Instrumentation instr, IBinder token, int ident, Application application, Intent intent, ActivityInfo info, CharSequence title, Activity parent, String id, NonConfigurationInstances lastNonConfigurationInstances, Configuration config, String referrer, IVoiceInteractor voiceInteractor) { attachBaseContext(context); mFragments.attachHost(null /*parent*/); mWindow = new PhoneWindow(this); mWindow.setCallback(this); mWindow.setOnWindowDismissedCallback(this); mWindow.getLayoutInflater().setPrivateFactory(this); |
顯然,mWindow持有了Activity的引用,它通過setCallback方法來持有Activity,因此,事件是從Window傳遞給了Activity。也許你會說:“我不信,這理由不充分!”,沒關係,我們繼續分析。
我們知道,Activity啟動以後,在它的onResume以後,DecorView才開始attach給WindowManager從而顯示出來。(什麼?你不知道?回去看藝術探索第8章),請看Activity的makeVisible方法,程式碼如下:
1 2 3 4 5 6 7 8 |
void makeVisible() { if (!mWindowAdded) { ViewManager wm = getWindowManager(); wm.addView(mDecor, getWindow().getAttributes()); mWindowAdded = true; } mDecor.setVisibility(View.VISIBLE); } |
接著,系統就會完成新增Window的過程,看WindowManagerGlobal的addView方法,如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 |
public void addView(View view, ViewGroup.LayoutParams params, Display display, Window parentWindow) { ViewRootImpl root; View panelParentView = null; ...這裡省略了一堆程式碼 root = new ViewRootImpl(view.getContext(), display); view.setLayoutParams(wparams); mViews.add(view); mRoots.add(root); mParams.add(wparams); // do this last because it fires off messages to start doing things try { root.setView(view, wparams, panelParentView); } catch (RuntimeException e) { // BadTokenException or InvalidDisplayException, clean up. synchronized (mLock) { final int index = findViewLocked(view, false); if (index >= 0) { removeViewLocked(index, true); } } throw e; } } |
可以看到,ViewRootImpl建立了,在ViewRootImpl的setView方法(此方法執行在UI執行緒)中,會通過跨程式的方式向WMS(WindowManagerService)發起一個呼叫,從而將DecorView最終新增到Window上,在這個過程中,ViewRootImpl、DecorView和WMS會彼此向關聯,同時會建立InputChannel、InputQueue和WindowInputEventReceiver來接受點選事件的訊息。
好了,言歸正傳,下面來說,點選事件到底怎麼傳遞給Activity的。首先要明白,點選事件是由使用者的觸控行為所產生的,因此它必須要通過硬體來捕獲,然後點選事件會交給WMS來處理。
在ViewRootImpl中,有一個方法,叫做dispatchInputEvent,如下:
1 2 3 4 5 6 7 8 |
public void dispatchInputEvent(InputEvent event, InputEventReceiver receiver) { SomeArgs args = SomeArgs.obtain(); args.arg1 = event; args.arg2 = receiver; Message msg = mHandler.obtainMessage(MSG_DISPATCH_INPUT_EVENT, args); msg.setAsynchronous(true); mHandler.sendMessage(msg); } |
那麼什麼是InputEvent呢?InputEvent有2個子類:KeyEvent和MotionEvent,其中KeyEvent表示鍵盤事件,而MotionEvent表示點選事件。在上面的程式碼中,mHandler是一個在UI執行緒建立的Handder,所以它會把執行邏輯切換到UI執行緒中。
1 |
final ViewRootHandler mHandler = new ViewRootHandler(); |
這個訊息的處理如下:
1 2 3 4 5 6 7 |
case MSG_DISPATCH_INPUT_EVENT: { SomeArgs args = (SomeArgs)msg.obj; InputEvent event = (InputEvent)args.arg1; InputEventReceiver receiver = (InputEventReceiver)args.arg2; enqueueInputEvent(event, receiver, 0, true); args.recycle(); } |
除此之外,WindowInputEventReceiver也可以來接收點選事件的訊息,同樣它也有一個dispatchInputEvent方法,注意,WindowInputEventReceiver中的Looper為UI執行緒的Looper。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
mInputEventReceiver = new WindowInputEventReceiver(mInputChannel, Looper.myLooper()); // Called from native code. @SuppressWarnings("unused") private void dispatchInputEvent(int seq, InputEvent event) { mSeqMap.put(event.getSequenceNumber(), seq); onInputEvent(event); } @Override public void onInputEvent(InputEvent event) { enqueueInputEvent(event, this, 0, true); } |
可以發現,不管是ViewRootImpl的dispatchInputEvent方法,還是WindowInputEventReceiver的dispatchInputEvent方法,它們本質上都是呼叫deliverInputEvent方法來處理點選事件的訊息,如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
private void deliverInputEvent(QueuedInputEvent q) { Trace.asyncTraceBegin(Trace.TRACE_TAG_VIEW, "deliverInputEvent", q.mEvent.getSequenceNumber()); if (mInputEventConsistencyVerifier != null) { mInputEventConsistencyVerifier.onInputEvent(q.mEvent, 0); } InputStage stage; if (q.shouldSendToSynthesizer()) { stage = mSyntheticInputStage; } else { stage = q.shouldSkipIme() ? mFirstPostImeInputStage : mFirstInputStage; } if (stage != null) { stage.deliver(q); } else { finishInputEvent(q); } } |
在ViewRootImpl中,有一系列類似於InputStage(輸入事件舞臺)的概念,每種InputStage可以處理一定的事件型別,比如AsyncInputStage、ViewPreImeInputStage、ViewPostImeInputStage等。當一個InputEvent到來時,ViewRootImpl會尋找合適它的InputStage來處理。對於點選事件來說,ViewPostImeInputStage可以處理它,ViewPostImeInputStage中,有一個processPointerEvent方法,如下,它會呼叫mView的dispatchPointerEvent方法,注意,這裡的mView其實就是DecorView。
1 2 3 4 5 6 7 8 9 10 11 12 13 |
private int processPointerEvent(QueuedInputEvent q) { final MotionEvent event = (MotionEvent)q.mEvent; mAttachInfo.mUnbufferedDispatchRequested = false; boolean handled = mView.dispatchPointerEvent(event); if (mAttachInfo.mUnbufferedDispatchRequested & !mUnbufferedInputDispatch) { mUnbufferedInputDispatch = true; if (mConsumeBatchedInputScheduled) { scheduleConsumeBatchedInputImmediately(); } } return handled ? FINISH_HANDLED : FORWARD; } |
在View的實現中,dispatchPointerEvent的邏輯如下,這樣一來,點選事件就傳遞給了DecorView的dispatchTouchEvent方法。
1 2 3 4 5 6 7 |
public final boolean dispatchPointerEvent(MotionEvent event) { if (event.isTouchEvent()) { return dispatchTouchEvent(event); } else { return dispatchGenericMotionEvent(event); } } |
DecorView的dispatchTouchEvent的實現如下,需要強調的是,DecorView是PhoneWindow的內部類,還記得前面提到的Window.Callback嗎?沒錯,在下面的程式碼中,這個cb物件其實就是Activity,就這樣點選事件就傳遞給了Activity了。
1 2 3 4 5 |
public boolean dispatchTouchEvent(MotionEvent ev) { final Callback cb = getCallback(); return cb != null & !isDestroyed() && mFeatureId 0 ? cb.dispatchTouchEvent(ev) : super.dispatchTouchEvent(ev); } |
例子
寫一個簡單的例子,驗證下。選擇一個View,重寫其onTouchEvent方法,然後通過dumpStack方法來列印出當前執行緒的呼叫棧資訊。
1 2 3 4 5 6 |
@Override public boolean onTouchEvent(MotionEvent event) { Log.d(TAG, "onTouchEvent, ev=" + event.getAction()); Thread.dumpStack(); return true; } |
選擇Google nexus 6執行一下,log如下所示:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 |
06-22 13:25:21.368 7365 7365 D FrameLayoutEx: onTouchEvent, ev=0 06-22 13:25:21.368 7365 7365 W System.err: java.lang.Throwable: stack dump 06-22 13:25:21.368 7365 7365 W System.err: at java.lang.Thread.dumpStack(Thread.java:490) 06-22 13:25:21.368 7365 7365 W System.err: at com.ryg.reveallayout.ui.FrameLayoutEx.onTouchEvent(FrameLayoutEx.java:27) 06-22 13:25:21.368 7365 7365 W System.err: at android.view.View.dispatchTouchEvent(View.java:9294) 06-22 13:25:21.368 7365 7365 W System.err: at android.view.ViewGroup.dispatchTransformedTouchEvent(ViewGroup.java:2547) 06-22 13:25:21.368 7365 7365 W System.err: at android.view.ViewGroup.dispatchTouchEvent(ViewGroup.java:2240) 06-22 13:25:21.368 7365 7365 W System.err: at android.view.ViewGroup.dispatchTransformedTouchEvent(ViewGroup.java:2553) 06-22 13:25:21.369 7365 7365 W System.err: at android.view.ViewGroup.dispatchTouchEvent(ViewGroup.java:2197) 06-22 13:25:21.369 7365 7365 W System.err: at android.view.ViewGroup.dispatchTransformedTouchEvent(ViewGroup.java:2553) 06-22 13:25:21.369 7365 7365 W System.err: at android.view.ViewGroup.dispatchTouchEvent(ViewGroup.java:2197) 06-22 13:25:21.369 7365 7365 W System.err: at android.view.ViewGroup.dispatchTransformedTouchEvent(ViewGroup.java:2553) 06-22 13:25:21.369 7365 7365 W System.err: at android.view.ViewGroup.dispatchTouchEvent(ViewGroup.java:2197) 06-22 13:25:21.369 7365 7365 W System.err: at com.android.internal.policy.PhoneWindow$DecorView.superDispatchTouchEvent(PhoneWindow.java:2403) 06-22 13:25:21.369 7365 7365 W System.err: at com.android.internal.policy.PhoneWindow.superDispatchTouchEvent(PhoneWindow.java:1737) 06-22 13:25:21.369 7365 7365 W System.err: at android.app.Activity.dispatchTouchEvent(Activity.java:2765) 06-22 13:25:21.369 7365 7365 W System.err: at com.android.internal.policy.PhoneWindow$DecorView.dispatchTouchEvent(PhoneWindow.java:2364) 06-22 13:25:21.369 7365 7365 W System.err: at android.view.View.dispatchPointerEvent(View.java:9514) 06-22 13:25:21.369 7365 7365 W System.err: at android.view.ViewRootImpl$ViewPostImeInputStage.processPointerEvent(ViewRootImpl.java:4230) 06-22 13:25:21.370 7365 7365 W System.err: at android.view.ViewRootImpl$ViewPostImeInputStage.onProcess(ViewRootImpl.java:4096) 06-22 13:25:21.370 7365 7365 W System.err: at android.view.ViewRootImpl$InputStage.deliver(ViewRootImpl.java:3642) 06-22 13:25:21.370 7365 7365 W System.err: at android.view.ViewRootImpl$InputStage.onDeliverToNext(ViewRootImpl.java:3695) 06-22 13:25:21.370 7365 7365 W System.err: at android.view.ViewRootImpl$InputStage.forward(ViewRootImpl.java:3661) 06-22 13:25:21.370 7365 7365 W System.err: at android.view.ViewRootImpl$AsyncInputStage.forward(ViewRootImpl.java:3787) 06-22 13:25:21.370 7365 7365 W System.err: at android.view.ViewRootImpl$InputStage.apply(ViewRootImpl.java:3669) 06-22 13:25:21.370 7365 7365 W System.err: at android.view.ViewRootImpl$AsyncInputStage.apply(ViewRootImpl.java:3844) 06-22 13:25:21.370 7365 7365 W System.err: at android.view.ViewRootImpl$InputStage.deliver(ViewRootImpl.java:3642) 06-22 13:25:21.370 7365 7365 W System.err: at android.view.ViewRootImpl$InputStage.onDeliverToNext(ViewRootImpl.java:3695) 06-22 13:25:21.370 7365 7365 W System.err: at android.view.ViewRootImpl$InputStage.forward(ViewRootImpl.java:3661) 06-22 13:25:21.370 7365 7365 W System.err: at android.view.ViewRootImpl$InputStage.apply(ViewRootImpl.java:3669) 06-22 13:25:21.370 7365 7365 W System.err: at android.view.ViewRootImpl$InputStage.deliver(ViewRootImpl.java:3642) 06-22 13:25:21.371 7365 7365 W System.err: at android.view.ViewRootImpl.deliverInputEvent(ViewRootImpl.java:5922) 06-22 13:25:21.371 7365 7365 W System.err: at android.view.ViewRootImpl.doProcessInputEvents(ViewRootImpl.java:5896) 06-22 13:25:21.371 7365 7365 W System.err: at android.view.ViewRootImpl.enqueueInputEvent(ViewRootImpl.java:5857) 06-22 13:25:21.371 7365 7365 W System.err: at android.view.ViewRootImpl$WindowInputEventReceiver.onInputEvent(ViewRootImpl.java:6025) 06-22 13:25:21.371 7365 7365 W System.err: at android.view.InputEventReceiver.dispatchInputEvent(InputEventReceiver.java:185) 06-22 13:25:21.371 7365 7365 W System.err: at android.os.MessageQueue.nativePollOnce(Native Method) 06-22 13:25:21.371 7365 7365 W System.err: at android.os.MessageQueue.next(MessageQueue.java:323) 06-22 13:25:21.371 7365 7365 W System.err: at android.os.Looper.loop(Looper.java:135) 06-22 13:25:21.371 7365 7365 W System.err: at android.app.ActivityThread.main(ActivityThread.java:5417) 06-22 13:25:21.371 7365 7365 W System.err: at java.lang.reflect.Method.invoke(Native Method) 06-22 13:25:21.371 7365 7365 W System.err: at com.android.internal.os.ZygoteInit$MethodAndArgsCaller.run(ZygoteInit.java:726) 06-22 13:25:21.371 7365 7365 W System.err: at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:616) |
通過上述log,大家不難看出MotionEvent的來源以及傳遞順序,本文止。
另外,就在今天,我的微信公眾號 “Android開發藝術探索” 竟然有打賞功能了,很新鮮,我打算體驗一把,自己給自己打賞一下,大家就不要給我打賞了。