一蚌堵、前言
作為一個Android開發(fā)人員介衔,setContentView方法肯定相當不陌生,因為在我們每一個需要呈現頁面的Activity的onCreate方法中都會調用setContentView方法來加載我們事先寫好的布局文件历极。然而或許大部分人也和我一樣一直都是用用就好,也沒有深入思考該方法具體是怎樣將我們的布局文件呈現給用戶的瑰谜。接下來我們來好好研究研究這個方法的作用原理吧!
二树绩、Android窗口
既然我們想要知道Android的頁面加載過程萨脑,那么我們就得先了解Android系統中的窗口布局。一般來說饺饭,當我們設置窗口的Theme為常見的樣式時渤早,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中包含一個Vertical的LinearLayout布局文件鹊杖,文件中有兩個FrameLayout悴灵,上面一個FrameLayout用于顯示Activity的標題,下面一個FrameLayout用于顯示Activity的具體內容骂蓖,也就是說积瞒,我們通過setContentView方法加載的布局文件/View將顯示在該FrameLayout中。
三登下、setContentView加載view的流程
- 1茫孔、Activity中的setContentView方法
public void setContentView(@LayoutRes int layoutResID) {
//getWindow()方法將返回與該Activity相關聯的Window對象
getWindow().setContentView(layoutResID);
/*
* 當該Activity是另一個Activity的子Activity、該Activity不含屬性值Window.FEATURE_ACTION_BAR
* 或者該Activity目前已有一個ActionBar時被芳,該方法不進行任何操作缰贝,直接返回
* 否則初始化窗口的ActionBar,并為其設置相應的屬性值
*/
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的四個setContentView方法中,都分別調用了Window的相應方法赋朦。
- 2篓冲、 PhoneWindow中的setContentView方法
Window中的setContentView方法均為抽象方法,所以跳過宠哄,直接看Window的實現類PhoneWindow中的setContentView方法
@Override
public void setContentView(int layoutResID) {
/*
* private ViewGroup mContentParent:該變量即為Activity的根布局文件壹将,這是mDecor自身或mDecor的子類
* installDecor()方法用于加載mDecor,后面詳說
* FEATURE_CONTENT_TRANSITIONS:窗口內容發(fā)生變化時是否需要使用TransitionManager進行過渡的標識
*/
if (mContentParent == null) {
installDecor();
} else if (!hasFeature(FEATURE_CONTENT_TRANSITIONS)) {
mContentParent.removeAllViews();
}
if (hasFeature(FEATURE_CONTENT_TRANSITIONS)) {
// 需要使用TransitionManager進行過渡時的處理
final Scene newScene = Scene.getSceneForLayout(mContentParent, layoutResID,
getContext());
transitionTo(newScene);
} else {
//不需要過渡時毛嫉,通過inflate方法將layoutResID中的View樹添加到窗口中
mLayoutInflater.inflate(layoutResID, mContentParent);
}
//請求設置Window內容的屬性值诽俯,將其寫入一個WindowInsets類中
mContentParent.requestApplyInsets();
final Callback cb = getCallback();
if (cb != null && !isDestroyed()) {
cb.onContentChanged();
}
}
@Override
public void setContentView(View view) {
//注意:當使用該方法設置窗口布局文件時,系統將默認設置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)唯一的不同點在于第二個if語句的else語句塊內容如下
//通過調用mContentParent的addView方法將view添加到窗口中
//也就是說這兩個方法唯一的區(qū)別在于將view添加到窗口的方式不同承粤,其余并無差別
mContentParent.addView(view, params);
}
綜上所述暴区,該方法的主要工作為;
第一步:
如果mContentParent 為空(即這是第一次調用setContentView方法)辛臊,則installDecor()
如果不是第一次調用該方法仙粱,且無需使用 TransitionManager進行過渡,則直接將窗口中的所有子View均移除第二步:
如果需要使用 TransitionManager進行過渡彻舰,使用 TransitionManager進行過渡
否則采用恰當的方式將view添加到窗口中
四伐割、部分方法詳解
1、installDecor()方法:
該方法位于PhoneWindow類中
private void installDecor() {
// 如果mDecor為空刃唤,則生成一個Decor隔心,并設置其屬性
if (mDecor == null) {
// 此句即mDecor = new DecorView(getContext(), -1)
mDecor = generateDecor();
/*
* setDescendantFocusability用于設置mDecor中的子View的聚焦性
* 該方法決定了mDecor與其中包含的子View之間關于焦點獲取的關系
* FOCUS_AFTER_DESCENDANTS表示只有當mDecor的子View都不愿意獲取焦點時 才讓mDecor獲取焦點
*/
mDecor.setDescendantFocusability(ViewGroup.FOCUS_AFTER_DESCENDANTS);
// 設置mDecor為整個Activity窗口的根節(jié)點,從此處可以看出窗口根節(jié)點為一個DecorView
mDecor.setIsRootNamespace(true);
/*
* if條件滿足時尚胞,在animation時執(zhí)行mInvalidatePanelMenuRunnable這個Runnable動作
*/
if (!mInvalidatePanelMenuPosted
&& mInvalidatePanelMenuFeatures != 0) {
mDecor.postOnAnimation(mInvalidatePanelMenuRunnable);
}
}
// 如果mContentParent為空硬霍,則生成一個Decor,并設置其屬性
// 后面會詳說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中笼裳,是一個接口
* 由應用程序窗口的頂層Decor實現须尚,該類主要為mDecor提供了許多title/window decor features
*/
final DecorContentParent decorContentParent = (DecorContentParent) mDecor
.findViewById(R.id.decor_content_parent);
if (decorContentParent != null) {
/*
* decorContentParent非空時
* 1. 將decorContentParent賦值給mDecorContentParent
* 2. 設置窗口回調函數
* 3.設置窗口的title崖堤、icon、logo等屬性值
* 為了加強博客的可讀性耐床,就未將這部分代碼貼出來密幔,只將主要功能進行了簡單介紹
* 想要詳細了解的可以直接參看源碼
*/
} else {
/*
* decorContentParent為空時根據窗口是否為一個包含Title的窗口決定是否顯示title
* 如果窗口包含特征FEATURE_NO_TITLE,則隱藏窗口的title view 否則設置窗口的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)方法
//返回當前Activity的內容區(qū)域視圖胯甩,即我們的布局文件顯示區(qū)域mContentParent
protected ViewGroup generateLayout(DecorView decor) {
// Apply data from current theme.
//從當前Window的Theme中獲取一組屬性值,賦給a
TypedArray a = getWindowStyle();
/*
* 此處有段代碼未貼出堪嫂,功能為:
* 1. 根據Activity的Theme特征偎箫,為當前窗口選擇布局文件的修飾feature
* 2. Inflate the window decor
*/
int layoutResource;
int features = getLocalFeatures();
/*
* 此處有段代碼未貼出
* 1. getLocalFeatures()返回一個用于描述當前Window特征的整數值
* 2. layoutResource為根據features所指代的窗口特征值而為當前窗口選定的資源文件id
* 3. 系統包含多個布局資源文件,位于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()方法內容: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();
}
//后面包含一段只能應用于頂層窗口的一些Remaining steps
//主要用于設置一些title和background屬性
return contentParent;
}
五、總結
1恶复、setContentView方法工作流程
setContentView方法的具體實現是在PhoneWindow類中怜森,主要通過如下幾個步驟完成xml布局資源文件或View的加載。
注意:
使用setContentView(View view)方法設置Activity的布局時谤牡,系統會默認將該view的width和height值均設為MATCH_PARENT,而不是使用view自己的屬性值副硅,所以如果想通過一個View對象設置布局,又想使用自己設置的參數值時翅萤,需要使用setContentView(View view, LayoutParams params)方法
第一步:若是首次使用setContentView方法恐疲,則先創(chuàng)建一個DecorView對象mDecor,該對象是整個Activity窗口的根視圖套么;然后根據程序中選擇的Activity的Theme/Style等屬性值為窗口添加布局屬性和相應的修飾文件培己,并通過findViewById方法獲取對應的根布局文件添加到mDecor中,也就是說胚泌,第一次使用該方法時會將Activity顯示區(qū)域進行初始化漱凝;若不是第一次使用該方法,則之前已完成初始化過程并獲得了mDecor和mContentParent對象诸迟,則只需要將之前添加到mContentParent區(qū)域的Views移除茸炒,空出該區(qū)域重新進行布局即可,簡而言之阵苇,就是對mContentParent區(qū)域進行刷新壁公;
第二步:通過inflate(加載xml文件)或addView(加載View)方法將Activity的布局文件添加到mContentParent區(qū)域;
當setContentView設置顯示OK以后绅项,回調Activity的onContentChanged方法紊册,通知Activity布局文件已經成功加載完成,接下來我們便可以使用findViewById方法獲取布局文件中含有id屬性的view對象了;
2囊陡、淺談布局文件優(yōu)化技巧
從上面的分析可知芳绩,在加載xml布局文件時,系統是通過遞歸的方式從根節(jié)點到葉子節(jié)點一步一步對控件的屬性進行解析的撞反,所以xml文件的層次越深妥色,效率越低,如果嵌套過多遏片,還有可能導致棧溢出嘹害,所以在書寫布局文件時,應盡量對布局文件進行優(yōu)化吮便,通過使用相對布局等方式減少不必要的嵌套層次
在源碼中笔呀,可以看到對merge標簽進行處理的過程。在某些場合下髓需,merge標簽的使用也可以有效減少布局文件的嵌套層次许师。如某些比較復雜的布局文件,需要將布局文件拆分開來僚匆,分為一個根布局文件和若干個子布局文件微渠,這時可能子布局文件的根節(jié)點在添加到根布局文件中時并沒有太多意義,只會增加根布局文件的嵌套層次白热,這種情況下敛助,在子布局文件處使用merge標簽就可以去掉無謂的嵌套層次粗卜。不過merge標簽的使用也是有限制的屋确,首先merge標簽只能用于一個xml文件的根節(jié)點;其次续扔,使用inflate方法來加載一個根節(jié)點為merge標簽的布局文件時攻臀,需要為該文件指定一個ViewGroup對象作為其父元素,同時需要設置attachToRoot屬性為true纱昧,否則會拋出異常刨啸;
利用include標簽增加布局文件的重用性和可讀性;