Android DecorView 一窥全貌(上)
前言
我们都知道DecorView是最顶层View(根View),它是怎么创建和使用的呢?
通过本篇文章,你将了解到:
1、DecorView创建过程。
2、DecorView与Window/Activity关系
3、DecorView各个子View创建
DecorView创建过程
来回顾一下Activity创建过程:
image.png
AMS管理着Activity生命周期,每当切换Activity状态时通过Binder告诉ActivityThread,ActivityThread通过Handler切换到主线程(UI线程)执行,最终分别调用到我们熟知的onCreate(xx)/onResume()方法。
更多细节请移步:Android Activity创建到View的显示过程
本次关注的重点是setContentView(xx)方法。
简单布局分析
相信大家都知道,该方法是将我们布局(layout)文件添加到一个id为“android.R.id.content”的ViewGroup里,我们只需要关心layout的内容。那么R.id.content是整个View树的根布局吗?来看看一个最简单的Activity的布局:
my_layout.xml
<?xml version="1.0" encoding="utf-8"?>
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:background="@color/colorGreen"
android:id="@+id/my_root"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".MainActivity">
<Button
android:id="@+id/btn"
android:text="hello"
android:onClick="onClick"
android:layout_width="wrap_content"
android:layout_height="wrap_content">
</Button>
</FrameLayout>
效果图:
image.png
把布局观察器打开:
image.png
可以看出,"content"布局对应的是ContentFrameLayout,它并不是View树的根布局,根布局是DecorView,DecorView和ContentFrameLayout之间还有几个View,接下来我们就来了解上图的这些View是怎么确定的。
setContentView(xx)源码解析
从Activity onCreate(xx)看起
AppCompatDelegateImpl
@Override
protected void onCreate(Bundle savedInstanceState) {
//先调用父类构造函数
//初始化一些必要变量
super.onCreate(savedInstanceState);
setContentView(R.layout.layout_path);
}
创建及初始化AppCompatDelegateImpl
AppCompatActivity.java
protected void onCreate(@Nullable Bundle savedInstanceState) {
final AppCompatDelegate delegate = getDelegate();
delegate.installViewFactory();
//交给AppCompatDelegate代理
delegate.onCreate(savedInstanceState);
super.onCreate(savedInstanceState);
}
AppCompatActivity将工作交给了AppCompatDelegate处理,而AppCompatDelegate是抽象类,真正工作的是其子类:AppCompatDelegateImpl。
@Override
public void onCreate(Bundle savedInstanceState) {
ensureWindow();
}
private void ensureWindow() {
//省略
//mHost为创建此代理的Activity
if (mWindow == null && mHost instanceof Activity) {
attachToWindow(((Activity) mHost).getWindow());
}
}
private void attachToWindow(@NonNull Window window) {
if (mWindow != null) {
throw new IllegalStateException(
"AppCompat has already installed itself into the Window");
}
//接收各种事件的window callback,实际就是将callback再包了一层
mAppCompatWindowCallback = new AppCompatWindowCallback(callback);
window.setCallback(mAppCompatWindowCallback);
//省略
//使用activity的window
mWindow = window;
}
上面代码的主要工作是关联AppCompatDelegateImpl和Activity的window变量。
接着来看看setContentView(R.layout.layout_path)本尊
Activity的setContentView(xx)最终调用了AppCompatDelegateImpl里的setContentView(xx):
AppCompatDelegateImpl.java
@Override
public void setContentView(int resId) {
//创建SubDecor,从名字可以猜测一下是DecorView的子View
ensureSubDecor();
//找到R.id.content布局
ViewGroup contentParent = mSubDecor.findViewById(android.R.id.content);
//清空content里的子View
contentParent.removeAllViews();
//加载在activity里设置的layout,并添加到content里
LayoutInflater.from(mContext).inflate(resId, contentParent);
}
可以看出,我们自定义的layout最后被添加到contentParent里,而contentParent是从mSubDecor里找寻的,因此我们重点来看看ensureSubDecor()方法。
ensureSubDecor里调用的是createSubDecor()方法来创建subDecor。
subDecor创建
AppCompatDelegateImpl.java
private ViewGroup createSubDecor() {
//根据Activity的主题设置不同设置window的feature
TypedArray a = mContext.obtainStyledAttributes(R.styleable.AppCompatTheme);
//确保window已经关联
ensureWindow();
//获取DecorView,没有则创建
mWindow.getDecorView();
ViewGroup subDecor = null;
//有标题
if (!mWindowNoTitle) {
//根据条件给subDecor加载不同的布局文件
if (mIsFloating) {
subDecor = (ViewGroup) inflater.inflate(
R.layout.abc_dialog_title_material, null);
} else if (mHasActionBar) {
//加载subDecor布局
subDecor = (ViewGroup) LayoutInflater.from(themedContext)
.inflate(R.layout.abc_screen_toolbar, null);
//寻找subDecor子布局
mDecorContentParent = (DecorContentParent) subDecor
.findViewById(R.id.decor_content_parent);
mDecorContentParent.setWindowCallback(getWindowCallback());
}
} else {
//省略
}
//标题栏标题
if (mDecorContentParent == null) {
mTitleView = (TextView) subDecor.findViewById(R.id.title);
}
//寻找subDecor子布局,命名为contentView
final ContentFrameLayout contentView = (ContentFrameLayout) subDecor.findViewById(
R.id.action_bar_activity_content);
//找到window里content布局,实际上找的是DecorView里名为content的布局
final ViewGroup windowContentView = (ViewGroup) mWindow.findViewById(android.R.id.content);
if (windowContentView != null) {
//挨个移除windowContentView的子View,并将之添加到contentView里
while (windowContentView.getChildCount() > 0) {
final View child = windowContentView.getChildAt(0);
windowContentView.removeViewAt(0);
contentView.addView(child);
}
//把windowContentView id去掉,之前名为content
windowContentView.setId(View.NO_ID);
//将"content"名赋予contentView
contentView.setId(android.R.id.content);
}
//把subDecor添加为Window的contentView,实际上添加为DecorView的子View。该方法后面再具体分析
mWindow.setContentView(subDecor);
return subDecor;
}
该方法较长,省略了一些细枝末节,主体功能是:
- 根据不同的前置条件,加载不同的布局文件作为subDecor
- 将subDecor加入到DecorView里,subDecor本身是ViewGroup
subDecor创建了,那么DecorView啥时候创建呢?createSubDecor()有段代码:
mWindow.getDecorView()
DecorView创建
mWindow之前分析过是Activity的window变量,它的实现类是PhoneWindow。
PhoneWindow.java
View getDecorView() {
if (mDecor == null || mForceDecorInstall) {
installDecor();
}
return mDecor;
}
private void installDecor() {
//mDecor作为PhoneWindow变量
if (mDecor == null) {
//新建DecorView,并关联window
mDecor = generateDecor(-1);
}
if (mContentParent == null) {
//创建DecorView子布局
mContentParent = generateLayout(mDecor);
}
}
protected DecorView generateDecor(int featureId) {
Context context;
if (mUseDecorContext) {
Context applicationContext = getContext().getApplicationContext();
if (applicationContext == null) {
context = getContext();
} else {
//applicationContext是Application
//getContext 是Activity
//DecorContext 继承自ContextThemeWrapper
context = new DecorContext(applicationContext, getContext());
if (mTheme != -1) {
context.setTheme(mTheme);
}
}
} else {
context = getContext();
}
//this 指的是phoneWindow,赋值给mWindow变量
return new DecorView(context, featureId, this, getAttributes());
}
从上可知:
DecorView继承自FrameLayout
注:DecorContext 有关请移步:Android各种Context的前世今生
重点来看看
mContentParent = generateLayout(mDecor);
PhoneWindow.java
protected ViewGroup generateLayout(DecorView decor) {
//获取WindowStyle
TypedArray a = getWindowStyle();
//根据style设置各种flags和feature
//省略...
WindowManager.LayoutParams params = getAttributes();
//待加载的布局资源id
int layoutResource;
int features = getLocalFeatures();
//根据不同的feature确定不同的布局
if ((features & (1 << FEATURE_SWIPE_TO_DISMISS)) != 0) {
layoutResource = R.layout.screen_swipe_dismiss;
setCloseOnSwipeEnabled(true);
} else if (features) {
//省略
} else {
//默认加载该布局
layoutResource = R.layout.screen_simple;
}
//实例化上面确定的布局,并将该布局添加到DecorView里
mDecor.onResourcesLoaded(mLayoutInflater, layoutResource);
//获取名为ID_ANDROID_CONTENT的子View
//public static final int ID_ANDROID_CONTENT = com.android.internal.R.id.content;
//实际上就是content
ViewGroup contentParent = (ViewGroup)findViewById(ID_ANDROID_CONTENT);
return contentParent;
}
将确定的布局加载,并添加到DecorView里。
void onResourcesLoaded(LayoutInflater inflater, int layoutResource) {
//实例化布局
final View root = inflater.inflate(layoutResource, null);
if (mDecorCaptionView != null) {
//省略
} else {
//添加到DecorView里
addView(root, 0, new ViewGroup.LayoutParams(MATCH_PARENT, MATCH_PARENT));
}
//记录root
mContentRoot = (ViewGroup) root;
}
总结来说,mWindow.getDecorView()做了哪些事呢?
- 创建DecorView,并关联PhoneWindow与DecorView(互相持有)
- 根据feature确定DecorView子布局,并添加填充满DecorView
可能看到上面的分析还是一头雾水,没关系先来理一下DecorView和SubDecor关系。
1、首先创建DecorView,并添加其子View,该子View在DecorView里称为:mContentRoot。当前场景下使用的子View资源id:R.layout.screen_simple;
2、mContentRoot里有名为"R.id.content"的子View,其在PhoneWindow里称作为:mContentParent
3、当前场景下使用的subDecor资源id:R.layout.abc_screen_toolbar,实例化该资源文件获得subDecor实例
4、该subDecor里有子View资源id:R.id.action_bar_activity_content,在AppCompatDelegateImpl里命名为:contentView
5、DecorView添加subDecor过程:取出DecorView里的mContentParent,并移除里面的子View,并将移除的子View添加到subDecor里的contentView里。
6、将contentView更名为“R.id.content”
7、最后通过mWindow.setContentView(subDecor),将subDecor添加到DecorView里
继续分析mWindow.setContentView(subDecor)
PhoneWindow.java
public void setContentView(View view, ViewGroup.LayoutParams params) {
if (mContentParent == null) {
//为空,说明DecorView没构造
installDecor();
} else if (!hasFeature(FEATURE_CONTENT_TRANSITIONS)) {
//清空子View
mContentParent.removeAllViews();
}
if (hasFeature(FEATURE_CONTENT_TRANSITIONS)) {
//省略
} else {
//View添加到mContentParent
mContentParent.addView(view, params);
}
//省略
}
以上就是DecorView的创建过程。
DecorView创建完成后,再将自定义布局添加到DecorView里名为R.id.content布局里。至此setContentView分析完毕,老规矩用图表示整个流程。
setContentView图示
image.pngDecorView与Window/Activity关系
Activity:
我们平时把Activity当做一个页面,直观上看这个页面承载着我们的布局显示以及相应的逻辑处理。Activity有生命周期,在不同的状态下做不同的处理。
Window
顾名思义,就是我们所见的窗口,Activity显示的内容实际上是展示在Window上的,对应的是PhoneWindow。
DecorView
Window本身是比较抽象的,可以想象成为一个管理的东西,它管理的就是ViewTree,而DecorView是ViewTree的根。我们具体的界面展示是通过View完成的,把设计好的View添加到DecorView里,整个ViewTree就构建起来了,最终添加到Window里。
因此:
- Activity管理着Window,Window管理着DecorView,Activity间接管理着DecorView
- Activity处在“create"状态的时候创建对应的Window,并在setContentView里创建DecorView,最终添加自定义布局到DecorView里。此时,Activity创建完毕,Window创建完毕,DecorView创建完毕,ViewTree构建成功,三者关系已建立。
- Acitivity处在“resume"状态的时候,通过Window的管理类将DecorView添加到Window里,并提交更新DecorView的请求。当下次屏幕刷新信号到来之时,执行ViewTree三大流程(测量,布局,绘制),最终的效果就是我们的自定义布局显示在屏幕上。
“注”:指的状态执行时机是处在ActivityThread里的。
这么看起来还是不太顺,我们所说的三者的联系实际上在代码里来看就是:“不是你持有我就是我引用你”,通过类图来看看三者之间的引用情况:
image.png
可以看出,Window作为Activity和DecorView的联结者。
注:Activity也持有DecorView的引用,只是不对外提供获取的接口。因此一般通过Activity获取Window,再获取DecorView。
在UI上看来,可以这么理解:
image.png
注:这图只是便于理解抽象关系,实际上对于Activity来说并没有尺寸的说法。
后续
限于当前篇幅,后续内容重开一篇:
Android DecorView 一窥全貌(下)
注:以上关于DecorView、subDecor、标题栏、布局文件和区块尺寸的选择是基于当前的demo的。可能你所使用的主题、设置的属性和本文不同导致布局效果差异,请注意甄别。
源码基于:Android 10.0