夜夜使用的setContentView()里面到底做了什么?

一、写在前面

我们都知道继承Activity的onCreate()方法然后setContentView(R.layout.xxx)可以设置我们自己的布局文件,但是布局文件生成的View到底添加到哪里去了呢?首先在Activity这个类中我们可以看到有个mWindow变量,这个变量是在attach()方法里面创建的:mWindow = new PhoneWindow(...),然后在我们setContentView()的时候会创建一个mDecor变量,这就是我们Activity的第一个View,然后再往里面添加各种View,包括我们自己布局的View。此文就是分析下这些View都是怎么添加的。

以前的以前我们自己的Activity都是直接继承Activity这个类的,从API21开始,我们一般都继承AppCompatActivity了,下面分别分析。

注:以下源码版本为API28。

二、继承Activity的时候

// 这是我们普通的Activity,继承onCreate,添加我们的布局
protected void onCreate(@Nullable Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.xxx);
}

// 类:Activity
public void setContentView(@LayoutRes int layoutResID) {
    // 跟踪getWindow()可以看到mWindow = new PhoneWindow(...),返回的是一个PhoneWindow
    // 所以这里跳到PhoneWindow的setContentView(...)
    // IDEA里面可以直接按ctrl+alt+单击弹出实现此方法的类,选择PhoneWindow的进入就行
    getWindow().setContentView(layoutResID);
    // ...
}

// 类:PhoneWindow
public void setContentView(int layoutResID) {
    if (mContentParent == null) {
        installDecor();
    }
    // ...
    // 把我们自己的布局文件inflate后添加到mContentParent中
    // mContentParent就是我们自己的布局的父View
    mLayoutInflater.inflate(layoutResID, mContentParent);
    // ...
}

// 类:PhoneWindow
private void installDecor() {
    // ...
    if (mDecor == null) {
        // 1、生成DecorView
        // mDecor是PhoneWindow的一个变量,同时PhoneWindow也是mDecor的一个变量
        mDecor = generateDecor(-1);
        // ...
    }
    // ...
    if (mContentParent == null) {
        // 2、生成mContentParent
        // 从前面我们知道这就是我们自己的布局的父类
        mContentParent = generateLayout(mDecor);
        // ...
    }    
}
1、生成DecorView
// 类:PhoneWindow
protected DecorView generateDecor(int featureId) {
    // ...
    // 直接创建了一个DecorView,点击去查看DecorView是继承了FrameLayout
    return new DecorView(context, featureId, this, getAttributes());
}
2、生成mContentParent
// 类:PhoneWindow
protected ViewGroup generateLayout(DecorView decor) {
    // 获取窗口属性,也就是AndroidManifest.xml中设置的主题
    // View的属性在layout.xml里面配置,Window的属性在主题里面配置
    TypedArray a = getWindowStyle();
    // 这里是一系列的主题设置
    // ...

    // 根据上面设置的属性,加载不同的根布局,所有布局都一定会有一个View的id是ID_ANDROID_CONTENT,
    // 也就是mContentParent,也就是我们setContentView()传入的View的父View
    // 这里随便选一个布局,比如:R.layout.screen_simple
    // 该文件在源码中的路径:frameworks/base/core/res/res/layout/screen_simple.xml
    // 下载源码方法:https://blog.csdn.net/qiantujava/article/details/102847414
    // 在线源码地址:http://aospxref.com/android-9.0.0_r45/xref/frameworks/base/core/res/res/layout/screen_simple.xml
    layoutResource = R.layout.screen_simple;

    // 1、解释上面选定的layout修饰布局文件
    mDecor.onResourcesLoaded(mLayoutInflater, layoutResource);

    // 这里findViewById其实调用的是getDecorView().findViewById(id)
    // 上面所有布局都会有ID_ANDROID_CONTENT这个id,也就是:android:id="@android:id/content"
    // 我们自己的布局文件就是添加到这个ViewGroup中的
    ViewGroup contentParent = (ViewGroup)findViewById(ID_ANDROID_CONTENT);
    // ...
    return contentParent;
}

1、解释修饰布局文件

// 类:DecorView
// 解释layout文件
void onResourcesLoaded(LayoutInflater inflater, int layoutResource) {
    // ...
    // Caption有标题的意思
    mDecorCaptionView = createDecorCaptionView(inflater);
    // 解释刚才那个布局screen_simple.xml,里面会有ID_ANDROID_CONTENT
    final View root = inflater.inflate(layoutResource, null);
    if (mDecorCaptionView != null) {
        // 没搞清楚mDecorCaptionView是个什么东西,先不管这个
    } else {
        // 没有mDecorCaptionView的时候,直接把root添加到DecorView中
        addView(root, 0, new ViewGroup.LayoutParams(MATCH_PARENT, MATCH_PARENT));
    }
    mContentRoot = (ViewGroup) root;
    // ...
}

上面有几个重点关注的变量:
mWindow是PhoneWindow的对象,是Activity的一个变量,在Activity.attach()方法里面就创建了;
mDecor是DecorView的对象,是PhoneWindow的一个变量;
mContentRoot是修饰布局文件inflate得到的View,添加到了mDecor中;
mContentParent是mContentRoot中id为ID_ANDROID_CONTENT的View;
最后我们setContentView传入的View是添加到mContentParent的。
所以我们可以得到个大概的布局关系图:

image

三、继承AppCompatActivity

// 这是我们普通的Activity,继承onCreate,添加我们的布局
protected void onCreate(@Nullable Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.xxx);
}

// 类:AppCompatActivity
public void setContentView(@LayoutRes int layoutResID) {
    // 点进去getDelegate()可以看到返回的是AppCompatDelegateImpl
    // 所以这里调用的是AppCompatDelegateImpl.setContentView()
    getDelegate().setContentView(layoutResID);
}

// 类:AppCompatDelegateImpl
public void setContentView(int resId) {
    // 确保mSubDecor是否已经生成
    // 这里还不知道mSubDecor是个什么东西,下面再看
    ensureSubDecor();
    // 把我们自己的布局添加到contentParent
    // 其实从ensureSubDecor()里面可以知道这个android.R.id.content对应的View已经不是前面分析的mContentParent了
    // 而是mSubDecor里面的一个ContentFrameLayout
    ViewGroup contentParent = (ViewGroup) mSubDecor.findViewById(android.R.id.content);
    contentParent.removeAllViews();
    LayoutInflater.from(mContext).inflate(resId, contentParent);
    // ...
}

// 类:AppCompatDelegateImpl
private void ensureSubDecor() {
    if (!mSubDecorInstalled) {
        mSubDecor = createSubDecor();
        // ...
    }
}

// 类:AppCompatDelegateImpl
private ViewGroup createSubDecor() {
    // 设置各种主题
    // ...
    // 跟踪这个mWindow,可以看到是从Activity传过来的,所以这个也是PhoneWindow
    // getDecorView()里面就一个判断,如果mDecor==null,就installDecor()
    // installDecor()在上面已经分析了,一毛一样的
    // installDecor()后就生成了mDecor和mContentParent了
    mWindow.getDecorView();

    // 根据不同的主题inflate不同的布局文件,然后赋值给subDecor
    // 所以,subDecor就是一个普通的ViewGroup
    final LayoutInflater inflater = LayoutInflater.from(mContext);
    ViewGroup subDecor = null;
    // ...
    subDecor = (ViewGroup) inflater.inflate(R.layout.abc_dialog_title_material, null);
    // ...
    // 上面所有的布局都会有个id=R.id.action_bar_activity_content的ContentFrameLayout
    final ContentFrameLayout contentView = (ContentFrameLayout) subDecor.findViewById(
            R.id.action_bar_activity_content);

    // 上面mWindow.getDecorView()已经生成mContentParent了,id就是android.R.id.content
    // 所以这里windowContentView其实就是mContentParent
    final ViewGroup windowContentView = (ViewGroup) mWindow.findViewById(android.R.id.content);
    if (windowContentView != null) {
        // 把windowContentView的所有子view都添加到contentView中
        while (windowContentView.getChildCount() > 0) {
            final View child = windowContentView.getChildAt(0);
            windowContentView.removeViewAt(0);
            contentView.addView(child);
        }

        // 把windowContentView的id设为了NO_ID,把contentView的id设成了android.R.id.content
        // 此后各个view的关系还是:DecorView->mContentRoot->mContentParent(就是windowContentView)
        // 但是mContentParent里面的view已经全部移到contentView中了,同时contentView还没添加到DecorView
        windowContentView.setId(View.NO_ID);
        contentView.setId(android.R.id.content);
        // ...
    }

    // 把contentView添加到DecorView中
    mWindow.setContentView(subDecor);
    // ...
    return subDecor;
}

// 类:PhoneWindow
public void setContentView(View view, ViewGroup.LayoutParams params) {
    // 前面createSubDecor()中调了mWindow.getDecorView()已经生成了mContentParent,所以直接跳过
    if (mContentParent == null) {
        installDecor();
    }
    // ...
    // 这个view是前面传过来的subDecor
    mContentParent.addView(view, params);
    // ...
}

由上面的分析,我们同样可以得到以下总结:
mWindow是PhoneWindow的对象,是Activity的一个变量,在Activity.attach()方法里面就创建了;
mDecor是DecorView的对象,是PhoneWindow的一个变量;
mContentRoot是修饰布局文件inflate得到的View,添加到了mDecor中;
mContentParent是mContentRoot中id为ID_ANDROID_CONTENT的View,但是被设置为NO_ID了;
mSubDecor是含有R.id.action_bar_activity_content的布局,mSubDecor会被添加到mContentParent中;
contentView是ContentFrameLayout,是mSubDecor中id=R.id.action_bar_activity_content的view,最后会被设成id=android.R.id.content。
最后我们setContentView()传入的view是会被添加到id=android.R.id.content的view中的,
所以我们可以得到个大概的布局关系图:

image

四、继承Activity和继承AppCompatActivity的区别

从上面的分析可以看出来,继承AppCompatActivity的时候,布局中多了个mSubDecorcontentView,之前我们的布局是添加到mContentParent的,现在是添加到contentView了,其实换汤不换药,只是在中间插入了一层view。为什么要这样呢?因为在API28的时候官方大力推荐MaterialDesign,在中间插入了一层mSubDecor就可以为所欲为的加入MaterialDesign的东西了,应该是酱紫吧。

五、写在最后

其实看源码也没有那么难嘛,一开始看可能会觉得超复杂,跳来跳去的,谷歌什么破工程师,写出来的代码乱七八糟。但是当你看的多了,这里的多包括看的次数多和看的内容多,看的次数多了你会对这段源码印象更深刻,下次再回来看的时候不会再那么陌生了,而看的内容多的话,你会发现越来越清晰了,各个点都是有关系的,之前那个点可能没看懂,看到这个点的时候忽然明白了上个点,都是有联系的。
PS. 标题里面的夜夜并没有说假话,现在是凌晨4点、

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

推荐阅读更多精彩内容