setContentView的流程主要就是讲在Activity的onCreate方法中调用setContentView方法之后,我们自定义的xml文件加载的过程,学习它可以让我们对整个View树的理解更加透彻,并且通过源码的学习,我们可以从根本上理解一些问题,比如
- 为什么requestWindowFeature要放到setContentView前面
- merge、include、ViewStub是如何工作的,尤其是include如何通过id查找布局的根View
- View view = LayoutInflater.from(context).inflate(R.layout.inflate, root, true)中inflate方法的参数有什么用呢?
我们在平时的开发中可能也经常会遇到,遇到之后百度一下ctrl+c ctr+v就解决了,但是其根本原因可能不是特别清楚,那么今天我们就通过源码来看下setContentView的流程,相信通过本篇的学习你一定会有收获。
随着Android版本的升级我们在声明一个Activity时可以继承自Activity也可以继承自AppCompatActivity,因此对于setContentView的流程也同样分为继承自Activity和继承自AppCompatActivity。这两种方式的大致流程差不多,都可以分为两大步骤
(1) installDecor 生成DecorView并获取到mContentParent对象
(2) 将我们Activity的xml布局添加到mContentParent中
继承自Activity和AppCompatActivity的区别主要在第一步,因此本篇的思路就是对第(1)步分别讲解,对于第(2)步两者相同就共同来讲,另外说明一下我这里采用的是API 30的源码。
一、继承自Activity DecorView生成过程
1 setContentView整体运行时序图
我们先来看下继承自Activity的setContentView的调用流程,我画了个时序图方便大家从整体上掌握各个模块的交互流程,学习一个知识一定要先从整体上了解,然后再去了解细节
可以看到整个流程其实就是刚提到的两步:
- 初始化DecorView——获取到mContentParent,并将系统的xml布局文件添加到DecorView
- 将开发者自定义的Acitivity的xml布局添加到mContentParent中
2 源码执行分析
接下来就根据上述流程来看下源码,我们看源码不应该扣细节,只关注主流程就行,因此对于源码我会进行精简,只把主流程中用到的留下。首先从Activity#setContentView的源码开始
public void setContentView(View view) {getWindow().setContentView(view);initWindowDecorActionBar();}public Window getWindow() {return mWindow;}
可以看到在第二行调用了getWindow方法,而getWindow返回一个mWindow,那么这个mWindow是怎么来的呢?我们知道Activity启动(后续会写一篇Activity启动流程的文章,这里大家先了解下)的时候有个非常重要的类ActivityThread,当我们点击应用图标启动应用时Android系统会创建一个新的进程,并且在启动Activity的时候会调用ActivityThread的performLaunchActivity方法,来看下它的源码ActivityThread#performLaunchActivity
private Activity performLaunchActivity(ActivityClientRecord r, Intent customIntent) {。。。Activity activity = null;// 初始化activityactivity = mInstrumentation.newActivity(cl, component.getClassName(), r.intent);。。。// 调用activity的attach方法activity.attach(appContext, this, getInstrumentation(), r.token,r.ident, app, r.intent, r.activityInfo, title, r.parent,r.embeddedID, r.lastNonConfigurationInstances, config,r.referrer, r.voiceInteractor, window, r.configCallback,r.assistToken, r.shareableActivityToken);。。。// 调用activity的onCreate方法 if (r.isPersistable()) {mInstrumentation.callActivityOnCreate(activity, r.state, r.persistentState);} else {mInstrumentation.callActivityOnCreate(activity, r.state);}return activity;}
可以看到在第5行初始化了一个activity,然后调用了activity的attach方法,并且从调用顺序上也能看到attach方法要早于onCreate方法执行,来看下Activity的attach方法的源码Activity#attach
final void attach(Context context, ActivityThread aThread,Instrumentation instr, IBinder token, int ident,Application application, Intent intent, ActivityInfo info,CharSequence title, Activity parent, String id,NonConfigurationInstances lastNonConfigurationInstances,Configuration config, String referrer, IVoiceInteractor voiceInteractor,Window window, ActivityConfigCallback activityConfigCallback, IBinder assistToken,IBinder shareableActivityToken) {。。。mWindow = new PhoneWindow(this, window, activityConfigCallback);。。。}
Activity中的attach方法参数很多,我们不需要关注,可以看到第10行它初始化了mWindow,这个mWindow其实就是PhoneWindow,因为它是在activity的attach方法中初始化的,并且attach方法早于onCreate方法执行,所以在onCreate->setContentView->getWindow获取window的时候,这个window已经初始化好了,可以直接拿到这个PhoneWindow对象。
因此在Activity中的onCreate方法中调用setContentView的时候,首先会调用getWindow().setContentView而这个getWindow就是在Activity中的attach方法中初始化的PhoneWindow对象。
所以接下来我们就要分析PhoneWindow的setContentView方法,它的源码如下PhoneWindow#setContentView
@Overridepublic void setContentView(int layoutResID) {if (mContentParent == null) {// 初始化DecorView 并获取到mContentParentinstallDecor();} else if (!hasFeature(FEATURE_CONTENT_TRANSITIONS)) {mContentParent.removeAllViews();}if (hasFeature(FEATURE_CONTENT_TRANSITIONS)) {。。。} else {// 将我们自定义Activity的布局添加到mContentParentmLayoutInflater.inflate(layoutResID, mContentParent);}。。。// setContentView执行完的标识mContentParentExplicitlySet = true;}
这里就比较重要了,它主要进行了两个操作,分别是第5行和第14行
- installDecor:初始化DecorView 并获取到mContentParent
- mLayoutInflater.inflate(layoutResID, mContentParent):将我们自定义Activity的布局添加到mContentParent
在最后它还会将mContentParentExplicitlySet置为true,随后我们也会讲到它的用处
我们一个一个来看,首先来看PhoneWindow#installDecor
public class PhoneWindow extends Window implements MenuBuilder.Callback {private void installDecor() {。。。if (mDecor == null) {// 初始化DecorView对象mDecor = generateDecor(-1);。。。} else {mDecor.setWindow(this);}if (mContentParent == null) {// 将id为ID_ANDROID_CONTENT的布局赋值给mContentParentmContentParent = generateLayout(mDecor);。。。}}}protected DecorView generateDecor(int featureId) {。。。// 将new的DecorView返回return new DecorView(context, featureId, this, getAttributes());}
}
可以看到第6行调用了generateDecor方法,而generateDecor方法其实就是根据各种特性new一个DecorView并返回。第13行调用了generateLayout,我们来看下它的源码PhoneWindow#generateLayout
protected ViewGroup generateLayout(DecorView decor) {TypedArray a = getWindowStyle();。。。// Inflate the window decor.int layoutResource;int features = getLocalFeatures();。。。省略多个if分支判断if ((features & (1 << FEATURE_NO_TITLE)) == 0) {。。。if ((features & (1 << FEATURE_ACTION_BAR)) != 0) {layoutResource = a.getResourceId(R.styleable.Window_windowActionBarFullscreenDecorLayout,R.layout.screen_action_bar);} else {layoutResource = R.layout.screen_title;}。。。} else {// Embedded, so no decoration is needed.layoutResource = R.layout.screen_simple;}mDecor.startChanging();// 将layoutResource布局添加到DecorViewmDecor.onResourcesLoaded(mLayoutInflater, layoutResource);// 调用findViewById将layoutResource布局中id为ID_ANDROID_CONTENT的ViewGroup赋值给contentParentViewGroup contentParent = (ViewGroup)findViewById(ID_ANDROID_CONTENT);。。。return contentParent;}
generateLayout它其实就是根据我们设置的特性去加载不同的布局,比如FEATURE_NO_TITLE、FEATURE_ACTION_BAR等,下面我们就假设进入了最后一个分支即第19行layoutResource = R.layout.screen_simple,然后到第23行调用DecorView的onResourcesLoaded方法将R.layout.screen_simple布局添加到DecorView,然后第25行获取R.layout.screen_simple这个布局的ID_ANDROID_CONTENT,并将其返回。接着来看下DecorView中onResourcesLoaded的源码,DecorView#onResourcesLoaded
public class DecorView extends FrameLayout implements RootViewSurfaceTaker, WindowCallbacks {void onResourcesLoaded(LayoutInflater inflater, int layoutResource) {。。。final View root = inflater.inflate(layoutResource, null);。。。// Put it below the color views.addView(root, 0, new ViewGroup.LayoutParams(MATCH_PARENT, MATCH_PARENT));
}
可以看到DecorView是继承自FrameLayout的,第4行调用LayoutInflater方法将R.layout.screen_simple转化成View,然后第7行调用自身的addView方法将刚生成的View添加到DecorView。到这里大家可能有个疑惑就是R.layout.screen_simple,这里面的内容是啥呢?R.layout.screen_simple.xml内容如下
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"android:layout_width="match_parent"android:layout_height="match_parent"android:fitsSystemWindows="true"android:orientation="vertical"><ViewStub android:id="@+id/action_mode_bar_stub"android:inflatedId="@+id/action_mode_bar"android:layout="@layout/action_mode_bar"android:layout_width="match_parent"android:layout_height="wrap_content"android:theme="?attr/actionBarTheme" /><FrameLayoutandroid:id="@android:id/content"android:layout_width="match_parent"android:layout_height="match_parent"android:foregroundInsidePadding="false"android:foregroundGravity="fill_horizontal|top"android:foreground="?android:attr/windowContentOverlay" />
</LinearLayout>
它是android系统里的布局,根据不同的特性会有不同的布局,R.layout.screen_simple算是比较简单的,讲解原理的话用这种简单的布局更好理解。到这里installDecor的流程就走完了,也就是setContentView的第一步:installDecor 生成DecorView并获取到mContentParent对象,我们来看下此时的布局是怎样的,如下图所示
3 requestWindowFeature使用
再回头看下PhoneWindow的setContentView的源码最后一个行mContentParentExplicitlySet = true这个作用是什么呢?还记得在刚开始做Android的时候想去掉顶部的Title需要做如下处理
@Overrideprotected void onCreate(Bundle savedInstanceState) {super.onCreate(savedInstanceState);requestWindowFeature(Window.FEATURE_NO_TITLE);setContentView(R.layout.activity_main);}
第4行调用requestWindowFeature(Window.FEATURE_NO_TITLE)并且这个方法只能放到setContentView之前,如果放到setContentView之后的话会报错
为什么会报错呢?这需要看下requestWindowFeature的源码,Activity#requestWindowFeature
public final boolean requestWindowFeature(int featureId) {return getWindow().requestFeature(featureId);}
这里的getWindow同样是PhoneWindow,因此最终调用的是PhoneWindow#requestFeature
@Overridepublic boolean requestFeature(int featureId) {if (mContentParentExplicitlySet) {throw new AndroidRuntimeException("requestFeature() must be called before adding content");}。。。return super.requestFeature(featureId);}
可以看到第3行if语句的条件就是如果mContentParentExplicitlySet为true就会抛异常,而刚才提到setContentView的最后一句将mContentParentExplicitlySet置为了true,所以如果requestWindowFeature方法放到setContentView之后就会抛异常,为什么这么设计呢?回到上面的PhoneWindow#generateLayout的源码可以看到DecorView的布局会用到requestWindowFeature设置的特性,根据不同的特性来加载不同的布局。好了到这里继承自Activity生成DecorView的过程就介绍完了。
二、继承自AppCompatActivity DecorView生成过程
1 源码执行流程分析
接下来看下继承自AppCompatActivity的相关内容,同样我们先看它的调用流程,AppCompatActivity#setContentView
public class AppCompatActivity extends FragmentActivity implements ...{@Overridepublic void setContentView(@LayoutRes int layoutResID) {initViewTreeOwners();getDelegate().setContentView(layoutResID);}public AppCompatDelegate getDelegate() {if (mDelegate == null) {mDelegate = AppCompatDelegate.create(this, this);}return mDelegate;}
}
可以看到第6行调用了getDelegate,在getDelegate方法中第11行调用了AppCompatDelegate.create方法
public static AppCompatDelegate create(@NonNull Activity activity,@Nullable AppCompatCallback callback) {return new AppCompatDelegateImpl(activity, callback);}
这个方法会返回一个AppCompatDelegateImpl对象,因此在AppCompatActivity的setContentView方法中调用getDelegate().setContentView(layoutResID)其实就是调用AppCompatDelegateImpl中的setContentView,我们来看下它的源码AppCompatDelegateImpl#setContentView
class AppCompatDelegateImpl extends AppCompatDelegate implements ... {@Overridepublic void setContentView(View v) {ensureSubDecor();ViewGroup contentParent = mSubDecor.findViewById(android.R.id.content);contentParent.removeAllViews();contentParent.addView(v);。。。}private void ensureSubDecor() {if (!mSubDecorInstalled) {mSubDecor = createSubDecor();。。。}}
}
在第4行调用了ensureSubDecor方法,在ensureSubDecor中调用createSubDecor来创建subDecor对象,重点来看下AppCompatDelegateImpl#createSubDecor
class AppCompatDelegateImpl extends AppCompatDelegate implements ... {private ViewGroup createSubDecor() {。。。ensureWindow();mWindow.getDecorView();final LayoutInflater inflater = LayoutInflater.from(mContext);ViewGroup subDecor = null;// 。。。此处省略很多if分支判断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);}final ContentFrameLayout contentView = (ContentFrameLayout) subDecor.findViewById(R.id.action_bar_activity_content);final ViewGroup windowContentView = (ViewGroup) mWindow.findViewById(android.R.id.content);if (windowContentView != null) {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);}// Now set the Window's content view with the decormWindow.setContentView(subDecor);return subDecor;}private void ensureWindow() {// We lazily fetch the Window for Activities, to allow DayNight to apply in// attachBaseContextif (mWindow == null && mHost instanceof Activity) {attachToWindow(((Activity) mHost).getWindow());}if (mWindow == null) {throw new IllegalStateException("We have not been given a Window");}}
}
在createSubDecor中首先调用了ensureWindow()这个方法的作用在第43行,通过Activity获取PhoneWindow对象,拿到PhoneWindow对象之后在第5行会调用mWindow.getDecorView(),mWindow其实就是PhoneWidow,我们就来看下它的代码
public class PhoneWindow extends Window implements MenuBuilder.Callback {@Overridepublic final @NonNull View getDecorView() {if (mDecor == null || mForceDecorInstall) {installDecor();}return mDecor;}
}
看到没,它调用了installDecor()方法,这个installDecor的流程跟继承自Activity的流程是一样的。再回到createSubDecor方法的源码,刚才我们讲了第4行和第5行的代码,接着往下看它会根据不同的特性加载不同的布局,这里为了让主流程更加清晰我省略了很多if分支判断,只留了第10~15行代码。可以看到在第15行会加载abc_screen_simple.xml布局(假设if分支走到这里),abc_screen_simple.xml如下
<?xml version="1.0" encoding="utf-8"?><androidx.appcompat.widget.FitWindowsLinearLayoutxmlns:android="http://schemas.android.com/apk/res/android"android:id="@+id/action_bar_root"android:layout_width="match_parent"android:layout_height="match_parent"android:orientation="vertical"android:fitsSystemWindows="true"><androidx.appcompat.widget.ViewStubCompatandroid:id="@+id/action_mode_bar_stub"android:inflatedId="@+id/action_mode_bar"android:layout="@layout/abc_action_mode_bar"android:layout_width="match_parent"android:layout_height="wrap_content" /><include layout="@layout/abc_screen_content_include" /></androidx.appcompat.widget.FitWindowsLinearLayout>
abc_screen_content_include.xml
<?xml version="1.0" encoding="utf-8"?><merge xmlns:android="http://schemas.android.com/apk/res/android"><androidx.appcompat.widget.ContentFrameLayoutandroid:id="@id/action_bar_activity_content"android:layout_width="match_parent"android:layout_height="match_parent"android:foregroundGravity="fill_horizontal|top"android:foreground="?android:attr/windowContentOverlay" />
</merge>
再回到createSubDecor方法的源码,加载好abc_screen_simple.xml之后会执行如下代码
final ContentFrameLayout contentView = (ContentFrameLayout) subDecor.findViewById(R.id.action_bar_activity_content);final ViewGroup windowContentView = (ViewGroup) mWindow.findViewById(android.R.id.content);if (windowContentView != null) {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);}// Now set the Window's content view with the decormWindow.setContentView(subDecor);return subDecor;
这个方法很重要,需要重点理解下,可以看到第一行根据id找到contentView,这个id:action_bar_activity_content就是上面abc_screen_content_include.xml中的id,接下来第4行中的android.R.id.content其实是继承自Acitivity的时候xml中的布局,将此id赋值给windowContentView,然后第6行的while循环是判断如果windowContentView有子View会把它们移除并添加到contentView中,最后第13行将windowContentView的id设置为NO_ID,然后将contentView的id设置为android.R.id.content,这么做的原因是为了兼容之前的版本。然后会执行第17行代码将subDecor添加到PhoneWindow。同样我们也来画一下继承自AppComaptActivity的层级图
跟继承自Activity的层级图差不太多,只是这里为了兼容老版本底层做了一些处理,确保我们应用层不受影响。
2 supportRequestWindowFeature使用注意事项
同样的继承自AppCompatActivity进行 Window特性设置时不再调用requestWindowFeature而是调用supportRequestWindowFeature并且也是要放到setContentView前面。
@Overrideprotected void onCreate(Bundle savedInstanceState) {super.onCreate(savedInstanceState);supportRequestWindowFeature(Window.FEATURE_NO_TITLE);setContentView(R.layout.activity_main);}
到这里DecorView的生成过程就讲完了
三、总结
(1) 我们activity中自定义的xml文件的根布局是DecorView,DecorView是继承自FrameLayout的
(2) 在DecorView的子View中有个id为R.id.content的布局,将其转化为mContentParent
(3) 最后使用LayoutInflater.from(mContext).inflate(resId, contentParent)将我们定义的xml文件resId添加到mContentParent中
篇幅原因第(3)点我们下篇博客讲解。
如果大家有疑问或者发现错误,欢迎在下方留言,我会在第一时间回答!!
如果觉得文章对你有帮助就帮忙点个赞吧。