1 继承 Activity 的流程
PhoneWindow.setContentView --- 主要目的 创建 DecorView 拿到 Content
在 Android 中,Activity 是应用的基本组件,它提供了 setContentView 方法用于设置布局文件。setContentView 的背后涉及多个层级的调用,最终实现了布局加载和显示到屏幕的过程。
下面对继承 Activity 时调用 setContentView 的流程进行源码分析。
1. setContentView 的调用
通常在 Activity 的 onCreate 方法中调用 setContentView:
java
复制代码
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
}
setContentView(int layoutResID)接收一个布局资源文件 ID,并加载该布局到当前Activity的窗口中。- 它的核心逻辑由
Activity的Window和WindowManager实现。
2. setContentView 的方法源码
在 Activity 中,setContentView 是一个简单的封装,主要调用了 Window 对象的 setContentView 方法。
java
复制代码
@Override
public void setContentView(@LayoutRes int layoutResID) {
getWindow().setContentView(layoutResID);
initWindowDecorActionBar();
}
关键点解析:
-
getWindow():- 返回
Activity所关联的Window对象,Window的默认实现是PhoneWindow。 PhoneWindow是 Android 系统中窗口的具体实现类,负责管理 DecorView。
- 返回
-
setContentView的实际实现:- 由
PhoneWindow#setContentView实现。
- 由
-
initWindowDecorActionBar():- 用于初始化
ActionBar。 - 如果
Activity不使用AppCompatActivity,通常不会有复杂的逻辑。
- 用于初始化
3. PhoneWindow#setContentView 方法
java
复制代码
@Override
public void setContentView(int layoutResID) {
// 确保 DecorView 已经初始化
if (mContentParent == null) {
installDecor();
}
// 清空已有的内容视图
mContentParent.removeAllViews();
// 将布局文件加载到 mContentParent
LayoutInflater.from(mContext).inflate(layoutResID, mContentParent);
// 通知内容视图已变更
mContentParent.requestApplyInsets();
}
关键逻辑解析:
3.1 初始化 DecorView
- 调用
installDecor()方法,确保DecorView和mContentParent已初始化。
java
复制代码
private void installDecor() {
if (mDecor == null) {
mDecor = generateDecor(); // 创建 DecorView
}
if (mContentParent == null) {
mContentParent = generateLayout(mDecor); // 创建内容区域
}
}
-
generateDecor:- 创建并返回一个
DecorView对象。 DecorView是整个窗口的根视图,包含标题栏和内容区域。
- 创建并返回一个
-
generateLayout:- 创建并初始化窗口布局,包括内容区域(
mContentParent)。 - 根据主题选择窗口样式(如全屏、ActionBar 等)。
- 创建并初始化窗口布局,包括内容区域(
3.2 加载布局文件
- 使用
LayoutInflater将 XML 布局解析成 View,并添加到mContentParent中:
java
复制代码
LayoutInflater.from(mContext).inflate(layoutResID, mContentParent);
-
LayoutInflater.inflate:- 将 XML 布局文件解析为对应的 View 对象。
mContentParent是解析出的 View 的容器。
-
mContentParent:mContentParent是DecorView中的一个子视图,通常是一个FrameLayout,用于显示用户定义的内容视图。
3.3 通知视图更新
- 调用
mContentParent.requestApplyInsets()通知系统重新计算窗口的Insets(内边距),以适配状态栏、导航栏等系统 UI 元素。
4. DecorView 和 View 层级结构
setContentView 执行完毕后,视图层级结构如下:
plaintext
复制代码
DecorView
├── StatusBarBackgroundView (状态栏背景,可能存在)
├── NavigationBarBackgroundView (导航栏背景,可能存在)
└── ContentParent (用户内容区域)
└── 用户定义的布局 (即 R.layout.activity_main)
-
DecorView:- 整个窗口的顶层视图,管理窗口的背景、状态栏、导航栏等。
-
ContentParent:DecorView的子 View,通常是一个FrameLayout,承载用户设置的内容视图。
5. View 渲染流程
当 setContentView 完成后,Android 系统会触发 View 的测量、布局和绘制流程。
-
WindowManager.addView:DecorView被添加到屏幕上。WindowManager是 Android 的窗口管理器,负责管理所有窗口的显示和排布。
-
DecorView的三大流程:-
测量(Measure):
- 测量所有子 View 的大小。
-
布局(Layout):
- 确定每个子 View 的位置。
-
绘制(Draw):
- 绘制每个子 View 的内容。
-
6. 总结流程
以下是继承 Activity 调用 setContentView 的完整流程:
plaintext
复制代码
Activity#setContentView
↓
PhoneWindow#setContentView
↓
PhoneWindow#installDecor
↓
Generate DecorView and ContentParent
↓
LayoutInflater#inflate (加载用户布局)
↓
Add View to ContentParent
↓
WindowManager#addView
↓
DecorView 渲染到屏幕
7. 总结
-
Activity#setContentView的核心逻辑在PhoneWindow中:PhoneWindow通过DecorView和ContentParent完成布局加载。
-
DecorView是窗口的顶层视图:- 它包含了状态栏、导航栏和内容区域(用户布局)。
-
视图加载与渲染分离:
setContentView只负责布局加载,真正的渲染由WindowManager和DecorView完成。
-
扩展性:
- 通过
DecorView和ContentParent,开发者可以定制窗口布局(如全屏、沉浸式状态栏等)。
- 通过
2 继承 AppCompatActivity 的流程
AppCompatActivity 是 Android 开发中常用的 Activity 基类,它继承自 FragmentActivity,并添加了对兼容性组件(如 Toolbar、ActionBar 等)的支持。由于它是对 Activity 的扩展,所以 setContentView 的调用流程不仅包含 Activity 的逻辑,还引入了 AppCompatDelegate 的逻辑。
下面是详细的源码分析和执行流程。
1. 基本调用过程
在 AppCompatActivity 中,调用 setContentView 的方式通常如下:
java
复制代码
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
}
从表面上看,与 Activity 的 setContentView 使用方法一致,但背后引入了更多支持库的逻辑。
2. AppCompatActivity 的 setContentView 方法
AppCompatActivity 中的 setContentView 并没有直接实现,而是依赖于 委托模式 来实现,核心逻辑是通过 AppCompatDelegate 实现的。
以下是 AppCompatActivity 的 setContentView 方法源码:
java
复制代码
@Override
public void setContentView(@LayoutRes int layoutResID) {
getDelegate().setContentView(layoutResID);
}
-
getDelegate():- 返回一个
AppCompatDelegate实例,用于管理兼容性功能(如主题、窗口特性等)。
- 返回一个
-
AppCompatDelegate:- 负责处理布局加载、窗口装饰等逻辑。
AppCompatDelegate的默认实现类是AppCompatDelegateImpl.
3. AppCompatDelegateImpl 的 setContentView
3.1 AppCompatDelegateImpl#setContentView
在 AppCompatDelegateImpl 中实现了具体的 setContentView 方法,以下是关键源码:
java
复制代码
@Override
public void setContentView(int resId) {
ensureSubDecor(); // 确保 SubDecor(窗口装饰)已初始化
ViewGroup contentParent = (ViewGroup) mSubDecor.findViewById(android.R.id.content);
contentParent.removeAllViews();
LayoutInflater.from(mContext).inflate(resId, contentParent);
mAppCompatWindowCallback.getWrapped().onContentChanged();
}
关键逻辑解析:
-
ensureSubDecor():- 确保
SubDecor(装饰视图)已被创建。 SubDecor是对DecorView的增强版本,支持兼容性组件(如Toolbar)。- 如果未创建,会根据主题和配置初始化
mSubDecor。
- 确保
-
找到内容区域:
- 通过
mSubDecor.findViewById(android.R.id.content)获取内容区域(ContentParent)。 ContentParent是SubDecor的一个子视图,用于加载用户定义的布局。
- 通过
-
移除已有视图:
- 调用
removeAllViews(),确保在添加新布局前清空内容区域。
- 调用
-
加载布局文件:
- 使用
LayoutInflater加载布局文件,将其解析为 View,并添加到ContentParent。
- 使用
-
回调通知:
- 调用
onContentChanged(),通知内容视图已更改。
- 调用
3.2 ensureSubDecor
ensureSubDecor 方法是整个 setContentView 流程的核心,它负责初始化和设置 SubDecor。以下是关键源码:
java
复制代码
private void ensureSubDecor() {
if (!mSubDecorInstalled) {
mSubDecor = createSubDecor(); // 创建 SubDecor
ViewGroup decorContent = (ViewGroup) mSubDecor.findViewById(android.R.id.content);
decorContent.setId(View.NO_ID);
Window window = getWindow();
window.setContentView(mSubDecor);
mSubDecorInstalled = true;
}
}
关键逻辑解析:
-
检查是否已安装:
- 通过
mSubDecorInstalled标志位判断是否已初始化。
- 通过
-
创建
SubDecor:- 调用
createSubDecor方法,根据主题创建一个合适的装饰视图。
- 调用
-
设置为窗口视图:
- 调用
Window#setContentView将SubDecor设置为窗口的根视图。
- 调用
-
标记已安装:
- 将
mSubDecorInstalled设置为true。
- 将
3.3 createSubDecor
createSubDecor 会根据主题创建合适的装饰视图,以下是关键逻辑:
java
复制代码
private ViewGroup createSubDecor() {
TypedArray a = mContext.obtainStyledAttributes(R.styleable.AppCompatTheme);
if (a.getBoolean(R.styleable.AppCompatTheme_windowActionBar, false)) {
subDecor = new ActionBarOverlayLayout(mContext);
LayoutInflater.from(mContext).inflate(R.layout.abc_screen_toolbar, subDecor);
} else {
subDecor = new ContentFrameLayout(mContext);
}
a.recycle();
return subDecor;
}
逻辑说明:
-
根据主题配置选择布局:
- 如果主题中启用了
ActionBar,会加载abc_screen_toolbar。 - 如果未启用,则直接创建一个
ContentFrameLayout。
- 如果主题中启用了
-
返回
SubDecor:- 返回创建好的
SubDecor,作为窗口视图的根视图。
- 返回创建好的
3.4 加载内容布局
当 SubDecor 初始化完成后,用户设置的布局会通过 LayoutInflater 添加到 ContentParent 中。
以下是加载内容布局的核心流程:
-
获取
ContentParent:- 通过
mSubDecor.findViewById(android.R.id.content)找到内容区域。
- 通过
-
解析 XML 布局:
- 使用
LayoutInflater将布局文件解析为 View。
- 使用
-
添加 View:
- 将解析好的 View 添加到
ContentParent中。
- 将解析好的 View 添加到
4. 渲染流程
当 setContentView 完成后,接下来的流程是视图的渲染,具体步骤包括:
-
View 的测量、布局、绘制:
- 通过
WindowManager.addView将DecorView添加到屏幕。 DecorView触发measure、layout和draw,最终渲染到屏幕。
- 通过
5. AppCompatActivity#setContentView 流程图
plaintext
复制代码
AppCompatActivity#setContentView
↓
AppCompatDelegate#setContentView
↓
AppCompatDelegateImpl#ensureSubDecor
↓
AppCompatDelegateImpl#createSubDecor
↓
DecorView(装饰视图创建)
↓
LayoutInflater#inflate(加载用户布局)
↓
ContentParent(添加内容视图)
↓
WindowManager#addView
↓
DecorView 渲染到屏幕
6. AppCompatActivity 与 Activity 的区别
-
装饰视图:
AppCompatActivity使用SubDecor,支持兼容性功能(如ActionBar、Toolbar)。Activity使用默认的DecorView。
-
兼容性支持:
AppCompatActivity通过AppCompatDelegate提供了主题切换、夜间模式等功能。Activity不提供这些功能。
-
组件支持:
AppCompatActivity支持使用Fragment、ViewModel等 Jetpack 组件。Activity仅提供基本功能。
7. 总结
-
AppCompatActivity#setContentView的核心:- 通过委托模式,将布局设置的逻辑交由
AppCompatDelegate实现。 - 引入了
SubDecor,增强了窗口装饰视图的兼容性。 - 最终加载用户布局到
ContentParent,并通过WindowManager渲染到屏幕。
- 通过委托模式,将布局设置的逻辑交由
-
意义:
- 增强了对主题和兼容性功能的支持。
- 简化了 ActionBar、Toolbar 等现代 UI 组件的使用。
3 PhoneWindow
创建PhoneWindow来源
-
应用主界面(Activity)
-
创建来源:当一个 Activity 启动时,
ActivityThread会调用Activity.attach方法,此时会创建一个PhoneWindow并将其与 Activity 绑定。 -
主要职责:
- 负责 Activity 的窗口内容(
DecorView)。 - 管理窗口特性(如全屏、标题栏)。
- 处理用户输入和窗口事件。
- 负责 Activity 的窗口内容(
源码分析:
java 复制代码 public void attach(Context context, ActivityThread aThread) { mWindow = new PhoneWindow(this, window, activityConfigCallback); ... } -
-
对话框(Dialog)
-
创建来源:当创建一个
Dialog(如AlertDialog)时,Dialog的构造方法会创建一个PhoneWindow。 -
主要职责:
- 管理对话框的内容和样式。
- 处理弹出窗口的显示和消失逻辑。
源码分析:
java 复制代码 public Dialog(@NonNull Context context, @StyleRes int themeResId) { mWindow = new PhoneWindow(context); ... } -
-
系统窗口(Toast、悬浮窗等)
-
创建来源:系统服务或者特殊场景需要创建
Window时(如Toast的展示),也会使用PhoneWindow作为其窗口的实现。 -
主要职责:
- 管理系统层窗口的内容。
- 处理透明背景、无边框等特殊需求。
示例场景:
- Toast 提示框:在 Toast 的显示过程中,
WindowManager会使用PhoneWindow来管理窗口。 - 悬浮窗(如系统级工具悬浮窗):也是通过
PhoneWindow实现窗口视图的管理。
-
创建 PhoneWindow 的关键位置
1. 在 Activity 中创建
PhoneWindow 由 ActivityThread 在 Activity 的生命周期中通过 attach 方法创建。
java
复制代码
ActivityThread.java:
Activity.attach() -> new PhoneWindow()
2. 在 Dialog 中创建
Dialog 的构造函数中直接创建了 PhoneWindow:
java
复制代码
Dialog.java:
mWindow = new PhoneWindow(context);
3. 在系统服务中使用
如 Toast 使用 WindowManager 添加 PhoneWindow:
java
复制代码
Toast.java:
mWindow = new PhoneWindow(context.getApplicationContext());
总结
PhoneWindow 的创建主要发生在以下三类场景:
- Activity:
ActivityThread.attach方法中初始化。 - Dialog:
Dialog的构造函数中初始化。 - 系统窗口:如
Toast和悬浮窗场景,由WindowManager或系统服务创建。
每类 PhoneWindow 的职责有所不同,但其核心作用是管理窗口的视图结构和行为逻辑,是 Android UI 系统中至关重要的一部分。
4 面试题
- LayoutInflate的参数的作用
// 方式一:将布局添加成功
View view = inflater.inflate(R.layout.inflate_layout, ll, true);
// 方式二:报错,一个View只能有一个父亲(The specified child already has a parent.)
View view = inflater.inflate(R.layout.inflate_layout, ll, true); // 已经addView
ll.addView(view);
// 方式三:布局成功,第三个参数为false // 目的:想要 inflate_layout 的根节点的属性(宽高)有效,又不想让其处于某一个容器中
View view = inflater.inflate(R.layout.inflate_layout, ll, false);
ll.addView(view);
// 方式四:root = null,这个时候不管第三个参数是什么,显示效果一样 // inflate_layout 的根节点的属性(宽高)设置无效,只是包裹子View, // 但是子View(Button)有效,因为Button是出于容器下的
View view = inflater.inflate(R.layout.inflate_layout, null, false);
ll.addView(view);
- 描述下merge、include、ViewStub标签的特点
include:
- 不能作为根元素,需要放在 ViewGroup中
- findViewById查找不到目标控件,这个问题出现的前提是在使用include时设置了id,而在findViewById时却用了被include进来的布局的根元素id。
- 为什么会报空指针呢? 如果使用include标签时设置了id,这个id就会覆盖 layout根view中设置的id,从而找不到这个id 代码:LayoutInflate.parseInclude --》final int id = a.getResourceId(R.styleable.Include_id, View.NO_ID); --》if (id != View.NO_ID) { view.setId(id); }
merge:
- merge标签必须使用在根布局
- 因为merge标签并不是View,所以在通过LayoutInflate.inflate()方法渲染的时候,第二个参数必须指定一个父容器, 且第三个参数必须为true,也就是必须为merge下的视图指定一个父亲节点.
- 由于merge不是View所以对merge标签设置的所有属性都是无效的.
ViewStub:就是一个宽高都为0的一个View,它默认是不可见的
- 类似include,但是一个不可见的View类,用于在运行时按需懒加载资源,只有在代码中调用了viewStub.inflate() 或者viewStub.setVisible(View.visible)方法时才内容才变得可见。
- 这里需要注意的一点是,当ViewStub被inflate到parent时,ViewStub就被remove掉了,即当前view hierarchy中不再存在ViewStub, 而是使用对应的layout视图代替。