Android DecorView 與 Activity 繫結原理分析
一年多以前,曾經以為自己對 View 的新增顯示邏輯已經有所瞭解了,事後發現也只是懂了些皮毛而已。經過一年多的實戰,Android 和 Java 基礎都有了提升,是時候該去看看 DecorView 的新增顯示。
概論
Android 中 Activity 是作為應用程式的載體存在,代表著一個完整的使用者介面,提供了一個視窗來繪製各種檢視,當 Activity 啟動時,我們會通過 setContentView 方法來設定一個內容檢視,這個內容檢視就是使用者看到的介面。那麼 View 和 activity 是如何關聯在一起的呢 ?
上圖是 View 和 Activity 之間的關係。先解釋圖中一些類的作用以及相關關係:
-
Activity : 對於每一個 activity 都會有擁有一個 PhoneWindow。
- PhoneWindow :該類繼承於 Window 類,是 Window 類的具體實現,即我們可以通過該類具體去繪製視窗。並且,該類內部包含了一個 DecorView 物件,該 DectorView 物件是所有應用視窗的根 View。
-
DecorView 是一個應用視窗的根容器,它本質上是一個 FrameLayout。DecorView 有唯一一個子 View,它是一個垂直 LinearLayout,包含兩個子元素,一個是 TitleView( ActionBar 的容器),另一個是 ContentView(視窗內容的容器)。
-
ContentView :是一個 FrameLayout(android.R.id.content),我們平常用的 setContentView 就是設定它的子 View 。
- WindowManager : 是一個介面,裡面常用的方法有:新增View,更新View和刪除View。主要是用來管理 Window 的。WindowManager 具體的實現類是WindowManagerImpl。最終,WindowManagerImpl 會將業務交給 WindowManagerGlobal 來處理。
- WindowManagerService (WMS) : 負責管理各 app 視窗的建立,更新,刪除, 顯示順序。執行在 system_server 程序。
ViewRootImpl :擁有 DecorView 的例項,通過該例項來控制 DecorView 繪製。ViewRootImpl 的一個內部類 W,實現了 IWindow 介面,IWindow 介面是供 WMS 使用的,WSM 通過呼叫 IWindow 一些方法,通過 Binder 通訊的方式,最後執行到了 W 中對應的方法中。同樣的,ViewRootImpl 通過 IWindowSession 來呼叫 WMS 的 Session 一些方法。Session 類繼承自 IWindowSession.Stub
,每一個應用程序都有一個唯一的 Session 物件與 WMS 通訊。
DecorView 的建立
先從 Mainactivity 中的程式碼看起,首先是呼叫了 setContentView;
protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); }
該方法是父類 AppCompatActivity 的方法,最終會呼叫 AppCompatDelegateImpl 的 setContentView 方法:
// AppCompatDelegateImpl
public void setContentView(int resId) { this.ensureSubDecor(); ViewGroup contentParent = (ViewGroup)this.mSubDecor.findViewById(16908290); contentParent.removeAllViews(); LayoutInflater.from(this.mContext).inflate(resId, contentParent); this.mOriginalWindowCallback.onContentChanged(); }
ensureSubDecor 從字面理解就是建立 subDecorView,這個是根據主題來建立的,下文也會講到。建立完以後,從中獲取 contentParent,再將從 activity 傳入的 id xml 佈局新增到裡面。不過大家注意的是,在新增之前先呼叫 removeAllViews() 方法,確保沒有其他子 View 的干擾。
private void ensureSubDecor() { if (!this.mSubDecorInstalled) { this.mSubDecor = this.createSubDecor(); ...... } ...... }
最終會呼叫 createSubDecor() ,來看看裡面的具體程式碼邏輯:
private ViewGroup createSubDecor() { // 1、獲取主題引數,進行一些設定,包括標題,actionbar 等 TypedArray a = this.mContext.obtainStyledAttributes(styleable.AppCompatTheme); if (!a.hasValue(styleable.AppCompatTheme_windowActionBar)) { a.recycle(); throw new IllegalStateException("You need to use a Theme.AppCompat theme (or descendant) with this activity."); } else { if (a.getBoolean(styleable.AppCompatTheme_windowNoTitle, false)) { this.requestWindowFeature(1); } else if (a.getBoolean(styleable.AppCompatTheme_windowActionBar, false)) { this.requestWindowFeature(108); } if (a.getBoolean(styleable.AppCompatTheme_windowActionBarOverlay, false)) { this.requestWindowFeature(109); } if (a.getBoolean(styleable.AppCompatTheme_windowActionModeOverlay, false)) { this.requestWindowFeature(10); } this.mIsFloating = a.getBoolean(styleable.AppCompatTheme_android_windowIsFloating, false); a.recycle(); // 2、確保優先初始化 DecorView this.mWindow.getDecorView(); LayoutInflater inflater = LayoutInflater.from(this.mContext); ViewGroup subDecor = null; // 3、根據不同的設定來對 subDecor 進行初始化 if (!this.mWindowNoTitle) { if (this.mIsFloating) { subDecor = (ViewGroup)inflater.inflate(layout.abc_dialog_title_material, (ViewGroup)null); this.mHasActionBar = this.mOverlayActionBar = false; } else if (this.mHasActionBar) { TypedValue outValue = new TypedValue(); this.mContext.getTheme().resolveAttribute(attr.actionBarTheme, outValue, true); Object themedContext; if (outValue.resourceId != 0) { themedContext = new ContextThemeWrapper(this.mContext, outValue.resourceId); } else { themedContext = this.mContext; } subDecor = (ViewGroup)LayoutInflater.from((Context)themedContext).inflate(layout.abc_screen_toolbar, (ViewGroup)null); this.mDecorContentParent = (DecorContentParent)subDecor.findViewById(id.decor_content_parent); this.mDecorContentParent.setWindowCallback(this.getWindowCallback()); if (this.mOverlayActionBar) { this.mDecorContentParent.initFeature(109); } if (this.mFeatureProgress) { this.mDecorContentParent.initFeature(2); } if (this.mFeatureIndeterminateProgress) { this.mDecorContentParent.initFeature(5); } } } else { if (this.mOverlayActionMode) { subDecor = (ViewGroup)inflater.inflate(layout.abc_screen_simple_overlay_action_mode, (ViewGroup)null); } else { subDecor = (ViewGroup)inflater.inflate(layout.abc_screen_simple, (ViewGroup)null); } if (VERSION.SDK_INT >= 21) { ViewCompat.setOnApplyWindowInsetsListener(subDecor, new OnApplyWindowInsetsListener() { public WindowInsetsCompat onApplyWindowInsets(View v, WindowInsetsCompat insets) { int top = insets.getSystemWindowInsetTop(); int newTop = AppCompatDelegateImpl.this.updateStatusGuard(top); if (top != newTop) { insets = insets.replaceSystemWindowInsets(insets.getSystemWindowInsetLeft(), newTop, insets.getSystemWindowInsetRight(), insets.getSystemWindowInsetBottom()); } return ViewCompat.onApplyWindowInsets(v, insets); } }); } else { ((FitWindowsViewGroup)subDecor).setOnFitSystemWindowsListener(new OnFitSystemWindowsListener() { public void onFitSystemWindows(Rect insets) { insets.top = AppCompatDelegateImpl.this.updateStatusGuard(insets.top); } }); } } if (subDecor == null) { throw new IllegalArgumentException("AppCompat does not support the current theme features: { windowActionBar: " + this.mHasActionBar + ", windowActionBarOverlay: " + this.mOverlayActionBar + ", android:windowIsFloating: " + this.mIsFloating + ", windowActionModeOverlay: " + this.mOverlayActionMode + ", windowNoTitle: " + this.mWindowNoTitle + " }"); } else { if (this.mDecorContentParent == null) { this.mTitleView = (TextView)subDecor.findViewById(id.title); } ViewUtils.makeOptionalFitsSystemWindows(subDecor); ContentFrameLayout contentView = (ContentFrameLayout)subDecor.findViewById(id.action_bar_activity_content); ViewGroup windowContentView = (ViewGroup)this.mWindow.findViewById(16908290); if (windowContentView != null) { while(windowContentView.getChildCount() > 0) { View child = windowContentView.getChildAt(0); windowContentView.removeViewAt(0); contentView.addView(child); } windowContentView.setId(-1); contentView.setId(16908290); if (windowContentView instanceof FrameLayout) { ((FrameLayout)windowContentView).setForeground((Drawable)null); } } // 將 subDecor 新增到 DecorView 中 this.mWindow.setContentView(subDecor); contentView.setAttachListener(new OnAttachListener() { public void onAttachedFromWindow() { } public void onDetachedFromWindow() { AppCompatDelegateImpl.this.dismissPopups(); } }); return subDecor; } } }
上面的程式碼總結來說就是在做一件事,就是建立 subDecor。攤開來說具體如下:
1、根據使用者選擇的主題來設定一些顯示特性,包括標題,actionbar 等。
2、根據不同特性來初始化 subDecor;對 subDecor 內部的子 View 進行初始化。
3、最後新增到 DecorView中。
新增的具體程式碼如下:此處是通過呼叫
// AppCompatDelegateImpl this.mWindow.getDecorView(); // phoneWindow public final View getDecorView() { if (mDecor == null || mForceDecorInstall) { installDecor(); } return mDecor; } private void installDecor() { mForceDecorInstall = false; if (mDecor == null) { // 生成 DecorView mDecor = generateDecor(-1); mDecor.setDescendantFocusability(ViewGroup.FOCUS_AFTER_DESCENDANTS); mDecor.setIsRootNamespace(true); if (!mInvalidatePanelMenuPosted && mInvalidatePanelMenuFeatures != 0) { mDecor.postOnAnimation(mInvalidatePanelMenuRunnable); } } else { // 這樣 DecorView 就持有了window mDecor.setWindow(this); } ...... } protected DecorView generateDecor(int featureId) { // System process doesn't have application context and in that case we need to directly use // the context we have. Otherwise we want the application context, so we don't cling to the // activity. Context context; if (mUseDecorContext) { Context applicationContext = getContext().getApplicationContext(); if (applicationContext == null) { context = getContext(); } else { context = new DecorContext(applicationContext, getContext()); if (mTheme != -1) { context.setTheme(mTheme); } } } else { context = getContext(); } return new DecorView(context, featureId, this, getAttributes()); }
到此,DecorView 的建立就講完了。可是我們似乎並沒有看到 DecorView 是被新增的,什麼時候對使用者可見的。
WindowManager
View 建立完以後,那 Decorview 是怎麼新增到螢幕中去的呢?當然是 WindowManager 呢,那麼是如何將 View 傳到 WindowManager 中呢。
看 ActivityThread 中的 handleResumeActivity 方法:
// ActivityThread
public void handleResumeActivity(IBinder token, boolean finalStateRequest, boolean isForward, String reason) { ...... final int forwardBit = isForward ? WindowManager.LayoutParams.SOFT_INPUT_IS_FORWARD_NAVIGATION : 0; // If the window hasn't yet been added to the window manager, // and this guy didn't finish itself or start another activity, // then go ahead and add the window. boolean willBeVisible = !a.mStartedActivity; if (!willBeVisible) { try { willBeVisible = ActivityManager.getService().willActivityBeVisible( a.getActivityToken()); } catch (RemoteException e) { throw e.rethrowFromSystemServer(); } } if (r.window == null && !a.mFinished && willBeVisible) { r.window = r.activity.getWindow(); View decor = r.window.getDecorView(); decor.setVisibility(View.INVISIBLE); ViewManager wm = a.getWindowManager(); WindowManager.LayoutParams l = r.window.getAttributes(); a.mDecor = decor; l.type = WindowManager.LayoutParams.TYPE_BASE_APPLICATION; l.softInputMode |= forwardBit; ...... 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); } } // If the window has already been added, but during resume // we started another activity, then don't yet make the // window visible. } else if (!willBeVisible) { if (localLOGV) Slog.v(TAG, "Launch " + r + " mStartedActivity set"); r.hideForNow = true; } // Get rid of anything left hanging around. cleanUpPendingRemoveWindows(r, false /* force */); // The window is now visible if it has been added, we are not // simply finishing, and we are not starting another activity. if (!r.activity.mFinished && willBeVisible && r.activity.mDecor != null && !r.hideForNow) { if (r.newConfig != null) { performConfigurationChangedForActivity(r, r.newConfig); if (DEBUG_CONFIGURATION) { Slog.v(TAG, "Resuming activity " + r.activityInfo.name + " with newConfig " + r.activity.mCurrentConfig); } r.newConfig = null; } if (localLOGV) Slog.v(TAG, "Resuming " + r + " with isForward=" + isForward); WindowManager.LayoutParams l = r.window.getAttributes(); if ((l.softInputMode & WindowManager.LayoutParams.SOFT_INPUT_IS_FORWARD_NAVIGATION) != forwardBit) { l.softInputMode = (l.softInputMode & (~WindowManager.LayoutParams.SOFT_INPUT_IS_FORWARD_NAVIGATION)) | forwardBit; if (r.activity.mVisibleFromClient) { ViewManager wm = a.getWindowManager(); View decor = r.window.getDecorView(); wm.updateViewLayout(decor, l); } } r.activity.mVisibleFromServer = true; mNumVisibleActivities++; if (r.activity.mVisibleFromClient) { // 這裡也會呼叫addview r.activity.makeVisible(); } } r.nextIdle = mNewActivities; mNewActivities = r; if (localLOGV) Slog.v(TAG, "Scheduling idle handler for " + r); Looper.myQueue().addIdleHandler(new Idler()); }
上面的程式碼主要做了以下幾件事:
1、獲取到 DecorView,設定不可見,然後通過 wm.addView(decor, l) 將 view 新增到 WindowManager;
2、在某些情況下,比如此時點選了輸入框調起了鍵盤,就會呼叫 wm.updateViewLayout(decor, l) 來更新 View 的佈局。
3、這些做完以後,會呼叫 activity 的 makeVisible ,讓檢視可見。如果此時 DecorView 沒有新增到 WindowManager,那麼會新增。
// Activity
void makeVisible() { if (!mWindowAdded) { ViewManager wm = getWindowManager(); wm.addView(mDecor, getWindow().getAttributes()); mWindowAdded = true; } mDecor.setVisibility(View.VISIBLE); }
接下來,看下 addview 的邏輯。 WindowManager 的實現類是 WindowManagerImpl,而它則是通過 WindowManagerGlobal 代理實現 addView 的,我們看下 addView 的方法:
// WindowManagerGlobal public void addView(View view, ViewGroup.LayoutParams params, Display display, Window parentWindow) { // ...... 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; } }
在這裡,例項化了 ViewRootImpl 。同時呼叫 ViewRootImpl 的 setView 方法來持有了 DecorView。此外這裡還儲存了 DecorView ,Params,以及 ViewRootImpl 的例項。
現在我們終於知道為啥 View 是在 OnResume 的時候可見的呢。
ViewRootImpl
實際上,View 的繪製是由 ViewRootImpl 來負責的。每個應用程式視窗的 DecorView 都有一個與之關聯的 ViewRootImpl 物件,這種關聯關係是由 WindowManager 來維護的。
先看 ViewRootImpl 的 setView 方法,該方法很長,我們將一些不重要的點註釋掉:
/** * We have one child */ public void setView(View view, WindowManager.LayoutParams attrs, View panelParentView) { synchronized (this) { if (mView == null) { mView = view; ...... 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(); ...... } } }
這裡先將 mView 儲存了 DecorView 的例項,然後呼叫 requestLayout() 方法,以完成應用程式使用者介面的初次佈局。
public void requestLayout() { if (!mHandlingLayoutInLayoutRequest) { checkThread(); mLayoutRequested = true; scheduleTraversals(); } }
因為是 UI 繪製,所以一定要確保是在主執行緒進行的,checkThread 主要是做一個校驗。接著呼叫 scheduleTraversals 開始計劃繪製了。
void scheduleTraversals() { if (!mTraversalScheduled) { mTraversalScheduled = true; mTraversalBarrier = mHandler.getLooper().getQueue().postSyncBarrier(); mChoreographer.postCallback( Choreographer.CALLBACK_TRAVERSAL, mTraversalRunnable, null); if (!mUnbufferedInputDispatch) { scheduleConsumeBatchedInput(); } notifyRendererOfFramePending(); pokeDrawLockIfNeeded(); } }
這裡主要關注兩點:
mTraversalBarrier : Handler 的同步屏障。它的作用是可以攔截 Looper 對同步訊息的獲取和分發,加入同步屏障之後,Looper 只會獲取和處理非同步訊息,如果沒有非同步訊息那麼就會進入阻塞狀態。也就是說,對 View 繪製渲染的處理操作可以優先處理(設定為非同步訊息)。
mChoreographer: 編舞者。統一動畫、輸入和繪製時機。也是這章需要重點分析的內容。
mTraversalRunnable :TraversalRunnable 的例項,是一個Runnable,最終肯定會呼叫其 run 方法:
final class TraversalRunnable implements Runnable { @Override public void run() { doTraversal(); } }
doTraversal,如其名,開始繪製了,該方法內部最終會呼叫 performTraversals 進行繪製。
void doTraversal() { if (mTraversalScheduled) { mTraversalScheduled = false; mHandler.getLooper().getQueue().removeSyncBarrier(mTraversalBarrier); if (mProfile) { Debug.startMethodTracing("ViewAncestor"); } performTraversals(); if (mProfile) { Debug.stopMethodTracing(); mProfile = false; } } }
到此,DecorView 與 activity 之間的繫結關係就講完了,下一章,將會介紹 performTraversals 所做的事情,也就是 View 繪製流程。