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 的结构;

<android.support.v7.widget.FitWindowsLinearLayout

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">

<android.support.v7.widget.ViewStubCompat
    android: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" />

</android.support.v7.widget.FitWindowsLinearLayout>

abc_screen_content_include.xml 布局如下所示:

<?xml version="1.0" encoding="utf-8"?>
<!-- Copyright (C) 2014 The Android Open Source Project

 Licensed under the Apache License, Version 2.0 (the "License");
 you may not use this file except in compliance with the License.
 You may obtain a copy of the License at

      http://www.apache.org/licenses/LICENSE-2.0

 Unless required by applicable law or agreed to in writing, software
 distributed under the License is distributed on an "AS IS" BASIS,
 WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 See the License for the specific language governing permissions and
 limitations under the License.

-->

<merge xmlns:android="http://schemas.android.com/apk/res/android">

<android.support.v7.widget.ContentFrameLayout
        android: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>

所以注释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 的结构如下所示:

<?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" />
<FrameLayout
     android: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>

这时候注释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 容器中,这样完成布局兼容操作。

本篇文章写到这里就结束了,由于技术水平有限,文章中难免会出错,欢迎大家批评指正。


公众号小二玩编程
4 声望4 粉丝

活到老,学到老。