AppCompatActivity.setContentView流程分析

513 阅读8分钟

前言

目前我们开发Activity时都会继承AppCompatActivity,本文会从该类为入口,分析我们onCreate()方法中调用的setContentView(View)做了些什么,到底将我们的View设置到了哪里。

Activity的层级结构

Activity:负责生命周期的管理,Window的创建

Window:代表一个窗口,包括Background、Titile、事件等

View:安卓中所有可显示的组件都继承自View

由于Window本身不是View,只负责管理、处理事件,所以内部包含一个DecorView用来显示真正的UI,根据用户不同的配置,如是否有ActionBar等,DecorView内部的组织结构也不一样,用户setContentView设置的content位置也不同。

AppCompatActivity

该类是一个代理类,当我们调用setContentView时会创建一个AppCompatActivityDelateImpl对象,然后调用对应的setContentView();

public void setContentView(View view) {

    initViewTreeOwners();

    getDelegate().setContentView(view);

}

AppCompatActivityDelateImpl

下面是具体setContentView中的代码,很直观,先确保subDecor已经创建过,然后通过findViewById根据contentParent,清楚其中的view,再我们指定的View设置进去即可。

public void setContentView(View v) {

    ensureSubDecor();

    ViewGroup contentParent = mSubDecor.findViewById(android.R.id.content);

    contentParent.removeAllViews();

    contentParent.addView(v);

    mAppCompatWindowCallback.getWrapped().onContentChanged();

}

顺着这个逻辑,现在我们有两个个问题:

  • 什么是subDecor?
  • contentParent是什么?

什么是SubDecor?

首先mSubDecor是一个位于DecorView中的ViewGroup,专门用来存放用户自己创建的content。

AppCompatActivityDelateImpl.ensureSubDecor会先检测subdecor是否被创建过,如果没有就先创建。

   // true if we have installed a window sub-decor layout.

   private boolean mSubDecorInstalled;

   private ViewGroup mSubDecor;

   

   private void ensureSubDecor() {

        if (!mSubDecorInstalled) {

            mSubDecor = createSubDecor();



            // If a title was set before we installed the decor, propagate it now

            CharSequence title = getTitle();

            if (!TextUtils.isEmpty(title)) {

                if (mDecorContentParent != null) {

                    mDecorContentParent.setWindowTitle(title);

                } else if (peekSupportActionBar() != null) {

                    peekSupportActionBar().setWindowTitle(title);

                } else if (mTitleView != null) {

                    mTitleView.setText(title);

                }

            }



            applyFixedSizeWindow();



            onSubDecorInstalled(mSubDecor);



            mSubDecorInstalled = true;



            // Invalidate if the panel menu hasn't been created before this.

            // Panel menu invalidation is deferred avoiding application onCreateOptionsMenu

            // being called in the middle of onCreate or similar.

            // A pending invalidation will typically be resolved before the posted message

            // would run normally in order to satisfy instance state restoration.

            PanelFeatureState st = getPanelState(FEATURE_OPTIONS_PANEL, false);

            if (!mDestroyed && (st == null || st.menu == null)) {

                invalidatePanelMenu(FEATURE_SUPPORT_ACTION_BAR);

            }

        }

    }

在createSubDector的过程中会再调用ensureWindow和getDectorView()。这两个步骤主要是为了保证Window和DecorView已经被创建过。里面的代码很长,但主要只干了两件事:

  1. 确保Window和DecorView已经被创建
  2. 根据对应的配置,如是否有Titile,是否悬浮等选择对应的layout文件,inflate后生成subDecor
  3. 调用mWindow.setContentView(View),将对应的subDecor设置进去
    private ViewGroup createSubDecor() {

        TypedArray a = mContext.obtainStyledAttributes(R.styleable.AppCompatTheme);

        /**

         * 经典错误,如果Activity在manifest中没有指定theme就会报错

         */

        if (!a.hasValue(R.styleable.AppCompatTheme_windowActionBar)) {

            a.recycle();

            throw new IllegalStateException(

                    "You need to use a Theme.AppCompat theme (or descendant) with this activity.");

        }

        //。。。



        // Now let's make sure that the Window has installed its decor by retrieving it

        //确保DecorView已经创建过

        mWindow.getDecorView();



        final LayoutInflater inflater = LayoutInflater.from(mContext);

        ViewGroup subDecor = null;



        //根据配置如是否有title,是否悬浮,是否有actionbar等选择layout文件,生成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) { //是否有ActionBar

                /**

                 * This needs some explanation. As we can not use the android:theme attribute

                 * pre-L, we emulate it by manually creating a LayoutInflater using a

                 * ContextThemeWrapper pointing to actionBarTheme.

                 */

                TypedValue outValue = new TypedValue();

                mContext.getTheme().resolveAttribute(R.attr.actionBarTheme, outValue, true);



                Context themedContext;

                if (outValue.resourceId != 0) {

                    themedContext = new ContextThemeWrapper(mContext, outValue.resourceId);

                } else {

                    themedContext = mContext;

                }



                // Now inflate the view using the themed context and set it as the content view

                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 (mDecorContentParent == null) {

            mTitleView = (TextView) subDecor.findViewById(R.id.title);

        }



        // Make the decor optionally fit system windows, like the window's decor

        ViewUtils.makeOptionalFitsSystemWindows(subDecor);



       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) {

            // 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);

            //关键,将其id设置为R.id.content

           contentView.setId(android.R.id.content);



            // The decorContent may have a foreground drawable set (windowContentOverlay).

            // Remove this as we handle it ourselves

            if (windowContentView instanceof FrameLayout) {

                ((FrameLayout) windowContentView).setForeground(null);

            }()

        }



        // Now set the Window's content view with the decor

       mWindow.setContentView(subDecor);

        

        return subDecor;

    }

什么是contentParent

contentParent是通过subDecor.findViewById(android.R.id.content) 获取到的,所以我们创建subDecor的布局中应该有一个id为android.R.id.content的布局控件才对。

以有ActionBar时的为例,查看R.layout.abc_screen_toolbar的结构



<androidx.appcompat.widget.ActionBarOverlayLayout

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

        xmlns:app="http://schemas.android.com/apk/res-auto"

        android:id="@+id/decor_content_parent"

        android:layout_width="match_parent"

        android:layout_height="match_parent"

        android:fitsSystemWindows="true">

    <!--存放content的地方-->

 <include layout="@layout/abc_screen_content_include"/>



    <androidx.appcompat.widget.ActionBarContainer

            android:id="@+id/action_bar_container"

            android:layout_width="match_parent"

            android:layout_height="wrap_content"

            android:layout_alignParentTop="true"

            style="?attr/actionBarStyle"

            android:touchscreenBlocksFocus="true"

            android:gravity="top">



        <androidx.appcompat.widget.Toolbar

                android:id="@+id/action_bar"

                android:layout_width="match_parent"

                android:layout_height="wrap_content"

                app:navigationContentDescription="@string/abc_action_bar_up_description"

                style="?attr/toolbarStyle"/>



        <androidx.appcompat.widget.ActionBarContextView

                android:id="@+id/action_context_bar"

                android:layout_width="match_parent"

                android:layout_height="wrap_content"

                android:visibility="gone"

                android:theme="?attr/actionModeTheme"

                style="?attr/actionModeStyle"/>



    </androidx.appcompat.widget.ActionBarContainer>



</androidx.appcompat.widget.ActionBarOverlayLayout>

layout/abc_screen_content_include代码结构:



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



    <androidx.appcompat.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>

很明显这里并没有id为android.R.id.content ****的控件,只有一个action_bar_activity_content

我们可以回头查看一下createSubDecor中最后一部分加黑的代码:

final ContentFrameLayout contentView = (ContentFrameLayout) subDecor.findViewById(

 R.id.action_bar_activity_content);

final ViewGroup windowContentView = (ViewGroup) mWindow.findViewById(android.R.id.content);

先分别获取subDecor中R.id.action_bar_activity_content的布局和mWindow中android.R.id.content的布局。注意!Window并不继承于view,其findViewById的方法是委托为DecorView的。

// Change our content FrameLayout to use the android.R.id.content id.

// Useful for fragments.

windowContentView.setId(View.NO_ID);

//关键,将其id设置为R.id.content

contentView.setId(android.R.id.content);

然后将DecorView中原有的android.R.id.content设置为NO_ID,然后再将R.id.action_bar_activity_content设置为android.R.id.content。这样subDecor中就有了android.R.id.content的控件,也就是我们后面setContentView中的contentParent,其本身是一个ContentFrameLayout类型的对象。

到这里逻辑就很清晰了,setContentView会根据我们的主题配置选择对应的layout,生成subDecor,然后将我们的View添加到对应layout的content布局处。不同的layout其内容放置的位置也不同。这种机制就可以让安卓方便的给我们提供多样的父布局。

但以上还有两个问题:

  1. mWindow.setContentView()的流程,即设置subDecor的流程。
  2. DecorView是怎么建立的?
  3. DecorView中的布局是怎样的,其中是否有android.R.id.content。毕竟是要先检测到DecorView中有android.R.id.content时才进行替换。

PhoneWindow.setContentView的流程

这块代码比较简单,分为两部分:

  1. 先判断DecorView是否初始化过,如果没有先初始化DecorView
  2. 原view的清除和新view的添加。先判断是否设置了动画,如果没有动画就直接removeAllViews()和addView(),如果有就使用Scene对象来运行动画。
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)) {

        view.setLayoutParams(params);

        final Scene newScene = new Scene(mContentParent, view);

        transitionTo(newScene);

    } else {

        mContentParent.addView(view, params);

    }

    mContentParent.requestApplyInsets();

    final Callback cb = getCallback();

    if (cb != null && !isDestroyed()) {

        cb.onContentChanged();

    }

    mContentParentExplicitlySet = true;

}

Window并不是View,而安卓中所有的显示组件都是View类型,所以Window中存在一个DecorView类型的实例。而DecorView的根布局也存在不同类型,显示content的位置也不一样,而mContentParent引用的便是DecorView中添加contentView的控件。

所以在setContentView时添加和删除都在mContentParent上进行操作。

    // This is the top-level view of the window, containing the window decor.

    private DecorView mDecor;



    // This is the view in which the window contents are placed. It is either

    // mDecor itself, or a child of mDecor where the contents go.

    ViewGroup mContentParent;

mContentParent的创建我们会在DecorView的创建过程中说明。

DecorView的创建过程

DecorView的创建在PhoneWindow.installDecor()中,当我们调用PhoneWindow.getDecorView或者直接调用installDecor()时会创建DecorView。

//PhoneWindow.getDecorView();

public final @NonNull View getDecorView() {

    if (mDecor == null || mForceDecorInstall) {

        installDecor();

    }

    return mDecor;

}

installDecor()中使用generateDecor(-1)来创建DecorView,然后使用generateLayout(mDecor)来创建mContentParent;

    

private void installDecor() {

    mForceDecorInstall = false;

    if (mDecor == null) {

        //创建对应的DecorView

        mDecor = generateDecor(-1);

        mDecor.setDescendantFocusability(ViewGroup.FOCUS_AFTER_DESCENDANTS);

        mDecor.setIsRootNamespace(true);

        if (!mInvalidatePanelMenuPosted && mInvalidatePanelMenuFeatures != 0) {

            mDecor.postOnAnimation(mInvalidatePanelMenuRunnable);

        }

    } else {

        mDecor.setWindow(this);

    }

    if (mContentParent == null) {

        //初始化contentParent

        mContentParent = generateLayout(mDecor);



        // Set up decor part of UI to ignore fitsSystemWindows if appropriate.

 mDecor.makeFrameworkOptionalFitsSystemWindows();



        final DecorContentParent decorContentParent = (DecorContentParent) mDecor.findViewById(

 R.id.decor_content_parent);



        if (decorContentParent != null) {

            mDecorContentParent = decorContentParent;

            mDecorContentParent.setWindowCallback(getCallback());

            if (mDecorContentParent.getTitle() == null) {

                mDecorContentParent.setWindowTitle(mTitle);

            }

            //...

        } else {

            mTitleView = findViewById(R.id.title);

            if (mTitleView != null) {

                if ((getLocalFeatures() & (1 << FEATURE_NO_TITLE)) != 0) {

                    final View titleContainer = findViewById(R.id.title_container);

                    if (titleContainer != null) {

                        titleContainer.setVisibility(View.GONE);

                    } else {

                        mTitleView.setVisibility(View.GONE);

                    }

                    mContentParent.setForeground(null);

                } else {

                    mTitleView.setText(mTitle);

                }

            }

        }



        if (mDecor.getBackground() == null && mBackgroundFallbackDrawable != null) {

            mDecor.setBackgroundFallback(mBackgroundFallbackDrawable);

        }

        //创建动画

        // Only inflate or create a new TransitionManager if the caller hasn't

 // already set a custom one.

 if (hasFeature(FEATURE_ACTIVITY_TRANSITIONS)) {

            if (mTransitionManager == null) {

                final int transitionRes = getWindowStyle().getResourceId(

                        R.styleable.Window_windowContentTransitionManager,

                        0);

                if (transitionRes != 0) {

                    final TransitionInflater inflater = TransitionInflater.from(getContext());

                    mTransitionManager = inflater.inflateTransitionManager(transitionRes,

                            mContentParent);

                } else {

                    mTransitionManager = new TransitionManager();

                }

            }

            //...

        }

    }

}

创建DecorView的逻辑在generateDecor() 中,逻辑很简单,主要是构造一个DecorContext和DecorView,代码如下:

//PhoneWindow.generateDecor()

protected DecorView generateDecor(int featureId) {

    // System process doesn't have application context and in that case we need to directly use

 // the context we have. Otherwise we want the application context, so we don't cling to the

 // activity.

 Context context;

    if (mUseDecorContext) {

        Context applicationContext = getContext().getApplicationContext();

        if (applicationContext == null) {

            context = getContext();

        } else {

            context = new DecorContext(applicationContext, this);

            if (mTheme != -1) {

                context.setTheme(mTheme);

            }

        }

    } else {

        context = getContext();

    }

 return new DecorView(context, featureId, this, getAttributes());

}

创建mContentParent的代码在generateLayout()中,代码很长,我们只关注主要逻辑。

其中主要就干了三件事:

  1. 先通过各种条件和配置确定对应layout的资源Id,
  2. 然后通过mDecor.onResourcesLoaded(mLayoutInflater, layoutResource)来加载布局文件。
  3. 调用findViewById(``ID_ANDROID_CONTENT``)获取对应的控件返回。
//PhoneWindow.generateLayout   



protected ViewGroup generateLayout(DecorView decor) {

    // Apply data from current theme.

    //...

    // Inflate the window decor.

    //查找对应的资源文件

  int layoutResource;

    int features = getLocalFeatures();

    // System.out.println("Features: 0x" + Integer.toHexString(features));

 if ((features & ((1 << FEATURE_LEFT_ICON) | (1 << FEATURE_RIGHT_ICON))) != 0) {

        if (mIsFloating) {

            TypedValue res = new TypedValue();

            getContext().getTheme().resolveAttribute(

                    R.attr.dialogTitleIconsDecorLayout, res, true);

            layoutResource = res.resourceId;

        } else {

           layoutResource = R.layout.screen_title_icons;

        }

        // XXX Remove this once action bar supports these features.

 removeFeature(FEATURE_ACTION_BAR);

        // System.out.println("Title Icons!");

 } else if ((features & ((1 << FEATURE_PROGRESS) | (1 << FEATURE_INDETERMINATE_PROGRESS))) != 0

            && (features & (1 << FEATURE_ACTION_BAR)) == 0) {

        // Special case for a window with only a progress bar (and title).

 // XXX Need to have a no-title version of embedded windows.

   layoutResource = R.layout.screen_progress;

        // System.out.println("Progress!");

 } else if ((features & (1 << FEATURE_CUSTOM_TITLE)) != 0) {

        // Special case for a window with a custom title.

 // If the window is floating, we need a dialog layout

 if (mIsFloating) {

            TypedValue res = new TypedValue();

            getContext().getTheme().resolveAttribute(

                    R.attr.dialogCustomTitleDecorLayout, res, true);

            layoutResource = res.resourceId;

        } else {

            layoutResource = R.layout.screen_custom_title;

        }

        // XXX Remove this once action bar supports these features.

 removeFeature(FEATURE_ACTION_BAR);

    } else if ((features & (1 << FEATURE_NO_TITLE)) == 0) {

        // If no other features and not embedded, only need a title.

 // If the window is floating, we need a dialog layout

 if (mIsFloating) {

            TypedValue res = new TypedValue();

            getContext().getTheme().resolveAttribute(

                    R.attr.dialogTitleDecorLayout, res, true);

            layoutResource = res.resourceId;

        } else 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;

        }

        // System.out.println("Title!");

 } else if ((features & (1 << FEATURE_ACTION_MODE_OVERLAY)) != 0) {

        layoutResource = R.layout.screen_simple_overlay_action_mode;

    } else {

        // Embedded, so no decoration is needed.

   layoutResource = R.layout.screen_simple;

        // System.out.println("Simple!");

 }



    mDecor.startChanging();

 mDecor.onResourcesLoaded(mLayoutInflater, layoutResource);

    ViewGroup contentParent = (ViewGroup)findViewById( ID_ANDROID_CONTENT );

    //...

    mDecor.finishChanging();



    return contentParent;

}

其中有个很重要的代码:mDecor.onResourcesLoaded(mLayoutInflater, layoutResource),这里会初始化DecorView的根布局,添加,并赋值给mContentRoot。

//DecorView.java

//加载布局文件并赋值给mContentRoot。

void onResourcesLoaded(LayoutInflater inflater, int layoutResource) {

    if (mBackdropFrameRenderer != null) {

        loadBackgroundDrawablesIfNeeded();

        mBackdropFrameRenderer.onResourcesLoaded(

                this, mResizingBackgroundDrawable, mCaptionBackgroundDrawable,

                mUserCaptionBackgroundDrawable, getCurrentColor(mStatusColorViewState),

                getCurrentColor(mNavigationColorViewState));

    }



    mDecorCaptionView = createDecorCaptionView(inflater);

    final View root = inflater.inflate(layoutResource, null);

    if (mDecorCaptionView != null) {

        if (mDecorCaptionView.getParent() == null) {

            addView(mDecorCaptionView,

 new ViewGroup.LayoutParams( MATCH_PARENT , MATCH_PARENT ));

        }

       mDecorCaptionView.addView(root,

 new ViewGroup.MarginLayoutParams( MATCH_PARENT , MATCH_PARENT ));

    } else {



        // Put it below the color views.

   addView(root, 0, new ViewGroup.LayoutParams(MATCH_PARENT, MATCH_PARENT));

    }

   mContentRoot = (ViewGroup) root;

    initializeElevation();

}

到此DecorView的初始化就完成了,但其中android.R.id.content在哪儿呢?

其实创建mContentParent里面的布局文件都会包含一个android.R.id.content的FrameLayout布局来放置我们的content内容。

总结

总体流程:

  1. 创建代理类AppCompatActivityDelateImpl,调用对应的setContentView(View)
  2. 调用ensureSubdecor
  3. 然后创建DecorView,根据主题、类型查找对应的layout文件,查找到id为com.android.internal.R.id.content的控件,设为contentParent
  4. 创建subDecor,根据主题、对应的类型查找layout文件创建subDecor
  5. 调用mWindow.setContentView()将subDecor设置到DecorView中
  6. 从subDecor中查找id为android.R.id.content的控件,将用户的view添加进去