Android中AppCompatActivity的setContentView方法分析

PS:本文系转载文章,阅读原文可读性会更好,文章末尾有原文链接
ps:源码是基于 android api 27 来分析的
前面写了一篇Android中Activity的setContentView方法分析,这一篇打算写对 AppCompatActivity 的setContentView 方法进行分析,AppCompatActivity 的 setContentView 方法和 Activity的 setContentView 方法是有区别的,区别在哪呢?我们就从AppCompatActivity 的 setContentView 方法进行分析,从而去寻找答案;

@Override public void setContentView(@LayoutRes int layoutResID) { getDelegate().setContentView(layoutResID); }

这里用 getDelegate 方法来调用 setContentView 方法的,那么 getDelegate 方法肯定是返回一个对象,我们来看 AppCompatActivity 的 getDelegate 方法;
@NonNull public AppCompatDelegate getDelegate() { if (mDelegate == null) { mDelegate = AppCompatDelegate.create(this, this); } return mDelegate; }

getDelegate 方法返回的是一个 AppCompatDelegate 类型的对象,从字面意思可以看出,它是一个做兼容的代理并且位于 v7 包里,v7 是为了做兼容而存在的;AppCompatDelegate 是一个抽象类,我们来看 AppCompatDelegate 的 create(Activity activity, AppCompatCallback callback) 方法,看看 AppCompatDelegate 具体的实现类是什么;
public static AppCompatDelegate create(Activity activity, AppCompatCallback callback) {
return create(activity, activity.getWindow(), callback);

}
private static AppCompatDelegate create(Context context, Window window,
AppCompatCallback callback) { if (BuildCompat.isAtLeastO()) { return new AppCompatDelegateImplO(context, window, callback); } else if (Build.VERSION.SDK_INT >= 24) { return new AppCompatDelegateImplN(context, window, callback); } else if (Build.VERSION.SDK_INT >= 23) { return new AppCompatDelegateImplV23(context, window, callback); } else if (Build.VERSION.SDK_INT >= 14) { return new AppCompatDelegateImplV14(context, window, callback); } else if (Build.VERSION.SDK_INT >= 11) { return new AppCompatDelegateImplV11(context, window, callback); } else { return new AppCompatDelegateImplV9(context, window, callback); }

}
从 create(Activity activity, AppCompatCallback callback) 方法可以看出,它又调用了 create(Context context, Window window,AppCompatCallback callback) 方法,create(Context context, Window window,AppCompatCallback callback) 方法根据 SDK 的版本实例化不同的 AppCompatDelegate 的子类,我们这次就拿 AppCompatDelegateImplV9 类进行分析,其他的子类就读者自行去看了,所以我们来看 AppCompatDelegateImplV9 类的 setContentView 方法;
@Override public void setContentView(int resId) { //1、 ensureSubDecor(); //2、 ViewGroup contentParent = (ViewGroup) mSubDecor.findViewById(android.R.id.content); //3、 contentParent.removeAllViews(); //4、 LayoutInflater.from(mContext).inflate(resId, contentParent); //5、 mOriginalWindowCallback.onContentChanged(); }

注释2 中的 contentParent 还是 DecorView 布局中 id 为 content 的 FrameLayout 吗?答案却不是,mSubDecor 是什么呢?从字面来看可以理解为 代替的 DecorView 或者兼容的 DecorView,又或者第二个 DecorView;注释3 表示将 contentParent 所有的子视图都删除掉;注释4 表示将 id 为 resId 的布局文件填充到 contentParent,也就是将我们 AppCompatActivity 中 setContentView 设置的内容布局文件填充到 contentParent;注释5 表示回调 AppCompatActivity 的 onContentChanged 方法;好,我们往下看 注释1 中 AppCompatDelegateImplV9 的 ensureSubDecor 方法;
private void ensureSubDecor() {
if (!mSubDecorInstalled) {//6、 mSubDecor = createSubDecor(); ...... //7、 mSubDecorInstalled = true; ...... }

}
注释7 表示视图是否已经创建,如果创建过了就说明已经调用过 AppCompatDelegateImplV9 的 ensureSubDecor 方法了;注释6的代码包含创建 mSubDecor 的方法,我们看一下 AppCompatDelegateImplV9 的 createSubDecor 方法;
private ViewGroup createSubDecor() {
TypedArray a = mContext.obtainStyledAttributes(R.styleable.AppCompatTheme); ...... //8、 if (a.getBoolean(R.styleable.AppCompatTheme_windowNoTitle, false)) { requestWindowFeature(Window.FEATURE_NO_TITLE); } else if (a.getBoolean(R.styleable.AppCompatTheme_windowActionBar, false)) { // Don't allow an action bar if there is no title. //9、 requestWindowFeature(FEATURE_SUPPORT_ACTION_BAR); } if (a.getBoolean(R.styleable.AppCompatTheme_windowActionBarOverlay, false)) { requestWindowFeature(FEATURE_SUPPORT_ACTION_BAR_OVERLAY); } if (a.getBoolean(R.styleable.AppCompatTheme_windowActionModeOverlay, false)) { requestWindowFeature(FEATURE_ACTION_MODE_OVERLAY); } ...... ViewGroup subDecor = null; //10、 if (!mWindowNoTitle) { if (mIsFloating) { // If we're floating, inflate the dialog title decor //11、 subDecor = (ViewGroup) inflater.inflate( R.layout.abc_dialog_title_material, null); ...... } else if (mHasActionBar) { ...... // Now inflate the view using the themed context and set it as the content view //12、 subDecor = (ViewGroup) LayoutInflater.from(themedContext) .inflate(R.layout.abc_screen_toolbar, null); ...... } } else {//13、 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); } //14、 final ContentFrameLayout contentView = (ContentFrameLayout) subDecor.findViewById( R.id.action_bar_activity_content); //15、 final ViewGroup windowContentView = (ViewGroup) mWindow.findViewById(android.R.id.content); if (windowContentView != null) { // There might be Views already added to the Window's content view so we need to // migrate them to our content view //16、 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. //17、 windowContentView.setId(View.NO_ID); contentView.setId(android.R.id.content); ...... }// Now set the Window's content view with the decor //18、 mWindow.setContentView(subDecor); ...... return subDecor;

}
类似与注释8 相似的 a.getBoolean(R.styleable.AppCompatTheme_windowXXX, false)) 语句其实是获取 Window 的 style 属性;类似注释9 的 requestWindowFeature(XXX) 其实是调用 Window 中的requestFeature 方法;注释10 表示如果没有标题;注释11 表示 Floating 类型的窗口;注释12 表示通过 themedContext 加载视图;注释13 表示 Overlay 模式下加载 Overlay 模式的视图;我们假设 subDecor 要加载的布局文件是 abc_screen_simple.xml,我们且看 abc_screen_simple.xml 的结构;
xmlns: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">


abc_screen_content_include.xml 布局如下所示:





所以注释14 中的 contentView 其实是 abc_screen_content_include.xml 文件中 id 为 action_bar_activity_content 的 ContentFrameLayout;注释15 的 mWindow 其实是 PhoneWindow,所以本质是调用 PhoneWindow 的 findViewById 方法,PhoneWindow 的 findViewById 方法又调用 DecorView 的 findViewById 方法,DecorView 的 findViewById 方法调用之前先判断 DecorView 是否为空,如果为空就调用 PhoneWindow 的 installDecor 方法,installDecor 方法的相关解析可以看Android中Activity的setContentView方法分析这篇文章,我们假设 DecorView 要加载的布局文件是 screen_simple.xml,screen_simple.xml 的结构如下所示:

android:layout_width="match_parent" android:layout_height="match_parent" android:fitsSystemWindows="true" android:orientation="vertical">


这时候注释15 的 windowContentView 本质上就是 screen_simple.xml 文件中 id 为 content 的 FrameLayout;注释16 的 while 整体代码是这样的,先判断 DecorView 中的 FrameLayout 中子元素个数是否大于0,如果大于0,就取出第一个子元素并删除,删除后的子元素就添加到 subDecor 的子元素 ContentFrameLayout 中,也就是说 把DecorView 中的 FrameLayout 中第一个子元素移动到 subDecor 的子元素 ContentFrameLayout 中;注释17 表示将 DecorView 中 FrameLayout 的 id 设置成-1,可以理解成没有 id,然后再将 subDecor 中 ContentFrameLayout 的 id 设置为 content;注释18 表示将我们的 subDecor 添加到 PhoneWindow 中,我们且看 PhoneWindow 的 setContentView(View view) 方法;
@Override public void setContentView(View view) { setContentView(view, new ViewGroup.LayoutParams(MATCH_PARENT, MATCH_PARENT)); }@Override public void setContentView(View view, ViewGroup.LayoutParams params) { // Note: FEATURE_CONTENT_TRANSITIONS may be set in the process of installing the window // decor, when theme attributes and the like are crystalized. Do not check the feature // before this happens. if (mContentParent == null) { installDecor(); } else if (!hasFeature(FEATURE_CONTENT_TRANSITIONS)) { mContentParent.removeAllViews(); }if (hasFeature(FEATURE_CONTENT_TRANSITIONS)) { ...... } else {//19、 mContentParent.addView(view, params); } ...... }

PhoneWindow 的 setContentView(View view) 方法又调用了 PhoneWindow 的 setContentView(View view, ViewGroup.LayoutParams params) 方法,注释19 中的 mContentParent 是什么?它是 DecorView 中的 FrameLayout,之前它的 id 是 content,现在它的 id 是-1,因为它的 id 在上面分析的代码中执行替换了;因为在第一次执行 PhoneWindow 的 installDecor 方法的时候就已经实例化 mContentParent 并且已经知道 mContentParent 对象具体的实现类是 FrameLayout,不明白的读者可以看看Android中Activity的setContentView方法分析这篇文章,mContentParent.addView(view, params) 其实就是将 subDecor 添加到 DecorView 的子元素 FrameLayout 里面;我们回到 AppCompatDelegateImplV9 类的 setContentView 方法中,注释2 的代码,注释2 中的 contentParent 其实是 subDecor 中的 ContentFrameLayout;假设我的 AppCompatActivity 中要设置的内容布局文件为 activity_main.xml,那么注释4 的 resId 就等于 R.layout.activity_main,那么 activity_main.xml 就会填充到 subDecor 中的 ContentFrameLayout。
下面我们来画一下 AppCompatActivity 中的 DecorView 的布局结构图:
图片
Activity 的 DecorView 布局结构图,我就不在这里画出来了,可以看一下认识Android中的ViewRootImpl和DecorView这篇文章的末尾,会有 Activity 的 DecorView 布局结构图,读者可以拿 AppCompatActivity 中的 DecorView 的布局结构图和 Activity 的 DecorView 布局结构图对比一下有什么不同。
在 Android Level 21之后,Android 引入了 Material Design 的设计,为了支持 Material,Color、调色版、Toolbar等各种新特性,AppCompatActivity 就应用而生;AppCompatActivity 为了兼容以前的东西,就做了偷梁换柱,其中一个就是替换了以前 Activity 加载内容布局的父容器,即在原 DecorView 的 content 区域添加一个 SubDecor,我们通过 setContentView 设置的布局最终被添加到该 SubDecor 的 content 容器中,这样完成布局兼容操作。
【Android中AppCompatActivity的setContentView方法分析】本篇文章写到这里就结束了,由于技术水平有限,文章中难免会出错,欢迎大家批评指正。

    推荐阅读