Activity调用setContentView将布局添加到窗口的流程如图:
在深入了解setContentView之前,先提出以下疑问:
- 为什么调用setContentView就能将布局显示出来?
- 为什么requestFeature需要在setContentView之前调用?
- PhoneWindow和Window之间有什么关系?
- DecorView和我们的布局有什么关系?
- 为什么继承AppCompactActivity后,主题需要继承AppCompactTheme?
Window和PhoneWindow
Activity有三个setContentView重载方法:
public void setContentView(@LayoutRes int layoutResID) {
getWindow().setContentView(layoutResID);
initWindowDecorActionBar();
}
public void setContentView(View view) {
getWindow().setContentView(view);
initWindowDecorActionBar();
}
public void setContentView(View view, ViewGroup.LayoutParams params) {
getWindow().setContentView(view, params);
initWindowDecorActionBar();
}
从Activity的setContentView方法的实现来看第一步会调用Window的setContentView方法,那我们就来看看Window类,从注释中可以得知这个类的实例是被当做顶级View添加到了WindowManager
中,由WindowManager
管理。而PhoneWindow
是抽象类Window的唯一子类,他们之间的关系如下图:
来看看Window中的几个重要的方法:
加载Window的主题
通过Window的getWindowStyle方法从style.xml
中获取此应用程序窗口主题的属性,这个属性定义在platform_frameworks_base/core/res/res/values/attrs.xml
synchronized (this) {
if (mWindowStyle == null) {
mWindowStyle = mContext.obtainStyledAttributes(
com.android.internal.R.styleable.Window);
}
return mWindowStyle;
}
Window#findViewById
这个方法是我们最常用的方法之一,在Activity中调用findViewById
方法,内部会调用Window的findViewById
方法,最终调用的是View中的findViewById
方法,这里不做深入研究。
return getDecorView().findViewById(id);
Window#setContentView(int)
在Window中该方法是抽象方法,查看它的唯一子类PhoneWindow
中的实现。由于这个方法有三个重载方法,我们重点关注setContentView(int)
方法,另外两个重载方法大同小异。
PhoneWindow#setContentView(int)
- 调用installDecor()方法初始化mDecor和mContentParent,当再次调用setContentView方法时,如果没有添加场景转换动画,mContentParent会移除所有添加的View
if (mContentParent == null) {
installDecor();
} else if (!hasFeature(FEATURE_CONTENT_TRANSITIONS)) {
mContentParent.removeAllViews();
}
- 如果添加了场景转换动画,会执行此动画效果;否则调用LayoutInflater的inflate()方法将布局添加到mDecor的mContentParent中
if (hasFeature(FEATURE_CONTENT_TRANSITIONS)) {
final Scene newScene = Scene.getSceneForLayout(mContentParent, layoutResID,
getContext());
transitionTo(newScene);
}else {
mLayoutInflater.inflate(layoutResID, mContentParent);
}
这里出现了一个关键成员变量mContentParent
,看注释得知这个成员变量是一个用来存放应用程序窗口内容的View,它有可能是mDecor
本身,或是mDecor
下的子View。而mDecor
是应用程序窗口的顶级View。
DecorView的创建过程
初始化PhoneWindow
PhoneWindow的初始化是在Activity的attach
方法中调用的
mWindow = new PhoneWindow(this, window);
创建DecorView —— mDecor
DecorView是在PhoneWindow
中的generateDecor
方法中创建的
...
return new DecorView(context, featureId, this, getAttributes());
并在PhoneWindow
中的installDecor
方法赋值给成员变量mDecor
if (mDecor == null) {
mDecor = generateDecor(-1);
...
}
然后会在Activity启动过程中,将DecorView添加到PhoneWindow,可以参考DecorView是如何添加到窗口的?
创建ViewGroup —— mContentParent
DecorView是在PhoneWindow
中的generateLayout
方法中创建的
- 获取TypedArray
TypedArray a = getWindowStyle();
- 根据TypedArray得到的属性设置是否启用屏幕的一些特性
if (a.getBoolean(R.styleable.Window_windowNoTitle, false)) {
requestFeature(FEATURE_NO_TITLE);
}
...
- 根据第二步设置的Features得到不同的layoutResource,并通过DecorView的onResourcesLoaded方法将layoutResource添加到DecorView中
int features = getLocalFeatures();
if ...
else{
// Embedded, so no decoration is needed.
layoutResource = R.layout.screen_simple;
}
mDecor.startChanging();
mDecor.onResourcesLoaded(mLayoutInflater, layoutResource);
...
- 创建mContentParent
ViewGroup contentParent = (ViewGroup)findViewById(ID_ANDROID_CONTENT);
- 为顶层窗口设置背景和标题
...
final Drawable background;
if (mBackgroundResource != 0) {
background = getContext().getDrawable(mBackgroundResource);
} else {
background = mBackgroundDrawable;
}
mDecor.setWindowBackground(background);
if (mTitle != null) {
setTitle(mTitle);
}
if (mTitleColor == 0) {
mTitleColor = mTextColor;
}
...
并在PhoneWindow
中的installDecor
方法赋值给成员变量mContentParent
if (mContentParent == null) {
mContentParent = generateLayout(mDecor);
...
}
从上可以得知mContentParent是DecorView下的一个id为content的ViewGroup,一般是FrameLayout
PhoneWinow#requestFeature
该方法用来设置主窗口的各种特性,例如是否显示标题栏、是否悬浮等,在Activity中使用requestWindowFeature来设置,内部会自己调用PhoneWinow的requestFeature方法。从mContentParent
的创建过程可知requestFeature方法需要在setContentView之前调用的原因。让我们来看看一些实际的运用:
根据上面的分析可以得到在Activity中View的布局结构图如下:
兼容包AppCompatActivity的setContentView流程
看过了Activity的setContentView之后,我们来看看经常使用的AppCompatActivity的setContentView有什么不同。
getDelegate().setContentView(layoutResID);
这个getDelegate
方法是用来兼容我们各个版本的:
AppCompatActivity#getDelegate
if (mDelegate == null) {
mDelegate = AppCompatDelegate.create(this, this);
}
AppCompatDelegate#create
if (BuildCompat.isAtLeastN()) {
return new AppCompatDelegateImplN(context, window, callback);
} else if (sdk >= 23) {
return new AppCompatDelegateImplV23(context, window, callback);
} else if (sdk >= 14) {
return new AppCompatDelegateImplV14(context, window, callback);
} else if (sdk >= 11) {
return new AppCompatDelegateImplV11(context, window, callback);
} else {
return new AppCompatDelegateImplV9(context, window, callback);
}
以API25为例,这时候会创建一个AppCompatDelegateImplN
代理类,从AppCompatDelegateImplN的父类
AppCompatDelegateImplV9
找到了setContentView
方法的具体实现:
AppCompatDelegateImplV9#setContentView
- 确保subDecor是否创建,如果没有则创建
ensureSubDecor();
- 将AppCompatActivity中setContentView中传入的布局添加到subDecor中id为content的FrameLayout
ViewGroup contentParent = (ViewGroup) mSubDecor.findViewById(android.R.id.content);
contentParent.removeAllViews();
LayoutInflater.from(mContext).inflate(resId, contentParent);
- 回调onContentChanged方法
mOriginalWindowCallback.onContentChanged();
AppCompatDelegateImplV9#ensureSubDecor
- 如果subDecor没有创建过,则创建
mSubDecor = createSubDecor();
- 如果在subDecor创建之前就设置了标题,在这里回调
onTitleChanged
// If a title was set before we installed the decor, propagate it now
CharSequence title = getTitle();
if (!TextUtils.isEmpty(title)) {
onTitleChanged(title);
}
- 将标记设置为true
mSubDecorInstalled = true;
AppCompatActivity中DecorView的创建 —— AppCompatDelegateImplV9#createSubDecor
加载Window的主题
创建subDecor
的时候使用的是AppCompatTheme
,此declare-styleable在AppCompatV7源码的res\values\values.xml
文件中定义的,这就是为什么我们的在style.xml
中定义的主题需要继承AppCompatTheme
的原因
TypedArray a = mContext.obtainStyledAttributes(R.styleable.AppCompatTheme);
创建DecorView
调用PhoneWindow中的getDecorView方法,内部会调用installDecor方法,从这则回到了Activity中调用setContentView的流程
mWindow.getDecorView();
PhoneWindow#getDecorView
if (mDecor == null || mForceDecorInstall) {
installDecor();
}
return mDecor;
创建subDecor
此subDecor并不是DecorView,只是模拟Activity中的mDecor
,类似Activity中DecorView的创建,不过这里subDecor的布局是各种兼容布局
if (!mWindowNoTitle) {
if (mIsFloating) {
// If we're floating, inflate the dialog title decor
subDecor = (ViewGroup) inflater.inflate(
R.layout.abc_dialog_title_material, null);
// Floating windows can never have an action bar, reset the flags
mHasActionBar = mOverlayActionBar = false;
} else if (mHasActionBar) {
// Now inflate the view using the themed context and set it as the content vi
subDecor = (ViewGroup) LayoutInflater.from(themedContext)
.inflate(R.layout.abc_screen_toolbar, null);
mDecorContentParent = (DecorContentParent) subDecor
.findViewById(R.id.decor_content_parent);
mDecorContentParent.setWindowCallback(getWindowCallback());
...
}
} else {
if (mOverlayActionMode) {
subDecor = (ViewGroup) inflater.inflate(
R.layout.abc_screen_simple_overlay_action_mode, null);
} else {
subDecor = (ViewGroup) inflater.inflate(R.layout.abc_screen_simple, null);
}
if (Build.VERSION.SDK_INT >= 21) {
// If we're running on L or above, we can rely on ViewCompat's
// setOnApplyWindowInsetsListener
...
} else {
// Else, we need to use our own FitWindowsViewGroup handling
...
}
}
让系统的mDecor
中加载的是兼容的布局
- 获取subDecor中的存放内容布局的的兼容FrameLayout,和PhoneWindow中的mDecor中存放内容布局的FrameLayout
final ContentFrameLayout contentView = (ContentFrameLayout) subDecor.findViewById(
R.id.action_bar_activity_content);
final ViewGroup windowContentView = (ViewGroup) mWindow.findViewById(android.R.id.content);
- 将PhoneWindow中的mDecor中的内容布局从mDecor中移除,添加到subDecor中,并修改其id
// There might be Views already added to the Window's content view so we need to
// migrate them to our content view
while (windowContentView.getChildCount() > 0) {
final View child = windowContentView.getChildAt(0);
windowContentView.removeViewAt(0);
contentView.addView(child);
}
// Change our content FrameLayout to use the android.R.id.content id.
// Useful for fragments.
windowContentView.setId(View.NO_ID);
contentView.setId(android.R.id.content);
- 将subDecor作为内容布局传给PhoneWindow中
// Now set the Window's content view with the decor
mWindow.setContentView(subDecor);
根据上面的分析可以得到在AppCompatActivity中View的布局结构图如下:
总结
至此我们已经分析完了setContentView的源码,对于之前提的疑问也有了答案:
- 为什么调用setContentView就能将布局显示出来?
调用setContentView方法内部会调用PhoneWindow的setContentView方法,其内部通过mLayoutInflater.inflate(layoutResID, mContentParent);
加载到DecorView的子布局mContentParent中,而DecorView是我们的顶级View,会在Activity启动后加载到当前Activity的应用程序窗口,所以我们调用setContentView就能将我们的布局显示出来。 - 为什么requestFeature需要在setContentView之前调用?
当我们在Activity中调用了setContentView方法,会调用PhoneWindow的generateLayout方法,该方法会根据requestFeature方法设置的属性来选择DecorView中加载的布局,以及根据一些特性,例如是否显示标题,来设置当前窗口的特性。 - PhoneWindow和Window之间有什么关系?
当我们在Activity中调用了setContentView方法,内部会调用Window的setContentView方法,Window是一个抽象类,而PhoneWindow是抽象类Window的唯一子类。Window的实例必须当做顶级View添加到WindowManager中。 - DecorView和我们的布局有什么关系?
DecorView是我们窗口的顶级View,意味着我们使用Hierarchy Viewer查看View的层级关系时,最上层的View都是DecorView。我们的布局是加载在DecorView下的一个id为content的FrameLayout中的。 - 为什么继承AppCompactActivity后,主题需要继承AppCompactTheme?
在AppCompactActivity中调用setContentView,内部会调用AppCompatDelegateImplV9的createSubDecor方法,其中会加载兼容Window的主题AppCompactTheme
。