setContentView源码分析

Activity调用setContentView将布局添加到窗口的流程如图:

setContentView_flow.png

在深入了解setContentView之前,先提出以下疑问:

  1. 为什么调用setContentView就能将布局显示出来?
  2. 为什么requestFeature需要在setContentView之前调用?
  3. PhoneWindow和Window之间有什么关系?
  4. DecorView和我们的布局有什么关系?
  5. 为什么继承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_class.png

来看看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)

  1. 调用installDecor()方法初始化mDecor和mContentParent,当再次调用setContentView方法时,如果没有添加场景转换动画,mContentParent会移除所有添加的View
if (mContentParent == null) {
    installDecor();
} else if (!hasFeature(FEATURE_CONTENT_TRANSITIONS)) {
    mContentParent.removeAllViews();
}
  1. 如果添加了场景转换动画,会执行此动画效果;否则调用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方法中创建的

  1. 获取TypedArray
TypedArray a = getWindowStyle();
  1. 根据TypedArray得到的属性设置是否启用屏幕的一些特性
if (a.getBoolean(R.styleable.Window_windowNoTitle, false)) {
            requestFeature(FEATURE_NO_TITLE);
}
...
  1. 根据第二步设置的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);
...
  1. 创建mContentParent
ViewGroup contentParent = (ViewGroup)findViewById(ID_ANDROID_CONTENT);
  1. 为顶层窗口设置背景和标题
...
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的布局结构图如下:

Activity的View的布局结构图.png


兼容包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

  1. 确保subDecor是否创建,如果没有则创建
ensureSubDecor();
  1. 将AppCompatActivity中setContentView中传入的布局添加到subDecor中id为content的FrameLayout
ViewGroup contentParent = (ViewGroup) mSubDecor.findViewById(android.R.id.content);
contentParent.removeAllViews();
LayoutInflater.from(mContext).inflate(resId, contentParent);
  1. 回调onContentChanged方法
mOriginalWindowCallback.onContentChanged();

AppCompatDelegateImplV9#ensureSubDecor

  1. 如果subDecor没有创建过,则创建
mSubDecor = createSubDecor();
  1. 如果在subDecor创建之前就设置了标题,在这里回调onTitleChanged
// If a title was set before we installed the decor, propagate it now
CharSequence title = getTitle();
if (!TextUtils.isEmpty(title)) {
    onTitleChanged(title);
}
  1. 将标记设置为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中加载的是兼容的布局

  1. 获取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);
  1. 将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);
  1. 将subDecor作为内容布局传给PhoneWindow中
// Now set the Window's content view with the decor
mWindow.setContentView(subDecor);

根据上面的分析可以得到在AppCompatActivity中View的布局结构图如下:

AppCompatActivity的View布局结构.png

总结

至此我们已经分析完了setContentView的源码,对于之前提的疑问也有了答案:

  1. 为什么调用setContentView就能将布局显示出来?
    调用setContentView方法内部会调用PhoneWindow的setContentView方法,其内部通过mLayoutInflater.inflate(layoutResID, mContentParent);加载到DecorView的子布局mContentParent中,而DecorView是我们的顶级View,会在Activity启动后加载到当前Activity的应用程序窗口,所以我们调用setContentView就能将我们的布局显示出来。
  2. 为什么requestFeature需要在setContentView之前调用?
    当我们在Activity中调用了setContentView方法,会调用PhoneWindow的generateLayout方法,该方法会根据requestFeature方法设置的属性来选择DecorView中加载的布局,以及根据一些特性,例如是否显示标题,来设置当前窗口的特性。
  3. PhoneWindow和Window之间有什么关系?
    当我们在Activity中调用了setContentView方法,内部会调用Window的setContentView方法,Window是一个抽象类,而PhoneWindow是抽象类Window的唯一子类。Window的实例必须当做顶级View添加到WindowManager中。
  4. DecorView和我们的布局有什么关系?
    DecorView是我们窗口的顶级View,意味着我们使用Hierarchy Viewer查看View的层级关系时,最上层的View都是DecorView。我们的布局是加载在DecorView下的一个id为content的FrameLayout中的。
  5. 为什么继承AppCompactActivity后,主题需要继承AppCompactTheme?
    在AppCompactActivity中调用setContentView,内部会调用AppCompatDelegateImplV9的createSubDecor方法,其中会加载兼容Window的主题AppCompactTheme

推荐阅读更多精彩内容