一、前言

作为一个Android开发人员,setContentView方法肯定相当不陌生,因为在我们每一个需要呈现页面的Activity的onCreate方法中都会调用setContentView方法来加载我们事先写好的布局文件。然而或许大部分人也和我一样一直都是用用就好,也没有深入思考该方法具体是怎样将我们的布局文件呈现给用户的。接下来我们来好好研究研究这个方法的作用原理吧!

二、Android窗口

既然我们想要知道Android的页面加载过程,那么我们就得先了解Android系统中的窗口布局。一般来说,当我们设置窗口的Theme为常见的样式时,Android的窗口如下图所示: Android的窗口主要是图中PhoneWindow所包含的部分:

Android窗口模型

Android窗口模型解析

Android常用的窗口布局文件为R.layout.screen_title,位于frameworks/base/core/res/layout/:

android:orientation="vertical"
android:fitsSystemWindows="true">
android:layout_width="match_parent"
android:layout_height="?android:attr/windowTitleSize"
style="?android:attr/windowTitleBackgroundStyle">
style="?android:attr/windowTitleStyle"
android:background="@null"
android:fadingEdge="horizontal"
android:gravity="center_vertical"
android:layout_width="match_parent"
android:layout_height="match_parent" />
android:layout_width="match_parent"
android:layout_height="0dip"
android:layout_weight="1"
android:foregroundGravity="fill_horizontal|top"
android:foreground="?android:attr/windowContentOverlay" />

可以看出,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:窗口内容发生变化时是否需要使用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添加到窗口中
//也就是说这两个方法唯一的区别在于将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窗口的根节点,从此处可以看出窗口根节点为一个DecorView
mDecor.setIsRootNamespace(true);
/*
* if条件满足时,在animation时执行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的内容区域视图,即我们的布局文件显示区域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方法,则先创建一个DecorView对象mDecor,该对象是整个Activity窗口的根视图;然后根据程序中选择的Activity的Theme/Style等属性值为窗口添加布局属性和相应的修饰文件,并通过findViewById方法获取对应的根布局文件添加到mDecor中,也就是说,第一次使用该方法时会将Activity显示区域进行初始化;若不是第一次使用该方法,则之前已完成初始化过程并获得了mDecor和mContentParent对象,则只需要将之前添加到mContentParent区域的Views移除,空出该区域重新进行布局即可,简而言之,就是对mContentParent区域进行刷新;

第二步:通过inflate(加载xml文件)或addView(加载View)方法将Activity的布局文件添加到mContentParent区域;

当setContentView设置显示OK以后,回调Activity的onContentChanged方法,通知Activity布局文件已经成功加载完成,接下来我们便可以使用findViewById方法获取布局文件中含有id属性的view对象了;

2、浅谈布局文件优化技巧

从上面的分析可知,在加载xml布局文件时,系统是通过递归的方式从根节点到叶子节点一步一步对控件的属性进行解析的,所以xml文件的层次越深,效率越低,如果嵌套过多,还有可能导致栈溢出,所以在书写布局文件时,应尽量对布局文件进行优化,通过使用相对布局等方式减少不必要的嵌套层次

在源码中,可以看到对merge标签进行处理的过程。在某些场合下,merge标签的使用也可以有效减少布局文件的嵌套层次。如某些比较复杂的布局文件,需要将布局文件拆分开来,分为一个根布局文件和若干个子布局文件,这时可能子布局文件的根节点在添加到根布局文件中时并没有太多意义,只会增加根布局文件的嵌套层次,这种情况下,在子布局文件处使用merge标签就可以去掉无谓的嵌套层次。不过merge标签的使用也是有限制的,首先merge标签只能用于一个xml文件的根节点;其次,使用inflate方法来加载一个根节点为merge标签的布局文件时,需要为该文件指定一个ViewGroup对象作为其父元素,同时需要设置attachToRoot属性为true,否则会抛出异常;

利用include标签增加布局文件的重用性和可读性;