Android-高级 UI-01-setContentView

170 阅读10分钟

1 继承 Activity 的流程

image.png PhoneWindow.setContentView --- 主要目的 创建 DecorView 拿到 Content

在 Android 中,Activity 是应用的基本组件,它提供了 setContentView 方法用于设置布局文件。setContentView 的背后涉及多个层级的调用,最终实现了布局加载和显示到屏幕的过程。

下面对继承 Activity 时调用 setContentView 的流程进行源码分析。


1. setContentView 的调用

通常在 ActivityonCreate 方法中调用 setContentView

java
复制代码
@Override
protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.activity_main);
}
  • setContentView(int layoutResID) 接收一个布局资源文件 ID,并加载该布局到当前 Activity 的窗口中。
  • 它的核心逻辑由 ActivityWindowWindowManager 实现。

2. setContentView 的方法源码

Activity 中,setContentView 是一个简单的封装,主要调用了 Window 对象的 setContentView 方法。

java
复制代码
@Override
public void setContentView(@LayoutRes int layoutResID) {
    getWindow().setContentView(layoutResID);
    initWindowDecorActionBar();
}

关键点解析:

  1. getWindow()

    • 返回 Activity 所关联的 Window 对象,Window 的默认实现是 PhoneWindow
    • PhoneWindow 是 Android 系统中窗口的具体实现类,负责管理 DecorView。
  2. setContentView 的实际实现

    • PhoneWindow#setContentView 实现。
  3. 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() 方法,确保 DecorViewmContentParent 已初始化。
java
复制代码
private void installDecor() {
    if (mDecor == null) {
        mDecor = generateDecor(); // 创建 DecorView
    }
    if (mContentParent == null) {
        mContentParent = generateLayout(mDecor); // 创建内容区域
    }
}
  1. generateDecor

    • 创建并返回一个 DecorView 对象。
    • DecorView 是整个窗口的根视图,包含标题栏和内容区域。
  2. generateLayout

    • 创建并初始化窗口布局,包括内容区域(mContentParent)。
    • 根据主题选择窗口样式(如全屏、ActionBar 等)。

3.2 加载布局文件
  • 使用 LayoutInflater 将 XML 布局解析成 View,并添加到 mContentParent 中:
java
复制代码
LayoutInflater.from(mContext).inflate(layoutResID, mContentParent);
  1. LayoutInflater.inflate

    • 将 XML 布局文件解析为对应的 View 对象。
    • mContentParent 是解析出的 View 的容器。
  2. mContentParent

    • mContentParentDecorView 中的一个子视图,通常是一个 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 的测量、布局和绘制流程。

  1. WindowManager.addView

    • DecorView 被添加到屏幕上。
    • WindowManager 是 Android 的窗口管理器,负责管理所有窗口的显示和排布。
  2. 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. 总结

  1. Activity#setContentView 的核心逻辑在 PhoneWindow

    • PhoneWindow 通过 DecorViewContentParent 完成布局加载。
  2. DecorView 是窗口的顶层视图

    • 它包含了状态栏、导航栏和内容区域(用户布局)。
  3. 视图加载与渲染分离

    • setContentView 只负责布局加载,真正的渲染由 WindowManagerDecorView 完成。
  4. 扩展性

    • 通过 DecorViewContentParent,开发者可以定制窗口布局(如全屏、沉浸式状态栏等)。

2 继承 AppCompatActivity 的流程

image.png

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);
}

从表面上看,与 ActivitysetContentView 使用方法一致,但背后引入了更多支持库的逻辑。


2. AppCompatActivitysetContentView 方法

AppCompatActivity 中的 setContentView 并没有直接实现,而是依赖于 委托模式 来实现,核心逻辑是通过 AppCompatDelegate 实现的。

以下是 AppCompatActivitysetContentView 方法源码:

java
复制代码
@Override
public void setContentView(@LayoutRes int layoutResID) {
    getDelegate().setContentView(layoutResID);
}
  • getDelegate()

    • 返回一个 AppCompatDelegate 实例,用于管理兼容性功能(如主题、窗口特性等)。
  • AppCompatDelegate

    • 负责处理布局加载、窗口装饰等逻辑。
    • AppCompatDelegate 的默认实现类是 AppCompatDelegateImpl.

3. AppCompatDelegateImplsetContentView

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();
}
关键逻辑解析
  1. ensureSubDecor()

    • 确保 SubDecor(装饰视图)已被创建。
    • SubDecor 是对 DecorView 的增强版本,支持兼容性组件(如 Toolbar)。
    • 如果未创建,会根据主题和配置初始化 mSubDecor
  2. 找到内容区域

    • 通过 mSubDecor.findViewById(android.R.id.content) 获取内容区域(ContentParent)。
    • ContentParentSubDecor 的一个子视图,用于加载用户定义的布局。
  3. 移除已有视图

    • 调用 removeAllViews(),确保在添加新布局前清空内容区域。
  4. 加载布局文件

    • 使用 LayoutInflater 加载布局文件,将其解析为 View,并添加到 ContentParent
  5. 回调通知

    • 调用 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;
    }
}
关键逻辑解析
  1. 检查是否已安装

    • 通过 mSubDecorInstalled 标志位判断是否已初始化。
  2. 创建 SubDecor

    • 调用 createSubDecor 方法,根据主题创建一个合适的装饰视图。
  3. 设置为窗口视图

    • 调用 Window#setContentViewSubDecor 设置为窗口的根视图。
  4. 标记已安装

    • 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;
}
逻辑说明
  1. 根据主题配置选择布局

    • 如果主题中启用了 ActionBar,会加载 abc_screen_toolbar
    • 如果未启用,则直接创建一个 ContentFrameLayout
  2. 返回 SubDecor

    • 返回创建好的 SubDecor,作为窗口视图的根视图。

3.4 加载内容布局

SubDecor 初始化完成后,用户设置的布局会通过 LayoutInflater 添加到 ContentParent 中。

以下是加载内容布局的核心流程:

  1. 获取 ContentParent

    • 通过 mSubDecor.findViewById(android.R.id.content) 找到内容区域。
  2. 解析 XML 布局:

    • 使用 LayoutInflater 将布局文件解析为 View。
  3. 添加 View:

    • 将解析好的 View 添加到 ContentParent 中。

4. 渲染流程

setContentView 完成后,接下来的流程是视图的渲染,具体步骤包括:

  1. View 的测量、布局、绘制

    • 通过 WindowManager.addViewDecorView 添加到屏幕。
    • DecorView 触发 measurelayoutdraw,最终渲染到屏幕。

5. AppCompatActivity#setContentView 流程图

plaintext
复制代码
AppCompatActivity#setContentView
    ↓
AppCompatDelegate#setContentView
    ↓
AppCompatDelegateImpl#ensureSubDecor
    ↓
AppCompatDelegateImpl#createSubDecor
    ↓
DecorView(装饰视图创建)
    ↓
LayoutInflater#inflate(加载用户布局)
    ↓
ContentParent(添加内容视图)
    ↓
WindowManager#addView
    ↓
DecorView 渲染到屏幕

6. AppCompatActivityActivity 的区别

  1. 装饰视图

    • AppCompatActivity 使用 SubDecor,支持兼容性功能(如 ActionBarToolbar)。
    • Activity 使用默认的 DecorView
  2. 兼容性支持

    • AppCompatActivity 通过 AppCompatDelegate 提供了主题切换、夜间模式等功能。
    • Activity 不提供这些功能。
  3. 组件支持

    • AppCompatActivity 支持使用 FragmentViewModel 等 Jetpack 组件。
    • Activity 仅提供基本功能。

7. 总结

  • AppCompatActivity#setContentView 的核心

    1. 通过委托模式,将布局设置的逻辑交由 AppCompatDelegate 实现。
    2. 引入了 SubDecor,增强了窗口装饰视图的兼容性。
    3. 最终加载用户布局到 ContentParent,并通过 WindowManager 渲染到屏幕。
  • 意义

    1. 增强了对主题和兼容性功能的支持。
    2. 简化了 ActionBar、Toolbar 等现代 UI 组件的使用。

3 PhoneWindow

创建PhoneWindow来源

  1. 应用主界面(Activity)

    • 创建来源:当一个 Activity 启动时,ActivityThread 会调用 Activity.attach 方法,此时会创建一个 PhoneWindow 并将其与 Activity 绑定。

    • 主要职责

      • 负责 Activity 的窗口内容(DecorView)。
      • 管理窗口特性(如全屏、标题栏)。
      • 处理用户输入和窗口事件。

    源码分析

    java
    复制代码
    public void attach(Context context, ActivityThread aThread) {
        mWindow = new PhoneWindow(this, window, activityConfigCallback);
        ...
    }
    

  1. 对话框(Dialog)

    • 创建来源:当创建一个 Dialog(如 AlertDialog)时,Dialog 的构造方法会创建一个 PhoneWindow

    • 主要职责

      • 管理对话框的内容和样式。
      • 处理弹出窗口的显示和消失逻辑。

    源码分析

    java
    复制代码
    public Dialog(@NonNull Context context, @StyleRes int themeResId) {
        mWindow = new PhoneWindow(context);
        ...
    }
    

  1. 系统窗口(Toast、悬浮窗等)

    • 创建来源:系统服务或者特殊场景需要创建 Window 时(如 Toast 的展示),也会使用 PhoneWindow 作为其窗口的实现。

    • 主要职责

      • 管理系统层窗口的内容。
      • 处理透明背景、无边框等特殊需求。

    示例场景

    • Toast 提示框:在 Toast 的显示过程中,WindowManager 会使用 PhoneWindow 来管理窗口。
    • 悬浮窗(如系统级工具悬浮窗):也是通过 PhoneWindow 实现窗口视图的管理。

创建 PhoneWindow 的关键位置

1. 在 Activity 中创建

PhoneWindowActivityThread 在 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 的创建主要发生在以下三类场景:

  1. ActivityActivityThread.attach 方法中初始化。
  2. DialogDialog 的构造函数中初始化。
  3. 系统窗口:如 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:

  1. 不能作为根元素,需要放在 ViewGroup中
  2. findViewById查找不到目标控件,这个问题出现的前提是在使用include时设置了id,而在findViewById时却用了被include进来的布局的根元素id。
    1. 为什么会报空指针呢? 如果使用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:

  1. merge标签必须使用在根布局
  2. 因为merge标签并不是View,所以在通过LayoutInflate.inflate()方法渲染的时候,第二个参数必须指定一个父容器, 且第三个参数必须为true,也就是必须为merge下的视图指定一个父亲节点.
  3. 由于merge不是View所以对merge标签设置的所有属性都是无效的.

ViewStub:就是一个宽高都为0的一个View,它默认是不可见的

  1. 类似include,但是一个不可见的View类,用于在运行时按需懒加载资源,只有在代码中调用了viewStub.inflate() 或者viewStub.setVisible(View.visible)方法时才内容才变得可见。
  2. 这里需要注意的一点是,当ViewStub被inflate到parent时,ViewStub就被remove掉了,即当前view hierarchy中不再存在ViewStub, 而是使用对应的layout视图代替。