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
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 204,921评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 87,635评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 151,393评论 0 338
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,836评论 1 277
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,833评论 5 368
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,685评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,043评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,694评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 42,671评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,670评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,779评论 1 332
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,424评论 4 321
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,027评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,984评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,214评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,108评论 2 351
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,517评论 2 343

推荐阅读更多精彩内容