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 整体流程总结
咱们把整个流程串起来,你感受一下:
- Activity启动,调用
attach(),创建PhoneWindow - 在
onCreate里调用setContentView - PhoneWindow检查DecorView是否存在,不存在则创建
- 根据主题加载系统布局模板,找到mContentParent
- 把我们写的布局inflate到mContentParent里
- 整个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里的门道,比你想的要多得多。