不管工作幾年的 Android 工程師,或多或少都聽說過 Window 的概念,並且隱隱約約感覺它在 Activity 與 View 之間應該發揮著某種連線的作用。但是如果需要說出這 3 者之間的關係,多數工程師不知道從何下手。
Activity 的 setContentView
Activity 是 Android 開發人員使用最頻繁的 API 之一,最初在接觸 Android 開發時,我始終認為它就是負責將 layout 佈局中的控制元件渲染繪製出來的。原因很簡單,每當我們想顯示一個新的介面時,都是通過 start 一個新的 Activity 方式;對於想顯示的內容或者佈局,也只需要在 Activity 中新增一行 setContentView 即可,剩下的 Activity 都自動幫我們搞定。但是我們從來沒有去建立一個 Window 來繫結 UI 或者 View 元素。
直到我點開 setContentView 原始碼的那一刻:
public void setContentView(@LayoutRes int layoutResID) { getWindow().setContentView(layoutResID); initWindowDecorActionBar(); }
public Window getWindow() { return mWindow; }
顯然 Activity 幾乎什麼都沒做,將操作直接交給了一個 Window 來處理。getWindow 返回的是 Activity 中的全域性變數 mWindow,它是 Window 視窗型別。那麼它是什麼時候賦值的呢?
記得上篇文章中分析 startActivity 的過程,最終程式碼會呼叫到 ActivityThread 中的 performLaunchActivity 方法,通過反射建立 Activity 物件,並執行其 attach 方法。Window 就是在這個方法中被建立,詳細程式碼如下:
@UnsupportedAppUsage 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, Window window, ActivityConfigCallback activityConfigCallback, IBinder assistToken) { attachBaseContext(context); mFragments.attachHost(null /*parent*/); mWindow = new PhoneWindow(this, window, activityConfigCallback); mWindow.setWindowControllerCallback(this); mWindow.setCallback(this); mWindow.setOnWindowDismissedCallback(this); mWindow.getLayoutInflater().setPrivateFactory(this); if (info.softInputMode != WindowManager.LayoutParams.SOFT_INPUT_STATE_UNSPECIFIED) { mWindow.setSoftInputMode(info.softInputMode); } if (info.uiOptions != 0) { mWindow.setUiOptions(info.uiOptions); } mUiThread = Thread.currentThread(); mMainThread = aThread; ... mWindow.setWindowManager( (WindowManager)context.getSystemService(Context.WINDOW_SERVICE), mToken, mComponent.flattenToString(), (info.flags & ActivityInfo.FLAG_HARDWARE_ACCELERATED) != 0); if (mParent != null) { mWindow.setContainer(mParent.getWindow()); } mWindowManager = mWindow.getWindowManager(); mCurrentConfig = config;
在 Activity 的 attach 方法中將 mWindow 賦值給一個 PhoneWindow 物件,實際上整個 Android 系統中 Window 只有一個實現類,就是 PhoneWindow。
接下來呼叫 setWindowManager 方法,將系統 WindowManager 傳給 PhoneWindow,如下所示:
public void setWindowManager(WindowManager wm, IBinder appToken, String appName, boolean hardwareAccelerated) { mAppToken = appToken; mAppName = appName; mHardwareAccelerated = hardwareAccelerated; if (wm == null) { wm = (WindowManager)mContext.getSystemService(Context.WINDOW_SERVICE); } mWindowManager = ((WindowManagerImpl)wm).createLocalWindowManager(this); }
public WindowManagerImpl createLocalWindowManager(Window parentWindow) { return new WindowManagerImpl(mContext, parentWindow); }
最終,在 PhoneWindow 中持有了一個 WindowManagerImpl 的引用。
PhoneWindow 的 setContentView
Activity 將 setContentView 的操作交給了 PhoneWindow,看下PhoneWindow的setContentView方法實現過程:
@Override public void setContentView(int layoutResID) { // Note: FEATURE_CONTENT_TRANSITIONS may be set in the process of installing the window // decor, when theme attributes and the like are crystalized. Do not check the feature // before this happens. if (mContentParent == null) { installDecor(); } else if (!hasFeature(FEATURE_CONTENT_TRANSITIONS)) { mContentParent.removeAllViews(); } if (hasFeature(FEATURE_CONTENT_TRANSITIONS)) { final Scene newScene = Scene.getSceneForLayout(mContentParent, layoutResID, getContext()); transitionTo(newScene); } else { mLayoutInflater.inflate(layoutResID, mContentParent); } mContentParent.requestApplyInsets(); final Callback cb = getCallback(); if (cb != null && !isDestroyed()) { cb.onContentChanged(); } mContentParentExplicitlySet = true; }
解釋說明:
- 標紅的1 處呼叫如果 mContentParent 為 null,則呼叫 installDecor 初始化 DecorView 和 mContentParent。
- 標紅的2處將我們呼叫 setContentView 傳入的佈局新增到 mContentParent 中。
可以看出在 PhoneWindow 中預設有一個 DecorView(實際上是一個 FrameLayout),在 DecorView 中預設自帶一個 mContentParent(實際上是一個 ViewGroup)。我們自己實現的佈局是被新增到 mContentParent 中的,因此經過 setContentView 之後,PhoneWindow 內部的 View 關係如下所示:
目前為止 PhoneWindow 中只是建立出了一個 DecorView,並在 DecorView 中填充了我們在 Activity 中傳入的 layoutId 佈局,可是 DecorView 還沒有跟 Activity 建立任何聯絡,也沒有被繪製到介面上顯示。那 DecorView 是何時被繪製到螢幕上的呢?
剛接觸 Android,學習生命週期時,經常會看到相關文件介紹 Activity 執行到 onCreate 時並不可見,只有執行完 onResume 之後 Activity 中的內容才是螢幕可見狀態。造成這種現象的原因就是,onCreate 階段只是初始化了 Activity 需要顯示的內容,而在 onResume 階段才會將 PhoneWindow 中的 DecorView 真正的繪製到螢幕上。
在 ActivityThread 的 handleResumeActivity 中,會呼叫 WindowManager 的 addView 方法將 DecorView 新增到 WMS(WindowManagerService) 上,如下所示:
@Override public void handleResumeActivity(IBinder token, boolean finalStateRequest, boolean isForward, String reason) { ... if (a.mVisibleFromClient) { if (!a.mWindowAdded) { a.mWindowAdded = true; wm.addView(decor, l); } else { // The activity will get a callback for this {@link LayoutParams} change // earlier. However, at that time the decor will not be set (this is set // in this method), so no action will be taken. This call ensures the // callback occurs with the decor set. a.onWindowAttributesChanged(l); } }
WindowManger 的 addView 結果有兩個:
- DecorView 被渲染繪製到螢幕上顯示;
- DecorView 可以接收螢幕觸控事件。
WindowManager 的 addView
PhoneWindow 只是負責處理一些應用視窗通用的邏輯(設定標題欄,導航欄等)。但是真正完成把一個 View 作為視窗新增到 WMS 的過程是由 WindowManager 來完成的。
WindowManager 是介面型別,它真正的實現者是 WindowManagerImpl 類,看一下它的 addView 方法如下:
public void addView(View view, ViewGroup.LayoutParams params, Display display, Window parentWindow) { if (view == null) { throw new IllegalArgumentException("view must not be null"); } if (display == null) { throw new IllegalArgumentException("display must not be null"); } if (!(params instanceof WindowManager.LayoutParams)) { throw new IllegalArgumentException("Params must be WindowManager.LayoutParams"); } ... 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. if (index >= 0) { removeViewLocked(index, true); } throw e; }
WindowManagerImpl 也是一個空殼,它呼叫了 WindowManagerGlobal 的 addView 方法。
WindowMangerGlobal 是一個單例,每一個程式中只有一個例項物件。如上圖紅框中所示,在其 addView 方法中,建立了一個最關鍵的 ViewRootImpl 物件,然後通過 ViewRootImpl 的 setView 方法將 view 新增到 WMS 中。
ViewRootImpl 的 setView
/** * We have one child */ public void setView(View view, WindowManager.LayoutParams attrs, View panelParentView) { synchronized (this) { if (mView == null) { mView = view; mAttachInfo.mDisplayState = mDisplay.getState(); mDisplayManager.registerDisplayListener(mDisplayListener, mHandler); mViewLayoutDirectionInitial = mView.getRawLayoutDirection(); mFallbackEventHandler.setView(view); mWindowAttributes.copyFrom(attrs); if (mWindowAttributes.packageName == null) { mWindowAttributes.packageName = mBasePackageName; } attrs = mWindowAttributes; setTag(); ... mAdded = true; int res; /* = WindowManagerImpl.ADD_OKAY; */ // Schedule the first layout -before- adding to the window // manager, to make sure we do the relayout before receiving // any other events from the system. requestLayout(); if ((mWindowAttributes.inputFeatures & WindowManager.LayoutParams.INPUT_FEATURE_NO_INPUT_CHANNEL) == 0) { mInputChannel = new InputChannel(); } mForceDecorViewVisibility = (mWindowAttributes.privateFlags & PRIVATE_FLAG_FORCE_DECOR_VIEW_VISIBILITY) != 0; try { mOrigWindowType = mWindowAttributes.type; mAttachInfo.mRecomputeGlobalAttributes = true; collectViewAttributes(); res = mWindowSession.addToDisplay(mWindow, mSeq, mWindowAttributes, getHostVisibility(), mDisplay.getDisplayId(), mTmpFrame, mAttachInfo.mContentInsets, mAttachInfo.mStableInsets, mAttachInfo.mOutsets, mAttachInfo.mDisplayCutout, mInputChannel, mTempInsets); setFrame(mTmpFrame); } catch (RemoteException e) { mAdded = false; mView = null; mAttachInfo.mRootView = null; mInputChannel = null; mFallbackEventHandler.setView(null); unscheduleTraversals(); setAccessibilityFocus(null, null); throw new RuntimeException("Adding window failed", e); } finally { if (restore) { attrs.restore(); } } ... } } }
解釋說明:
- 標紅的 1 處的 requestLayout 是重新整理佈局的操作,呼叫此方法後 ViewRootImpl 所關聯的 View 也執行 measure - layout - draw 操作,確保在 View 被新增到 Window 上顯示到螢幕之前,已經完成測量和繪製操作。
- 標紅的2 處呼叫 mWindowSession 的 addToDisplay 方法將 View 新增到 WMS 中。
WindowSession 是 WindowManagerGlobal 中的單例物件,初始化程式碼如下:
@UnsupportedAppUsage public static IWindowSession getWindowSession() { synchronized (WindowManagerGlobal.class) { if (sWindowSession == null) { try { // Emulate the legacy behavior. The global instance of InputMethodManager // was instantiated here. // TODO(b/116157766): Remove this hack after cleaning up @UnsupportedAppUsage InputMethodManager.ensureDefaultInstanceForDefaultDisplayIfNecessary(); IWindowManager windowManager = getWindowManagerService(); sWindowSession = windowManager.openSession( new IWindowSessionCallback.Stub() { @Override public void onAnimatorScaleChanged(float scale) { ValueAnimator.setDurationScale(scale); } }); } catch (RemoteException e) { throw e.rethrowFromSystemServer(); } } return sWindowSession; } }
sWindowSession 實際上是 IWindowSession 型別,是一個 Binder 型別,真正的實現類是 System 程式中的 Session。上面程式碼中標紅的就是用 AIDL 獲取 System 程式中 Session 的物件。其 addToDisplay 方法如下:
圖中的 mService 就是 WMS。至此,Window 已經成功的被傳遞給了 WMS。剩下的工作就全部轉移到系統程式中的 WMS 來完成最終的新增操作。
再看 Activity
之前提到 addView 成功有一個標誌就是能夠接收觸屏事件,通過對 setContentView 流程的分析,可以看出新增 View 的操作實質上是 PhoneWindow 在全盤操作,背後負責人是 WMS,反之 Activity 自始至終沒什麼參與感。但是我們也知道當觸屏事件發生之後,Touch 事件首先是被傳入到 Activity,然後才被下發到佈局中的 ViewGroup 或者 View。那麼 Touch 事件是如何傳遞到 Activity 上的呢?
ViewRootImpl 中的 setView 方法中,除了呼叫 IWindowSession 執行跨程式新增 View 之外,還有一項重要的操作就是設定輸入事件的處理:
// Set up the input pipeline. CharSequence counterSuffix = attrs.getTitle(); 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);
如上所示,設定了一系列的輸入通道。一個觸屏事件的發生是由螢幕發起,然後經過驅動層一系列的優化計算通過 Socket 跨程式通知 Android Framework 層(實際上就是 WMS),最終螢幕的觸控事件會被髮送到上圖中的輸入管道中。
這些輸入管道實際上是一個連結串列結構,當某一個螢幕觸控事件到達其中的 ViewPostImeInputState 時,會經過 onProcess 來處理,如下所示:
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); } } }
private int processPointerEvent(QueuedInputEvent q) { final MotionEvent event = (MotionEvent)q.mEvent; mAttachInfo.mUnbufferedDispatchRequested = false; mAttachInfo.mHandlingPointerEvent = true; 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; }
可以看到在 onProcess 中最終呼叫了一個 mView的dispatchPointerEvent 方法,mView 實際上就是 PhoneWindow 中的 DecorView,而 dispatchPointerEvent 是被 View.java 實現的,如下View中dispatchPointerEvent方法所示:
@UnsupportedAppUsage public final boolean dispatchPointerEvent(MotionEvent event) { if (event.isTouchEvent()) { return dispatchTouchEvent(event); } else { return dispatchGenericMotionEvent(event); } }
DecorView中的dispatchTouchEvent方法程式碼:
@Override public boolean dispatchTouchEvent(MotionEvent ev) { final Window.Callback cb = mWindow.getCallback(); return cb != null && !mWindow.isDestroyed() && mFeatureId < 0 ? cb.dispatchTouchEvent(ev) : super.dispatchTouchEvent(ev); }
最終呼叫了 PhoneWindow 中 Callback的dispatchTouchEvent 方法,那這個 Callback 是不是 Activity 呢?
在啟動 Activity 階段,建立 Activity 物件並呼叫 attach 方法時,有如下一段程式碼:
@UnsupportedAppUsage 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, Window window, ActivityConfigCallback activityConfigCallback, IBinder assistToken) { attachBaseContext(context); mFragments.attachHost(null /*parent*/); mWindow = new PhoneWindow(this, window, activityConfigCallback); mWindow.setWindowControllerCallback(this); mWindow.setCallback(this); mWindow.setOnWindowDismissedCallback(this); mWindow.getLayoutInflater().setPrivateFactory(this);
果然將 Activity 自身傳遞給了 PhoneWindow,再接著看 Activity的dispatchTouchEvent 方法:
public boolean dispatchTouchEvent(MotionEvent ev) { if (ev.getAction() == MotionEvent.ACTION_DOWN) { onUserInteraction(); } if (getWindow().superDispatchTouchEvent(ev)) { return true; } return onTouchEvent(ev); }
PhoneWindow中
@Override public boolean superDispatchTouchEvent(MotionEvent event) { return mDecor.superDispatchTouchEvent(event); }
Touch 事件在 Activity 中只是繞了一圈最後還是回到了 PhoneWindow 中的 DecorView 來處理。剩下的就是從 DecorView 開始將事件層層傳遞給內部的子 View 中了。
總結
文中主要通過 setContentView 的流程,分析了 Activity、Window、View 之間的關係。整個過程 Activity 表面上參與度比較低,大部分 View 的新增操作都被封裝到 Window 中實現。而 Activity 就相當於 Android 提供給開發人員的一個管理類,通過它能夠更簡單的實現 Window 和 View 的操作邏輯。
最後再簡單列一下整個流程需要注意的點:
1》一個 Activity 中有一個 window,也就是 PhoneWindow 物件,在 PhoneWindow 中有一個 DecorView,在 setContentView 中會將 layout 填充到此 DecorView 中。
2》一個應用程式中只有一個 WindowManagerGlobal 物件,因為在 ViewRootImpl 中它是 static 靜態型別。
3》每一個 PhoneWindow 對應一個 ViewRootImple 物件。
4》WindowMangerGlobal 通過呼叫 ViewRootImpl 的 setView 方法,完成 window 的新增過程。
5》ViewRootImpl 的 setView 方法中主要完成兩件事情:View 渲染(requestLayout)以及接收觸屏事件。
————來自拉勾教育筆記