一、前言
作為一個(gè)Android開發(fā)人員,setContentView方法肯定相當(dāng)不陌生,因?yàn)樵谖覀兠恳粋€(gè)需要呈現(xiàn)頁面的Activity的onCreate方法中都會(huì)調(diào)用setContentView方法來加載我們事先寫好的布局文件。然而或許大部分人也和我一樣一直都是用用就好,也沒有深入思考該方法具體是怎樣將我們的布局文件呈現(xiàn)給用戶的。接下來我們來好好研究研究這個(gè)方法的作用原理吧!
二、Android窗口
既然我們想要知道Android的頁面加載過程,那么我們就得先了解Android系統(tǒng)中的窗口布局。一般來說,當(dāng)我們?cè)O(shè)置窗口的Theme為常見的樣式時(shí),Android的窗口如下圖所示: Android的窗口主要是圖中PhoneWindow所包含的部分:
Android常用的窗口布局文件為R.layout.screen_title,位于frameworks/base/core/res/layout/:
<!--
This is an optimized layout for a screen, with the minimum set of features
enabled.
-->
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:orientation="vertical"
android:fitsSystemWindows="true">
<FrameLayout
android:layout_width="match_parent"
android:layout_height="?android:attr/windowTitleSize"
style="?android:attr/windowTitleBackgroundStyle">
<TextView android:id="@android:id/title"
style="?android:attr/windowTitleStyle"
android:background="@null"
android:fadingEdge="horizontal"
android:gravity="center_vertical"
android:layout_width="match_parent"
android:layout_height="match_parent" />
</FrameLayout>
<FrameLayout android:id="@android:id/content"
android:layout_width="match_parent"
android:layout_height="0dip"
android:layout_weight="1"
android:foregroundGravity="fill_horizontal|top"
android:foreground="?android:attr/windowContentOverlay" />
</LinearLayout>
可以看出,DecorView中包含一個(gè)Vertical的LinearLayout布局文件,文件中有兩個(gè)FrameLayout,上面一個(gè)FrameLayout用于顯示Activity的標(biāo)題,下面一個(gè)FrameLayout用于顯示Activity的具體內(nèi)容,也就是說,我們通過setContentView方法加載的布局文件/View將顯示在該FrameLayout中。
三、setContentView加載view的流程
- 1、Activity中的setContentView方法
public void setContentView(@LayoutRes int layoutResID) {
//getWindow()方法將返回與該Activity相關(guān)聯(lián)的Window對(duì)象
getWindow().setContentView(layoutResID);
/*
* 當(dāng)該Activity是另一個(gè)Activity的子Activity、該Activity不含屬性值Window.FEATURE_ACTION_BAR
* 或者該Activity目前已有一個(gè)ActionBar時(shí),該方法不進(jìn)行任何操作,直接返回
* 否則初始化窗口的ActionBar,并為其設(shè)置相應(yīng)的屬性值
*/
initWindowDecorActionBar();
}
public void setContentView(View view) {
getWindow().setContentView(view);
initWindowDecorActionBar();
}
public void setContentView(View view, ViewGroup.LayoutParams params) {
getWindow().setContentView(view, params);
initWindowDecorActionBar();
}
public void addContentView(View view, ViewGroup.LayoutParams params) {
getWindow().addContentView(view, params);
initWindowDecorActionBar();
}
可以看到,在Activity的四個(gè)setContentView方法中,都分別調(diào)用了Window的相應(yīng)方法。
- 2、 PhoneWindow中的setContentView方法
Window中的setContentView方法均為抽象方法,所以跳過,直接看Window的實(shí)現(xiàn)類PhoneWindow中的setContentView方法
@Override
public void setContentView(int layoutResID) {
/*
* private ViewGroup mContentParent:該變量即為Activity的根布局文件,這是mDecor自身或mDecor的子類
* installDecor()方法用于加載mDecor,后面詳說
* FEATURE_CONTENT_TRANSITIONS:窗口內(nèi)容發(fā)生變化時(shí)是否需要使用TransitionManager進(jìn)行過渡的標(biāo)識(shí)
*/
if (mContentParent == null) {
installDecor();
} else if (!hasFeature(FEATURE_CONTENT_TRANSITIONS)) {
mContentParent.removeAllViews();
}
if (hasFeature(FEATURE_CONTENT_TRANSITIONS)) {
// 需要使用TransitionManager進(jìn)行過渡時(shí)的處理
final Scene newScene = Scene.getSceneForLayout(mContentParent, layoutResID,
getContext());
transitionTo(newScene);
} else {
//不需要過渡時(shí),通過inflate方法將layoutResID中的View樹添加到窗口中
mLayoutInflater.inflate(layoutResID, mContentParent);
}
//請(qǐng)求設(shè)置Window內(nèi)容的屬性值,將其寫入一個(gè)WindowInsets類中
mContentParent.requestApplyInsets();
final Callback cb = getCallback();
if (cb != null && !isDestroyed()) {
cb.onContentChanged();
}
}
@Override
public void setContentView(View view) {
//注意:當(dāng)使用該方法設(shè)置窗口布局文件時(shí),系統(tǒng)將默認(rèn)設(shè)置view的width和height均為MATCH_PARENT
//這里便可以解釋上一篇博客《Android LayoutInflater.inflater方法詳解》中的Case1了
setContentView(view, new ViewGroup.LayoutParams(MATCH_PARENT, MATCH_PARENT));
}
@Override
public void setContentView(View view, ViewGroup.LayoutParams params) {
//該方法與上面setContentView(int layoutResID)唯一的不同點(diǎn)在于第二個(gè)if語句的else語句塊內(nèi)容如下
//通過調(diào)用mContentParent的addView方法將view添加到窗口中
//也就是說這兩個(gè)方法唯一的區(qū)別在于將view添加到窗口的方式不同,其余并無差別
mContentParent.addView(view, params);
}
綜上所述,該方法的主要工作為;
第一步:
如果mContentParent 為空(即這是第一次調(diào)用setContentView方法),則installDecor()
如果不是第一次調(diào)用該方法,且無需使用 TransitionManager進(jìn)行過渡,則直接將窗口中的所有子View均移除第二步:
如果需要使用 TransitionManager進(jìn)行過渡,使用 TransitionManager進(jìn)行過渡
否則采用恰當(dāng)?shù)姆绞綄iew添加到窗口中
四、部分方法詳解
1、installDecor()方法:
該方法位于PhoneWindow類中
private void installDecor() {
// 如果mDecor為空,則生成一個(gè)Decor,并設(shè)置其屬性
if (mDecor == null) {
// 此句即mDecor = new DecorView(getContext(), -1)
mDecor = generateDecor();
/*
* setDescendantFocusability用于設(shè)置mDecor中的子View的聚焦性
* 該方法決定了mDecor與其中包含的子View之間關(guān)于焦點(diǎn)獲取的關(guān)系
* FOCUS_AFTER_DESCENDANTS表示只有當(dāng)mDecor的子View都不愿意獲取焦點(diǎn)時(shí) 才讓mDecor獲取焦點(diǎn)
*/
mDecor.setDescendantFocusability(ViewGroup.FOCUS_AFTER_DESCENDANTS);
// 設(shè)置mDecor為整個(gè)Activity窗口的根節(jié)點(diǎn),從此處可以看出窗口根節(jié)點(diǎn)為一個(gè)DecorView
mDecor.setIsRootNamespace(true);
/*
* if條件滿足時(shí),在animation時(shí)執(zhí)行mInvalidatePanelMenuRunnable這個(gè)Runnable動(dòng)作
*/
if (!mInvalidatePanelMenuPosted
&& mInvalidatePanelMenuFeatures != 0) {
mDecor.postOnAnimation(mInvalidatePanelMenuRunnable);
}
}
// 如果mContentParent為空,則生成一個(gè)Decor,并設(shè)置其屬性
// 后面會(huì)詳說generateLayout(DecorView decor)方法
if (mContentParent == null) {
mContentParent = generateLayout(mDecor);
// Set up decor part of UI to ignore fitsSystemWindows if
// appropriate.
mDecor.makeOptionalFitsSystemWindows();
/*
* DecorContentParent位于com.android.internal.widget中,是一個(gè)接口
* 由應(yīng)用程序窗口的頂層Decor實(shí)現(xiàn),該類主要為mDecor提供了許多title/window decor features
*/
final DecorContentParent decorContentParent = (DecorContentParent) mDecor
.findViewById(R.id.decor_content_parent);
if (decorContentParent != null) {
/*
* decorContentParent非空時(shí)
* 1. 將decorContentParent賦值給mDecorContentParent
* 2. 設(shè)置窗口回調(diào)函數(shù)
* 3.設(shè)置窗口的title、icon、logo等屬性值
* 為了加強(qiáng)博客的可讀性,就未將這部分代碼貼出來,只將主要功能進(jìn)行了簡單介紹
* 想要詳細(xì)了解的可以直接參看源碼
*/
} else {
/*
* decorContentParent為空時(shí)根據(jù)窗口是否為一個(gè)包含Title的窗口決定是否顯示title
* 如果窗口包含特征FEATURE_NO_TITLE,則隱藏窗口的title view 否則設(shè)置窗口的title
*/
}
if (mDecor.getBackground() == null
&& mBackgroundFallbackResource != 0) {
mDecor.setBackgroundFallback(mBackgroundFallbackResource);
}
if (hasFeature(FEATURE_ACTIVITY_TRANSITIONS)) {
// Only inflate or create a new TransitionManager if the caller
// hasn't already set a custom one.
//源碼未貼出
}
}
}
2、ViewGroup generateLayout(DecorView decor)方法
//返回當(dāng)前Activity的內(nèi)容區(qū)域視圖,即我們的布局文件顯示區(qū)域mContentParent
protected ViewGroup generateLayout(DecorView decor) {
// Apply data from current theme.
//從當(dāng)前Window的Theme中獲取一組屬性值,賦給a
TypedArray a = getWindowStyle();
/*
* 此處有段代碼未貼出,功能為:
* 1. 根據(jù)Activity的Theme特征,為當(dāng)前窗口選擇布局文件的修飾feature
* 2. Inflate the window decor
*/
int layoutResource;
int features = getLocalFeatures();
/*
* 此處有段代碼未貼出
* 1. getLocalFeatures()返回一個(gè)用于描述當(dāng)前Window特征的整數(shù)值
* 2. layoutResource為根據(jù)features所指代的窗口特征值而為當(dāng)前窗口選定的資源文件id
* 3. 系統(tǒng)包含多個(gè)布局資源文件,位于frameworks/base/core/res/layout/
* 4. 主要有:R.layout.dialog_titile_icons、R.layout.screen_title_icons
* R.layout.screen_progress、R.layout.dialog_custom_title
* R.layout.dialog_title
* R.layout.screen_title 最常用的Activity窗口修飾布局文件
* R.layout.screen_simple 全屏的Activity窗口布局文件
*/
//startChanging()方法內(nèi)容:mChanging = true;
mDecor.startChanging();
//將layoutResource資源文件包含的View樹添加到decor中
//width和height均為MATCH_PARENT
//并為mContentRoot和contentParent賦值
View in = mLayoutInflater.inflate(layoutResource, null);
decor.addView(in, new ViewGroup.LayoutParams(MATCH_PARENT, MATCH_PARENT));
mContentRoot = (ViewGroup) in;
ViewGroup contentParent = (ViewGroup)findViewById(ID_ANDROID_CONTENT);
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();
}
//后面包含一段只能應(yīng)用于頂層窗口的一些Remaining steps
//主要用于設(shè)置一些title和background屬性
return contentParent;
}
五、總結(jié)
1、setContentView方法工作流程
setContentView方法的具體實(shí)現(xiàn)是在PhoneWindow類中,主要通過如下幾個(gè)步驟完成xml布局資源文件或View的加載。
注意:
使用setContentView(View view)方法設(shè)置Activity的布局時(shí),系統(tǒng)會(huì)默認(rèn)將該view的width和height值均設(shè)為MATCH_PARENT,而不是使用view自己的屬性值,所以如果想通過一個(gè)View對(duì)象設(shè)置布局,又想使用自己設(shè)置的參數(shù)值時(shí),需要使用setContentView(View view, LayoutParams params)方法
第一步:若是首次使用setContentView方法,則先創(chuàng)建一個(gè)DecorView對(duì)象mDecor,該對(duì)象是整個(gè)Activity窗口的根視圖;然后根據(jù)程序中選擇的Activity的Theme/Style等屬性值為窗口添加布局屬性和相應(yīng)的修飾文件,并通過findViewById方法獲取對(duì)應(yīng)的根布局文件添加到mDecor中,也就是說,第一次使用該方法時(shí)會(huì)將Activity顯示區(qū)域進(jìn)行初始化;若不是第一次使用該方法,則之前已完成初始化過程并獲得了mDecor和mContentParent對(duì)象,則只需要將之前添加到mContentParent區(qū)域的Views移除,空出該區(qū)域重新進(jìn)行布局即可,簡而言之,就是對(duì)mContentParent區(qū)域進(jìn)行刷新;
第二步:通過inflate(加載xml文件)或addView(加載View)方法將Activity的布局文件添加到mContentParent區(qū)域;
當(dāng)setContentView設(shè)置顯示OK以后,回調(diào)Activity的onContentChanged方法,通知Activity布局文件已經(jīng)成功加載完成,接下來我們便可以使用findViewById方法獲取布局文件中含有id屬性的view對(duì)象了;
2、淺談布局文件優(yōu)化技巧
從上面的分析可知,在加載xml布局文件時(shí),系統(tǒng)是通過遞歸的方式從根節(jié)點(diǎn)到葉子節(jié)點(diǎn)一步一步對(duì)控件的屬性進(jìn)行解析的,所以xml文件的層次越深,效率越低,如果嵌套過多,還有可能導(dǎo)致棧溢出,所以在書寫布局文件時(shí),應(yīng)盡量對(duì)布局文件進(jìn)行優(yōu)化,通過使用相對(duì)布局等方式減少不必要的嵌套層次
在源碼中,可以看到對(duì)merge標(biāo)簽進(jìn)行處理的過程。在某些場合下,merge標(biāo)簽的使用也可以有效減少布局文件的嵌套層次。如某些比較復(fù)雜的布局文件,需要將布局文件拆分開來,分為一個(gè)根布局文件和若干個(gè)子布局文件,這時(shí)可能子布局文件的根節(jié)點(diǎn)在添加到根布局文件中時(shí)并沒有太多意義,只會(huì)增加根布局文件的嵌套層次,這種情況下,在子布局文件處使用merge標(biāo)簽就可以去掉無謂的嵌套層次。不過merge標(biāo)簽的使用也是有限制的,首先merge標(biāo)簽只能用于一個(gè)xml文件的根節(jié)點(diǎn);其次,使用inflate方法來加載一個(gè)根節(jié)點(diǎn)為merge標(biāo)簽的布局文件時(shí),需要為該文件指定一個(gè)ViewGroup對(duì)象作為其父元素,同時(shí)需要設(shè)置attachToRoot屬性為true,否則會(huì)拋出異常;
利用include標(biāo)簽增加布局文件的重用性和可讀性;