Android View源碼解讀 DecorView與ViewRootImpl淺談
前言
對(duì)于Android開(kāi)發(fā)者來(lái)說(shuō),View無(wú)疑是開(kāi)發(fā)中經(jīng)常接觸的,包括它的事件分發(fā)機(jī)制、測(cè)量、布局、繪制流程等,如果要自定義一個(gè)View,那么應(yīng)該對(duì)以上流程有所了解、研究。本系列文章將會(huì)為大家?guī)?lái)View的工作流程詳細(xì)解析。在深入接觸View的測(cè)量、布局、繪制這三個(gè)流程之前,我們從Activity入手,看看從Activity創(chuàng)建后到View的正式工作之前,所要經(jīng)歷的步驟。以下源碼均取自Android API 21。
從setContentView說(shuō)起
一般地,我們?cè)贏ctivity中,會(huì)在onCreate()方法中寫(xiě)下這樣一句:
setContentView(R.layout.main);
顯然,這是為activity設(shè)置一個(gè)我們定義好的main.xml布局,我們跟蹤一下源碼,看看這個(gè)方法是怎樣做的,Activity#setContentView:
public void setContentView(@LayoutRes int layoutResID) { getWindow().setContentView(layoutResID); //調(diào)用getWindow方法,返回mWindow initWindowDecorActionBar(); } ... public Window getWindow() { return mWindow; }
從上面看出,里面調(diào)用了mWindow的setContentView方法,那么這個(gè)“mWindow”是何方神圣呢?嘗試追蹤一下源碼,發(fā)現(xiàn)mWindow是Window類(lèi)型的,但是它是一個(gè)抽象類(lèi),setContentView也是抽象方法,所以我們要找到Window類(lèi)的實(shí)現(xiàn)類(lèi)才行。我們?cè)贏ctivity中查找一下mWindow在哪里被賦值了,可以發(fā)現(xiàn)它在Activity#attach方法中有如下實(shí)現(xiàn):
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) { ... mWindow = new PhoneWindow(this); ... }
我們只看關(guān)鍵部分,這里實(shí)例化了PhoneWindow類(lèi),由此得知,PhoneWindow是Window的實(shí)現(xiàn)類(lèi),那么我們?cè)赑honeWindow類(lèi)里面找到它的setContentView方法,看看它又實(shí)現(xiàn)了什么,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) { // 1 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); // 2 } final Callback cb = getCallback(); if (cb != null && !isDestroyed()) { cb.onContentChanged(); } }
首先判斷了mContentParent是否為null,如果為空則執(zhí)行installDecor()方法,那么這個(gè)mContentParent又是什么呢?我們看一下它的注釋?zhuān)?/p>
// This is the view in which the window contents are placed. It is either // mDecor itself, or a child of mDecor where the contents go. private ViewGroup mContentParent;
它是一個(gè)ViewGroup類(lèi)型,結(jié)合②號(hào)代碼處,可以得知,這個(gè)mContentParent是我們?cè)O(shè)置的布局(即main.xml)的父布局。注釋還提到了,這個(gè)mContentParent是mDecor本身或者是mDecor的一個(gè)子元素,這句話什么意思呢?這里先留一個(gè)疑問(wèn),下面會(huì)解釋。
這里先梳理一下以上的內(nèi)容:Activity通過(guò)PhoneWindow的setContentView方法來(lái)設(shè)置布局,而設(shè)置布局之前,會(huì)先判斷是否存在mContentParent,而我們?cè)O(shè)置的布局文件則是mContentParent的子元素。
創(chuàng)建DecorView
接著上面提到的installDecor()方法,我們看看它的源碼,PhoneWindow#installDecor:
private void installDecor() { if (mDecor == null) { mDecor = generateDecor(); // 1 mDecor.setDescendantFocusability(ViewGroup.FOCUS_AFTER_DESCENDANTS); mDecor.setIsRootNamespace(true); if (!mInvalidatePanelMenuPosted && mInvalidatePanelMenuFeatures != 0) { mDecor.postOnAnimation(mInvalidatePanelMenuRunnable); } } if (mContentParent == null) { mContentParent = generateLayout(mDecor); // 2 ... } } }
首先,會(huì)執(zhí)行①號(hào)代碼,調(diào)用PhoneWindow#generateDecor方法:
protected DecorView generateDecor() { return new DecorView(getContext(), -1); }
可以看出,這里實(shí)例化了DecorView,而DecorView則是PhoneWindow類(lèi)的一個(gè)內(nèi)部類(lèi),繼承于FrameLayout,由此可知它也是一個(gè)ViewGroup。
那么,DecroView到底充當(dāng)了什么樣的角色呢?
其實(shí),DecorView是整個(gè)ViewTree的最頂層View,它是一個(gè)FrameLayout布局,代表了整個(gè)應(yīng)用的界面。在該布局下面,有標(biāo)題view和內(nèi)容view這兩個(gè)子元素,而內(nèi)容view則是上面提到的mContentParent。我們接著看②號(hào)代碼,PhoneWindow#generateLayout方法
protected ViewGroup generateLayout(DecorView decor) { // Apply data from current theme. // 從主題文件中獲取樣式信息 TypedArray a = getWindowStyle(); ... if (a.getBoolean(R.styleable.Window_windowNoTitle, false)) { requestFeature(FEATURE_NO_TITLE); } else if (a.getBoolean(R.styleable.Window_windowActionBar, false)) { // Don't allow an action bar if there is no title. requestFeature(FEATURE_ACTION_BAR); } if(...){ ... } // Inflate the window decor. // 加載窗口布局 int layoutResource; int features = getLocalFeatures(); // System.out.println("Features: 0x" + Integer.toHexString(features)); if ((features & (1 << FEATURE_SWIPE_TO_DISMISS)) != 0) { layoutResource = R.layout.screen_swipe_dismiss; } else if(...){ ... } View in = mLayoutInflater.inflate(layoutResource, null); //加載layoutResource decor.addView(in, new ViewGroup.LayoutParams(MATCH_PARENT, MATCH_PARENT)); //往DecorView中添加子View,即mContentParent mContentRoot = (ViewGroup) in; ViewGroup contentParent = (ViewGroup)findViewById(ID_ANDROID_CONTENT); // 這里獲取的就是mContentParent if (contentParent == null) { throw new RuntimeException("Window couldn't find content container view"); } if ((features & (1 << FEATURE_INDETERMINATE_PROGRESS)) != 0) { ProgressBar progress = getCircularProgressBar(false); if (progress != null) { progress.setIndeterminate(true); } } if ((features & (1 << FEATURE_SWIPE_TO_DISMISS)) != 0) { registerSwipeCallbacks(); } // Remaining setup -- of background and title -- that only applies // to top-level windows. ... return contentParent; }
由以上代碼可以看出,該方法還是做了相當(dāng)多的工作的,首先根據(jù)設(shè)置的主題樣式來(lái)設(shè)置DecorView的風(fēng)格,比如說(shuō)有沒(méi)有titlebar之類(lèi)的,接著為DecorView添加子View,而這里的子View則是上面提到的mContentParent,如果上面設(shè)置了FEATURE_NO_ACTIONBAR,那么DecorView就只有mContentParent一個(gè)子View,這也解釋了上面的疑問(wèn):mContentParent是DecorView本身或者是DecorView的一個(gè)子元素。
用一幅圖來(lái)表示DecorView的結(jié)構(gòu)如下:
小結(jié):DecorView是頂級(jí)View,內(nèi)部有titlebar和contentParent兩個(gè)子元素,contentParent的id是content,而我們?cè)O(shè)置的main.xml布局則是contentParent里面的一個(gè)子元素。
在DecorView創(chuàng)建完畢后,讓我們回到PhoneWindow#setContentView方法,直接看②號(hào)代碼: mLayoutInflater.inflate(layoutResID, mContentParent);這里加載了我們?cè)O(shè)置的main.xml布局文件,并且設(shè)置mContentParent為main.xml的父布局,至于它怎么加載的,這里就不展開(kāi)來(lái)說(shuō)了。
到目前為止,通過(guò)setContentView方法,創(chuàng)建了DecorView和加載了我們提供的布局,但是這時(shí),我們的View還是不可見(jiàn)的,因?yàn)槲覀儍H僅是加載了布局,并沒(méi)有對(duì)View進(jìn)行任何的測(cè)量、布局、繪制工作。在View進(jìn)行測(cè)量流程之前,還要進(jìn)行一個(gè)步驟,那就是把DecorView添加至window中,然后經(jīng)過(guò)一系列過(guò)程觸發(fā)ViewRootImpl#performTraversals方法,在該方法內(nèi)部會(huì)正式開(kāi)始測(cè)量、布局、繪制這三大流程。至于該一系列過(guò)程是怎樣的,因?yàn)樯婕暗搅撕芏鄼C(jī)制,這里簡(jiǎn)單說(shuō)明一下:
將DecorView添加至Window
每一個(gè)Activity組件都有一個(gè)關(guān)聯(lián)的Window對(duì)象,用來(lái)描述一個(gè)應(yīng)用程序窗口。每一個(gè)應(yīng)用程序窗口內(nèi)部又包含有一個(gè)View對(duì)象,用來(lái)描述應(yīng)用程序窗口的視圖。上文分析了創(chuàng)建DecorView的過(guò)程,現(xiàn)在則要把DecorView添加到Window對(duì)象中。而要了解這個(gè)過(guò)程,我們首先要簡(jiǎn)單先了解一下Activity的創(chuàng)建過(guò)程:
首先,在ActivityThread#handleLaunchActivity中啟動(dòng)Activity,在這里面會(huì)調(diào)用到Activity#onCreate方法,從而完成上面所述的DecorView創(chuàng)建動(dòng)作,當(dāng)onCreate()方法執(zhí)行完畢,在handleLaunchActivity方法會(huì)繼續(xù)調(diào)用到ActivityThread#handleResumeActivity方法,我們看看這個(gè)方法的源碼:
final void handleResumeActivity(IBinder token, boolean clearHide, boolean isForward) { //... ActivityClientRecord r = performResumeActivity(token, clearHide); // 這里會(huì)調(diào)用到onResume()方法 if (r != null) { final Activity a = r.activity; //... if (r.window == null && !a.mFinished && willBeVisible) { r.window = r.activity.getWindow(); // 獲得window對(duì)象 View decor = r.window.getDecorView(); // 獲得DecorView對(duì)象 decor.setVisibility(View.INVISIBLE); ViewManager wm = a.getWindowManager(); // 獲得windowManager對(duì)象 WindowManager.LayoutParams l = r.window.getAttributes(); a.mDecor = decor; l.type = WindowManager.LayoutParams.TYPE_BASE_APPLICATION; l.softInputMode |= forwardBit; if (a.mVisibleFromClient) { a.mWindowAdded = true; wm.addView(decor, l); // 調(diào)用addView方法 } //... } } }
在該方法內(nèi)部,獲取該activity所關(guān)聯(lián)的window對(duì)象,DecorView對(duì)象,以及windowManager對(duì)象,而WindowManager是抽象類(lèi),它的實(shí)現(xiàn)類(lèi)是WindowManagerImpl,所以后面調(diào)用的是WindowManagerImpl#addView方法,我們看看源碼:
public final class WindowManagerImpl implements WindowManager { private final WindowManagerGlobal mGlobal = WindowManagerGlobal.getInstance(); ... @Override public void addView(View view, ViewGroup.LayoutParams params) { mGlobal.addView(view, params, mDisplay, mParentWindow); } }
接著調(diào)用了mGlobal的成員函數(shù),而mGlobal則是WindowManagerGlobal的一個(gè)實(shí)例,那么我們接著看WindowManagerGlobal#addView方法:
public void addView(View view, ViewGroup.LayoutParams params, Display display, Window parentWindow) { ... ViewRootImpl root; View panelParentView = null; synchronized (mLock) { ... root = new ViewRootImpl(view.getContext(), display); // 1 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); // 2 } catch (RuntimeException e) { // BadTokenException or InvalidDisplayException, clean up. synchronized (mLock) { final int index = findViewLocked(view, false); if (index >= 0) { removeViewLocked(index, true); } } throw e; } }
先看①號(hào)代碼處,實(shí)例化了ViewRootImpl類(lèi),接著,在②號(hào)代碼處,調(diào)用ViewRootImpl#setView方法,并把DecorView作為參數(shù)傳遞進(jìn)去,在這個(gè)方法內(nèi)部,會(huì)通過(guò)跨進(jìn)程的方式向WMS(WindowManagerService)發(fā)起一個(gè)調(diào)用,從而將DecorView最終添加到Window上,在這個(gè)過(guò)程中,ViewRootImpl、DecorView和WMS會(huì)彼此關(guān)聯(lián),至于詳細(xì)過(guò)程這里不展開(kāi)來(lái)說(shuō)了。
最后通過(guò)WMS調(diào)用ViewRootImpl#performTraverals方法開(kāi)始View的測(cè)量、布局、繪制流程,這三大流程在下文會(huì)詳細(xì)講述,謝謝大家的閱讀。
相關(guān)文章
Android開(kāi)發(fā)實(shí)例之登錄界面的實(shí)現(xiàn)
本文主要介紹Android 登錄界面實(shí)現(xiàn),這里主要講解類(lèi)似Twitter的登錄界面的實(shí)現(xiàn),有興趣的小伙伴可以參考下2016-08-08Android編程實(shí)現(xiàn)攝像頭臨摹效果的方法
這篇文章主要介紹了Android編程實(shí)現(xiàn)攝像頭臨摹效果的方法,涉及Android權(quán)限控制、布局及攝像頭功能調(diào)用等相關(guān)操作技巧,需要的朋友可以參考下2017-09-09Android進(jìn)階——安卓調(diào)用ESC/POS打印機(jī)打印實(shí)例
本篇文章主要介紹了Android進(jìn)階——安卓調(diào)用ESC/POS打印機(jī)打印實(shí)例,具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下。2017-04-04Android Studio導(dǎo)入第三方類(lèi)庫(kù)的方法
這篇文章主要介紹了Android Studio導(dǎo)入第三方類(lèi)庫(kù)的方法,導(dǎo)入*.jar包、導(dǎo)入第三方j(luò)ava類(lèi)庫(kù)含源碼包以及aar的引入,需要的朋友可以參考下2016-07-07RecyclerView實(shí)現(xiàn)常見(jiàn)的列表菜單
這篇文章主要為大家詳細(xì)介紹了用RecyclerView實(shí)現(xiàn)移動(dòng)應(yīng)用中常見(jiàn)的列表菜單,具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2016-12-12Android TV 3D卡片無(wú)限循環(huán)效果
這篇文章主要為大家詳細(xì)介紹了Android TV 3D卡片無(wú)限循環(huán)效果,文中示例代碼介紹的非常詳細(xì),具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2021-11-11Android Manifest中meta-data擴(kuò)展元素?cái)?shù)據(jù)的配置與獲取方式
這篇文章主要介紹了Android Manifest中meta-data擴(kuò)展元素?cái)?shù)據(jù)的配置與獲取方式,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。一起跟隨小編過(guò)來(lái)看看吧2020-03-03AndroidStudio修改Code Style來(lái)格式化自定義標(biāo)簽的xml文件方式
這篇文章主要介紹了AndroidStudio修改Code Style來(lái)格式化自定義標(biāo)簽的xml文件方式,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。一起跟隨小編過(guò)來(lái)看看吧2020-03-03