setContentView 背后那些事儿

一行简单的 setContentView() 背后也会有大量的底层工作。往常总是手快的敲下这一行代码,甚至使用 AS 自动创建 Activity 都不用自己敲这一行代码,但是你有没有想过这一行简单代码背后的机制呢?这次就一起来看看。

Hierarchy View

在对这行代码背后的机制进行分析之前,要先学会如何去看一个具体的 Activity 是由哪些部分构成的,这对我们接下来的理解有很大的帮助。Hierarchy View 可以用图形化的视图来展示界面的组成结构。打开 AS 的 DDMS 视图,也就是 Android Device Monitor 这个工具。点击顶部工具栏 DDMS 按钮左边的 Open Perspective 按钮,选择 Hierarchy View ,就可以进入了。点击左边的小手机图标就能选择具体的 Activity 来查看组成结构。不过这个工具只能用来看虚拟机上的 Activity,实体机目前还不支持。

上面就是我的某个 App 的一个 Activity 界面的组成结构。其最左边,也就是最顶层的视图,是一个 PhoneWindow,同时也是一个 DecorView。记住这两个词,后面会用到。

Activity#setContentView()

我们知道,Activity 是所有类型的 Activity 的父类,那么自然 setContentView 这个方法最开始也是在 Activity 这个类中定义的。

1
2
3
4
public void setContentView(@LayoutRes int layoutResID) {
getWindow().setContentView(layoutResID);
initWindowDecorActionBar();
}

从代码里看得出来,(系统)拿到了一个 window 并把布局 id 设置到了这个 window 里,然后初始化了 window 的 Decor 和 ActionBar,就这么两件事情。首先看看拿到的 window 是什么:

1
2
3
public Window getWindow() {
return mWindow;
}

mWindow 是一个抽象类 Window, 而 mWindow 这个变量在整个 Activity 中只有一处被初始化:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
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,
Window window) {
attachBaseContext(context);

mFragments.attachHost(null /*parent*/);

mWindow = new PhoneWindow(this, window);
...
mWindow.setCallback(this);
...
}

PhoneWindow?好像有点眼熟,在第一步 Activity 的组织结构里,最左边的不就是个 PhoneWindow 么?那么这个 PhoneWindow 应该就是一个界面最顶层的 View 了,不过在看 PhoneWindow 的源码之前,先看看它的抽象类 Window 的说明:

1
2
3
4
5
6
7
8
9
10
11
12
13
/**
* Abstract base class for a top-level window look and behavior policy. An
* instance of this class should be used as the top-level view added to the
* window manager. It provides standard UI policies such as a background, title
* area, default key processing, etc.
*
* <p>The only existing implementation of this abstract class is
* android.view.PhoneWindow, which you should instantiate when needing a
* Window.
*/
public abstract class Window {
...
}

Window 是一个顶层窗口(界面)的外观和行为的代理,是个抽象类。这个类的实例应该当做顶级 View 添加到 window manager 中。这个类提供基本的 UI,例如背景、标题区域、默认的按键处理程序等。同时 PhoneWindow 这个类也是 Window 的唯一实现类。当你要显示一个视窗的时候你就必须实例化这个类(PhoneWindow)。也就是说,setContentView() 最终会调用 PhoneWindow 里的 setContentView() 方法。

PhoneWindow#setContentView()

下面是 PhoneWindow 中的代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
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) {
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);
}
mContentParent.requestApplyInsets();
final Callback cb = getCallback();
if (cb != null && !isDestroyed()) {
cb.onContentChanged();
}
mContentParentExplicitlySet = true;
}

这个方法中的第二行 insallDecor 这个方法确保了 mDecor 和 mContentParent 这两个变量都已经被实例化,将 mDecor 和 window 进行绑定,同时初始化视窗的标题栏,并从实例化的 window 中获取一些视窗标记(flag),还记得我们为了隐藏 Activity 的标题栏而调用 requestWindowFeature() 这个方法么,就是与这里是相同类型的标记,因为获取这些标记的过程是在 setContentView 里进行的。如果我们想让自定义的标记生效,就得在 setContentView() 这个方法之前调用 requestWindowFeature()
那么 mContentParent 又是什么呢:

1
2
3
// 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.
ViewGroup mContentParent;

这个 view 是用来放置视窗内容的(就是后期由我们自行添加的区域),要是隐藏了系统状态栏,它的大小就会跟它的上一层 mDecor 相同。也就是说,系统的状态栏是处于 mDecor 内部、mContentParent 外部的。

在实例化 mDecor 和 mContentParent 之后,就开始解析传进来的布局文件,将这个布局设置到 mContentParent 里面,最后通知 Activity 布局已经发生变化:首先 Activity 是实际的视窗的控制者,不通知它还能通知谁呢?其次呢,确实就是这么回事,还记得前面 Acticity 中的 attach 方法么,里面有这么一句:mWindow.setCallback(this);,而 mWindow 就是我们现在在讨论的 PhoneWindow 了,所以铁证如山啊。

Activity#insallDecor()

而关于 insallDecor 这个方法的具体情况,内容比较多,就不贴代码了,只要知道:

  • mDecor 是通过 generateDecor() 这个方法生成的,这个方法获取当前所能得到最高级别的 Context 来生成 DecorView 的实例,就是有 ApplicationContext 就尽量用 ApplicationContext;
  • mContentParent 是通过 generateLayout() 这个方法生成的,在这个方法里,会根据预设的 Window Style 来对布局文件多进行定制,也就是多次调用 requestWindowFeature() 这个方法,根据 Feature 的不同,还会选择不同的布局作为 DecorView 的初始布局。

Activity#initWindowDecorActionBar()

到这里 PhoneWindow 的 setContentView 就解读完了,在开始的 Activity 里的 setContentView 方法中还有一句代码:initWindowDecorActionBar(); 我们也来看看这个方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
private void initWindowDecorActionBar() {
Window window = getWindow();

// Initializing the window decor can change window feature flags.
// Make sure that we have the correct set before performing the test below.
window.getDecorView();

if (isChild() || !window.hasFeature(Window.FEATURE_ACTION_BAR) || mActionBar != null) {
return;
}

mActionBar = new WindowDecorActionBar(this);
mActionBar.setDefaultDisplayHomeAsUpEnabled(mEnableDefaultActionBarUp);

mWindow.setDefaultIcon(mActivityInfo.getIconResource());
mWindow.setDefaultLogo(mActivityInfo.getLogoResource());
}

这个方法是对初始化布局的进一步补充,在 PhoneWindow 里知识加载出一些默认的布局比如标题栏,那么这里就是对默认的标题栏进行一些初始的设置,比如在 manifest 文件中给一个 Activity 设置 label 属性,那么打开 Activity 就会直接显示 label 的内容,这个 label 就是在这里被设置的。

总结

结合前面这些加载过程的详情和最开始的组织结构,能够得到下面这个更直观的视窗组织结构示意图:

  • 系统 Launcher 界面:每一个 App 的图标都是在系统 Launcher 里面的一个按钮,通过按钮打开我们自己的 App,所以 Activity 外面就是 Launcher 的界面的了;
  • 应用窗口(mWindow):这个就是一个 Activity 内容的容器,是区分不同的 Activity 界面的最小单位,也是 PhoneWindow 的一个实例,Activity 加载过程中调用 attach方法的时候被实例化;
  • 顶级 View :如果说 Window 知识一个容器,那么从 mDecor 开始就能够真的显示一些东西了,这是一个 DecorView 对象,定义在 PhoneWindow 中,用来加载包括状态栏和导航栏在内的所有布局;
  • 系统状态栏:statusbar,PhoneWindow 会加载其背景进行占位,但并不具体绘制其内容;
  • 导航栏:navigationbar,与状态栏一样,PhoneWindow 会加载其背景进行占位,但并不具体绘制其内容;
  • mContentParent:这个就是可以经由我们自由添加和修改的布局了,其中可以添加和隐藏标题栏,而如果没有标题栏,大小就会跟 mDecor 相同;

而 setContentView 的过程,就是实例化 mWindow、mDecor、mContentParent 的过程,在这个过程中,根据 window 的主题、属性特征的不同,会加载不同的布局和 UI,包括是否显示状态栏、导航栏甚至是标题栏。经过这个过程,一个基本的 Activity 就可以显示出来,在这个基础上,我们就可以去按照自己的需求对 mContentParent 进行定制来充实自己的 App。