4、setContentView源码解析:PhoneWindow与DecorView的创建,布局加载到内容区域

好,咱们今天来啃一块硬骨头——setContentView。说实话,这个方法是每个Android开发者最早接触的API之一,但真正理解它背后发生了什么的人,其实不多。我记得我刚入行那会儿,以为它就是简单地把一个布局文件塞进屏幕,后来踩了几个坑才明白,这背后是一整套窗口机制的启动流程。

4.1 从Activity说起

咱们平时写的Activity,在onCreate里调用的第一行代码往往是:

setContentView(R.layout.activity_main);

那这个setContentView到底是谁的方法?你可能会说是Activity的。没错,但Activity自己并不干活,它把活儿委托给了谁?

答案是——PhoneWindow

每个Activity在创建时,都会关联一个Window对象。这个Window的默认实现就是PhoneWindow。说白了,Activity就是个壳,真正管理视图层的是PhoneWindow。

核心要点:Activity.setContentView() 内部调用的是 PhoneWindow.setContentView()。

4.2 PhoneWindow的创建时机

PhoneWindow是什么时候创建的呢?我建议你去看一下Activity.attach()方法。这个方法在Activity被创建时调用,里面有一行关键代码:

mWindow = new PhoneWindow(this, window, activityConfigCallback);

嗯,就是这里。Activity和PhoneWindow的绑定,发生在attach阶段。这时候Window还没有任何视图内容,只是一个空壳。

4.3 DecorView的诞生

接下来,我们进入PhoneWindow.setContentView()的源码。我简化一下核心逻辑:

public void setContentView(int layoutResID) {
    // 1. 如果DecorView还没创建,就创建它
    if (mContentParent == null) {
        installDecor();
    } else {
        // 2. 如果已经有了,先移除所有子View
        mContentParent.removeAllViews();
    }
    // 3. 把我们的布局文件inflate到mContentParent里
    mLayoutInflater.inflate(layoutResID, mContentParent);
}

这里有个关键方法——installDecor()。它做了两件事:

  • 创建DecorView
  • 找到内容区域的容器(mContentParent)

DecorView是什么?它是整个Window的根View。你可以把它理解成一个画框,而我们写的布局就是画框里的画。DecorView本身是一个FrameLayout,它包含了系统提供的标题栏、状态栏区域,以及我们真正放内容的区域。

个人经验:我曾经在自定义Dialog时,直接操作了DecorView,结果导致触摸事件分发出了问题。后来才意识到,DecorView是系统管理的,我们最好只操作它内部的mContentParent。

4.4 内容区域mContentParent

mContentParent是DecorView里的一个子ViewGroup。它的ID是android.R.id.content。说白了,我们写的R.layout.activity_main最终就是被塞进了这个容器里。

那mContentParent是怎么找到的呢?看源码:

private void installDecor() {
    if (mDecor == null) {
        mDecor = generateDecor(-1);
    }
    if (mContentParent == null) {
        mContentParent = generateLayout(mDecor);
    }
}

generateLayout方法会根据当前的主题,选择合适的布局文件。比如你有没有标题栏、有没有ActionBar,都会影响最终加载的布局模板。这些模板里都包含了一个ID为content的FrameLayout。

注意:如果你在主题里设置了android:windowNoTitle=true,那么加载的布局模板就不包含标题栏区域。这时候mContentParent的位置会往上移,覆盖掉原本标题栏的空间。

4.5 布局加载到内容区域

回到setContentView的最后一步:

mLayoutInflater.inflate(layoutResID, mContentParent);

这一步其实很简单,就是把我们的XML布局解析成View树,然后添加到mContentParent里。但这里有个细节——inflate方法的第二个参数传了true(默认),表示直接把解析出来的View添加到父容器中。

如果你自己写代码时,有时候会看到这样的写法:

View view = LayoutInflater.from(context).inflate(R.layout.item, parent, false);

第三个参数传false,表示只解析但不添加。这个区别,我在做ListView适配器时踩过坑——如果不传false,会导致View重复添加,界面直接崩掉。

4.6 整体流程总结

咱们把整个流程串起来,你感受一下:

  1. Activity启动,调用attach(),创建PhoneWindow
  2. onCreate里调用setContentView
  3. PhoneWindow检查DecorView是否存在,不存在则创建
  4. 根据主题加载系统布局模板,找到mContentParent
  5. 把我们写的布局inflate到mContentParent里
  6. 整个View树构建完成,等待后续的onMeasure、onLayout、onDraw

说白了,setContentView干的就是一件事——搭好舞台,把我们的内容放上去。舞台是DecorView,舞台上的表演区域是mContentParent,而我们写的布局就是演员。

避坑指南:我曾经在项目里遇到过一个bug——某个页面启动特别慢。后来定位发现,是因为在setContentView之前,有人通过getWindow()修改了DecorView的一些属性,导致系统重新创建了DecorView。嗯,记住一点:在setContentView之前,DecorView还没创建,别急着操作它。

4.7 面试常问点

如果你去面试,面试官问起setContentView的流程,你可以这样回答:

  • Activity的setContentView委托给PhoneWindow
  • PhoneWindow先创建DecorView(如果还没创建)
  • 然后根据主题加载系统布局,找到ID为content的FrameLayout
  • 最后把我们的布局inflate到这个FrameLayout里

如果能再补充一句「DecorView是FrameLayout的子类,它包含了状态栏、标题栏和内容区域」,那就更完美了。

好,这一章就到这里。下一章咱们会深入DecorView的测量流程,看看Android是怎么计算每个View的大小的。到时候你会发现,onMeasure里的门道,比你想的要多得多。