@TOC
好的,我们来深入分析一下 Android 中 setContentView 方法的源码和原理。这是一个理解 Android 视图系统如何工作的绝佳入口。
一、概述
setContentView 是 Activity 中一个至关重要的方法,它负责将我们编写的 XML 布局文件与 Activity 关联起来,从而在屏幕上显示用户界面。它的核心工作是构建一个由 DecorView 和 ContentParent 组成的视图树,并将我们的自定义布局嵌入其中。
二、基本用法回顾
java
public class MainActivity extends AppCompatActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main); // 关键调用
}
}
作用 :将 activity_main.xml 布局加载并显示在屏幕上。
但它的背后涉及了多个系统组件的协作:Activity → PhoneWindow → Window → DecorView → LayoutInflater → ViewRootImpl。
三、源码分析 (基于 Android API 30+)
我们将沿着调用链,从 Activity 深入到 PhoneWindow。
1. Activity.setContentView(int layoutResID)
这是最常用的入口。
java
// Activity.java
public void setContentView(@LayoutRes int layoutResID) {
// 关键点 1:getWindow() 返回的是 PhoneWindow 实例
getWindow().setContentView(layoutResID);
// 关键点 2:初始化 ActionBar(如果存在)
initWindowDecorActionBar();
}
// 返回的是 PhoneWindow 对象
public Window getWindow() {
return mWindow;
}
要点:
mWindow是在Activity.attach()方法中被初始化的,它是一个PhoneWindow实例。PhoneWindow是Window抽象类的唯一实现。Activity将具体的视图设置工作委托给了Window。
2. PhoneWindow.setContentView(int layoutResID)
现在我们进入核心类 PhoneWindow。
java
// PhoneWindow.java
@Override
public void setContentView(int layoutResID) {
// 关键点 3:installDecor() - 初始化 DecorView 和 ContentParent
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 {
// 关键点 4:将我们的布局 inflate 到 mContentParent 中
mLayoutInflater.inflate(layoutResID, mContentParent);
}
mContentParent.requestApplyInsets();
// 关键点 5:回调通知 Activity 内容视图已改变
final Callback cb = getCallback();
if (cb != null && !isDestroyed()) {
cb.onContentChanged();
}
mContentParentExplicitlySet = true;
}
要点:
mContentParent是ViewGroup类型,它是我们自定义布局的直接父容器。- 如果
mContentParent为空,说明是第一次调用setContentView,需要调用installDecor()来创建整个窗口的视图结构。 - 最终,通过
LayoutInflater.inflate()将我们的布局文件解析成 View 对象树,并添加到mContentParent中。 - 完成后,会通过
Callback(也就是Activity)的onContentChanged()方法通知 Activity。
3. PhoneWindow.installDecor()
这是构建窗口根视图结构的地方。
java
// PhoneWindow.java
private void installDecor() {
mForceDecorInstall = false;
if (mDecor == null) {
// 关键点 6:生成 DecorView
mDecor = generateDecor(-1);
mDecor.setDescendantFocusability(ViewGroup.FOCUS_AFTER_DESCENDANTS);
mDecor.setIsRootNamespace(true);
if (!mInvalidatePanelMenuPosted && mInvalidatePanelMenuFeatures != 0) {
mDecor.postOnAnimation(mInvalidatePanelMenuRunnable);
}
} else {
mDecor.setWindow(this);
}
if (mContentParent == null) {
// 关键点 7:根据窗口风格(如有无ActionBar)生成 mContentParent
mContentParent = generateLayout(mDecor);
// 初始化其他UI部件,如标题栏、ActionBar等
// ... (代码省略)
}
}
要点:
generateDecor()会创建一个DecorView对象,它是整个 Activity 窗口的根视图(一个FrameLayout)。generateLayout(mDecor)是重中之重,它负责选择并加载具体的窗口布局模板,并从中找到mContentParent。
4. PhoneWindow.generateLayout(DecorView decor)
这个方法决定了窗口的整体外观。
java
// PhoneWindow.java
protected ViewGroup generateLayout(DecorView decor) {
// 应用系统主题样式...
TypedArray a = getWindowStyle();
// ... (读取各种窗口属性,如 FEATURE_NO_TITLE, FEATURE_ACTION_BAR 等)
// 关键点 8:根据请求的 Features 和样式,选择一个预定义的布局文件
int layoutResource;
int features = getLocalFeatures();
// 一系列 if-else 逻辑,根据 features 选择不同的布局
if ((features & (1 << FEATURE_SWIPE_TO_DISMISS)) != 0) {
layoutResource = R.layout.screen_swipe_dismiss;
} else if ((features & (1 << FEATURE_ACTION_MODE_OVERLAY)) != 0) {
layoutResource = R.layout.screen_simple_overlay_action_mode;
} else if ((features & (1 << FEATURE_NO_TITLE)) == 0) {
// 有标题栏的布局
if (mIsFloating) {
layoutResource = R.layout.dialog_title;
} else {
layoutResource = R.layout.screen_title; // 例如这个
}
} else if ((features & (1 << FEATURE_ACTION_BAR)) != 0) {
layoutResource = R.layout.screen_action_bar;
} else {
// 关键点 9:最常用的,简单的全屏布局
layoutResource = R.layout.screen_simple; // <--- 就是这个!
}
// 关键点 10:将选中的布局 inflate 到 DecorView 中
mDecor.onResourcesLoaded(mLayoutInflater, layoutResource);
// 关键点 11:找到 id 为 "content" 的 ViewGroup,它就是 mContentParent
ViewGroup contentParent = (ViewGroup) findViewById(ID_ANDROID_CONTENT);
if (contentParent == null) {
throw new RuntimeException("Window couldn't find content container view");
}
// ... (其他样式设置)
return contentParent;
}
要点:
- 系统根据
requestWindowFeature()设置的Window特性(如FEATURE_NO_TITLE),选择一个内置的布局模板。最常见的模板是R.layout.screen_simple。 mDecor.onResourcesLoaded()会将这个选中的模板(一个 XML 布局)加载并作为DecorView的直接子视图。- 通过
findViewById(com.android.internal.R.id.content)找到模板中那个准备承载我们自定义内容的FrameLayout,它就是mContentParent。
5. 看看 screen_simple.xml 布局模板
这个文件位于 frameworks/base/core/res/res/layout/screen_simple.xml,它清晰地展示了视图层级。
xml
<!-- screen_simple.xml -->
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:fitsSystemWindows="true"
android:orientation="vertical">
<!-- 这个是可选的,用于处理状态栏背景等 -->
<ViewStub android:id="@+id/action_mode_bar_stub"
android:inflatedId="@+id/action_mode_bar"
android:layout="@layout/action_mode_bar"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:theme="?attr/actionBarTheme" />
<!-- 关键点 12:这就是我们的 mContentParent! -->
<FrameLayout
android:id="@android:id/content"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:foregroundInsidePadding="false"
android:foregroundGravity="fill_horizontal|top"
android:foreground="?android:attr/windowContentOverlay" />
</LinearLayout>
最终层级结构:
scss
DecorView (FrameLayout)
└── LinearLayout (来自 screen_simple.xml)
├── ViewStub (可选,用于ActionMode)
└── FrameLayout (id = android.R.id.content) <-- mContentParent
└── Your Custom Layout (通过 inflate(layoutResID, mContentParent) 添加)
三、核心成员解析
1.核心类
| 类名 | 作用 |
|---|---|
Activity |
提供 setContentView() 接口 |
Window |
抽象类,定义窗口行为 |
PhoneWindow |
Window 的唯一实现类,管理 DecorView |
DecorView |
窗口的根视图,继承自 FrameLayout,包含系统 UI 和用户内容 |
AppCompatDelegateImpl |
兼容库实现,处理 Material Design、夜间模式等 |
2 核心概念
- 委托模式 :
Activity并不直接处理视图,而是将工作委托给Window(具体实现是PhoneWindow)。 - 构建窗口根视图 :
PhoneWindow在第一次调用setContentView时,会创建DecorView作为根视图。DecorView是一个FrameLayout。 - 应用窗口模板 :系统根据主题和
requestWindowFeature的设置,选择一个预定义的 XML 布局模板(如screen_simple),并将其加载到DecorView中。 - 定位内容区域 :在这个模板中,有一个 ID 为
android.R.id.content的FrameLayout,这就是mContentParent,它是我们自定义布局的直接容器。 - 嵌入用户布局 :最后,通过
LayoutInflater将开发者提供的布局文件解析成 View 树,并作为子视图添加到mContentParent中。
3.DecorView 结构详解
运行时你可以通过以下方式查看结构:
java
View decorView = getWindow().getDecorView();
ViewGroup content = findViewById(android.R.id.content);
打印 decorView 的层级:
less
DecorView@123456
├── StatusBarBackground (状态栏背景)
├── NavigationBarBackground (导航栏背景)
├── LinearLayout (整体结构)
│ ├── ActionBar 或 Toolbar
│ └── ContentFrameLayout (@android:id/content)
│ └── ConstraintLayout (你的 activity_main.xml)
└── SubDecor (用于对话框样式等)
💡 所以你用
findViewById(android.R.id.content)得到的是一个ViewGroup,它只包含一个子 View ------ 你的布局根节点。
4.LayoutInflater 的角色
setContentView 内部使用了 LayoutInflater 来解析 XML 布局文件:
java
LayoutInflater.from(context).inflate(resId, parent, true);
- 将 XML 解析为 Java 对象(View 树)。
- 添加到
@android:id/content容器中。
5. 与 WindowManager 的关系
虽然 setContentView 设置了界面,但真正把 View 显示到屏幕 上是由 WindowManager 完成的。
在 ActivityThread.handleResumeActivity() 中:
java
// 此时才会将 DecorView 添加到 WindowManager
wm.addView(decorView, layoutParams);
此时触发:
ViewRootImpl创建- requestLayout() → measure → layout → draw 流程启动
- 界面真正绘制到屏幕上
四、流程图
五、常见问题与扩展
1. 为什么必须在 super.onCreate() 之后调用 setContentView()?
因为 super.onCreate() 会初始化 Window 和 AppCompatDelegate,如果没有这些对象,setContentView 无法工作。
2. setContentView() 调用了多次会发生什么?
java
setContentView(R.layout.a);
setContentView(R.layout.b); // 覆盖之前的
第二次调用会清空 @android:id/content 的所有子 View,然后加载新布局。相当于替换整个界面内容。
3. 如何获取 DecorView?有什么用途?
java
View decorView = getWindow().getDecorView();
// 例如:实现沉浸式状态栏
int uiOptions = View.SYSTEM_UI_FLAG_FULLSCREEN;
decorView.setSystemUiVisibility(uiOptions);
六、总结:setContentView 原理精要
| 阶段 | 关键动作 |
|---|---|
| 1. 初始化 | Activity 创建 PhoneWindow |
| 2. 构建壳子 | ensureSubDecor() 创建 DecorView |
| 3. 加载内容 | LayoutInflater 将布局 inflate 到 @android:id/content |
| 4. 显示 | WindowManager.addView(decorView) 触发绘制流程 |
🔑 核心思想:
setContentView()并不直接显示界面,而是将布局"塞进"窗口的"内容区",真正的显示由系统在 resume 阶段完成。