一、简介
这篇博客分析的是 API 28 最新的源码,来回顾一下我们的布局是怎么加载到 Activity 上的,采取的策略是先分析重要代码片段 + 阅后总结的形式。
二、Activity 的 setContentView()源码阅读
那么先看下 Activity.setContentView() 源码,这个看起来思路简单一些,AppCompatActivity中逻辑稍微不太一样。
// Activity#setContentView()
public void setContentView(@LayoutRes int layoutResID) {
getWindow().setContentView(layoutResID);
initWindowDecorActionBar();
}
private Window mWindow;
public Window getWindow() {
return mWindow;
}
// 这个 mWindow 就是 PhoneWindow,是在 Activity.attach()方法中进行初始化的
//API 23和28的源码这里开始就不太一样了,我们看最新的
mWindow = new PhoneWindow(this, window, activityConfigCallback);
- 我们在 Activity 中设置的布局,交给了 getWindow()获取的 Window 对象
- Window 是一个抽象类,其实现是
PhoneWindow
类,那么就转化为 PhoneWindow 的setContentView()
是怎么工作的
三、PhoneWindow 的 setContentView() 源码阅读
// PhoneWindow 类
ViewGroup mContentParent;
@Override
public void setContentView(int layoutResID) {
// mContentParent 是一个 ViewGroup,第一次是 null,会执行
if (mContentParent == null) {
installDecor();
} else if (!hasFeature(FEATURE_CONTENT_TRANSITIONS)) {
mContentParent.removeAllViews();
}
if (hasFeature(FEATURE_CONTENT_TRANSITIONS)) {
// 这里是判断是否做动画的,动画有起始和结束状态,抽象成了开始和结束的场景,回头专一写篇Android 里边的 场景动画
final Scene newScene = Scene.getSceneForLayout(mContentParent, layoutResID,
getContext());
transitionTo(newScene);
} else {
// 将我们在 Activity 中设置的布局,就这样添加到了 ViewGroup 容器中了
mLayoutInflater.inflate(layoutResID, mContentParent);
}
}
PhoneWindow 的 setContentView() 做了两件事
- 创建一个 mContentParent 容器,
实际上就是一个 id 为 content 的FrameLayout容器
- 将我们设置的布局,通过 inflate 的方式,添加到了这个容器中
官方示意这个 mContentParent 就是存放 window 内容的容器,这个容器可能是 DecorView 本身,或者是 DecorView 的一个子类
至于 DecorView 是什么,马上就分析到了。
四、installDecor() 分析
是通过调用 PhoneWindow 里边的 installDecor() 方法来创建 DecorView 和 mContentParent 的。
// PhoneWindow 类,省略了 N 行代码,只留下重点代码
private void installDecor() {
if (mDecor == null) {
mDecor = generateDecor(-1);
} else {
mDecor.setWindow(this);
}
if (mContentParent == null) {
mContentParent = generateLayout(mDecor);
// ***
}
//***
}
- 创建一个 DecorView
- 创建了一个 mContentParent 容器
4.1 DecorView 的实例化
// PhoneWindow 类
protected DecorView generateDecor(int featureId) {
Context context;
//*** 省略 context 的初始化来源方式
return new DecorView(context, featureId, this, getAttributes());
}
// Decorview 是一个 hide 的类,本质是一个 FrameLayout
public class DecorView extends FrameLayout{***}
DecorView 就是简单的通过 new 来实例化的。
4.2 mContentParent 的实例化
protected ViewGroup generateLayout(DecorView decor) {
TypedArray a = getWindowStyle();
// 这里是从系统主题里边读取一些配置(其实就是系统的自定义属性),设置进去的,比如说是否是悬浮窗口,
//是否有 title,是否有 actionBar,是否全屏等等 省略
//比如下面就是有无 ActionBar 的判断
if (a.getBoolean(R.styleable.Window_windowNoTitle, false)) {
requestFeature(FEATURE_NO_TITLE);
} else if (a.getBoolean(R.styleable.Window_windowActionBar, false)) {
// Don't allow an action bar if there is no title.
requestFeature(FEATURE_ACTION_BAR);
}
// ***
// 还省略了其它很多不关心的代码,比如会判断不同的 sdk 版本,走不同的配置,省略
// Inflate the window decor.
int layoutResource;
int features = getLocalFeatures();
// *** 省略一系列逻辑判断,最终会加载一个系统的布局文件,这个布局文件也即是添加到 DecorView 的布局 ID,
// 我们以下面这两个系统的布局文件为例
if ((features & (1 << FEATURE_ACTION_MODE_OVERLAY)) != 0) {
layoutResource = R.layout.screen_simple_overlay_action_mode;
} else {
layoutResource = R.layout.screen_simple;
}
// 重点代码来了,SDK API23 和 28这里也有所不同了
mDecor.startChanging();
mDecor.onResourcesLoaded(mLayoutInflater, layoutResource); // API28多出来的方法
// ID_ANDROID_CONTENT即 com.android.internal.R.id.content,就是一个 FrameLayout,
// 就是刚添加进 DecorView 中的布局上的一个 id
ViewGroup contentParent = (ViewGroup)findViewById(ID_ANDROID_CONTENT);
// 接下来就是设置 DecorView 的背景 标题 标题颜色等等,省略部分代码
mDecor.finishChanging();
return contentParent; // 将布局容器返回
}
我们对 generateLayout(DecorView)
方法做的事情做个总结:
- 获取系统的自定义属性作为配置,比如有没有标题栏啊,有没有状态栏啊,是否全屏等等
- 根据一系列的逻辑判断,最终会确定加载一个系统的 layout 布局
- 将该 layout 布局 xml 转化为一个 View 对象,添加到 DecorView 中
- 给 DecorView 设置一些属性,比如 background、title、titleColor 等等
- 将加载的系统的 layout布局文件中的,id 为 com.android.internal.R.id.content 的 FrameLayout 容器强转为 ViewGroup,并最终返回该实例
五、将系统的布局文件添加到 DecorView 上
在不同版本的 sdk 里边的处理逻辑是不一样的,之前看的 API23 的逻辑,更简单也更清晰一点:
mDecor.startChanging();
View in = mLayoutInflater.inflate(layoutResource, null);
decor.addView(in, new ViewGroup.LayoutParams(MATCH_PARENT, MATCH_PARENT));
mContentRoot = (ViewGroup) in;
ViewGroup contentParent = (ViewGroup)findViewById(ID_ANDROID_CONTENT);
直接将上一步确定下来的系统的布局资源文件转化为一个 view,然后通过 DecorView.add(View,LayoutParams)的方式,将一个系统的布局挂载到了 DecorView 上。
看下 API28 中的逻辑,通过mDecor.onResourcesLoaded(mLayoutInflater, layoutResource);
将逻辑单独提取到了 DecorView 类中,
// DecorView 类
void onResourcesLoaded(LayoutInflater inflater, int layoutResource) {
// ***
// 将最终确定下来的系统的布局,转为 View,添加到 DecorView 容器中
final View root = inflater.inflate(layoutResource, null);
if (mDecorCaptionView != null) {
if (mDecorCaptionView.getParent() == null) {
addView(mDecorCaptionView,
new ViewGroup.LayoutParams(MATCH_PARENT, MATCH_PARENT));
}
mDecorCaptionView.addView(root,
new ViewGroup.MarginLayoutParams(MATCH_PARENT, MATCH_PARENT));
} else {
// Put it below the color views.
addView(root, 0, new ViewGroup.LayoutParams(MATCH_PARENT, MATCH_PARENT));
}
mContentRoot = (ViewGroup) root;
initializeElevation();
}
六、系统的布局文件
加载的系统布局资源文件可以在 AS 里边直接看,前提是关联好源码;也可以在 sdk/platforms/android-28/data/res/layout 路径下查看。
那么系统的布局文件 [screen_simple.xml]长什么样子呢?
可以看到就是一个简单的 线性布局,有一个 ViewStub 子元素和一个 android:id/content
的 FrameLayout,我们注意下一这个 id。
看下系统的布局文件[screen_simple_overlay_action_mode.xml]又是怎么样的。
其实多看几个系统的布局文件大致都差不多,都会有一个 ViewStub 和一个 id 为 android:id/content
的 FrameLayout,只不过其它的布局文件中还有一些其它的 View 罢了,我们在 activity 中设置的布局 layout 其实就是加载到了 id 为 content 的 FrameLayout 容器中的。
上面我们已经分析过了,正是通过 ViewGroup contentParent = (ViewGroup)findViewById(ID_ANDROID_CONTENT);
来将该 FrameLayout 强转成 ViewGroup 并返回的,即是加载到了 contentParent 上了。
七、总结
我们在 Activity 中通过 setContentView(layoutResID)设置的布局,通过下面几步加载出来的
- 在 Activity 的 onCreate()中设置我们的布局,实际上是设置给了 Window
- 在 Activity 的 attach()方法中,进行Window 的实例化,即实例化 PhoneWindow 对象
- 调用 PhoneWindow 的 setContentView() 方法
- 在 PhoneWindow 类中创建 DecorView,将系统的一个布局转为View,然后添加到 DecorView 上
- 将我们自己写的布局填充到,上一步确定下来的系统布局中 id 为
R.id.content
的 FrameLayout 容器上
根据源码,我们绘制出来的 Activity 的层级结构如下:
低版本的 Android Studio 里边有个很好用的工具,Android Device Monitor
工具,可以用来页面的布局层级结构,高版本 as 上已经没有该 tool 的入口了,但是可以在 sdk/tools/目录下找 monitor
就可以了,但是我电脑上还是不能用,那么取而代之的是as 菜单里边(Tools ->Layout Inspector)提供了相对应的工具,但是只能查看到布局中 DecorView 节点的内容,下面简单放张图。
我们有一个布局如下:
该布局文件对应的 Layout Captures
为:
最后放一张,早些年写的笔记中的一张图: