Android布局加载之setContentView源码分析

一、简介

这篇博客分析的是 API 28 最新的源码,来回顾一下我们的布局是怎么加载到 Activity 上的,采取的策略是先分析重要代码片段 + 阅后总结的形式。

二、Activity 的 setContentView()源码阅读

那么先看下 Activity.setContentView() 源码,这个看起来思路简单一些,AppCompatActivity中逻辑稍微不太一样。

// Activity#setContentView()
public void setContentView(@LayoutRes int layoutResID) {
    getWindow().setContentView(layoutResID);
    initWindowDecorActionBar();
}

private Window mWindow;
public Window getWindow() {
    return mWindow;
}

// 这个 mWindow 就是 PhoneWindow,是在 Activity.attach()方法中进行初始化的
//API 23和28的源码这里开始就不太一样了,我们看最新的
mWindow = new PhoneWindow(this, window, activityConfigCallback);  
  1. 我们在 Activity 中设置的布局,交给了 getWindow()获取的 Window 对象
  2. Window 是一个抽象类,其实现是 PhoneWindow 类,那么就转化为 PhoneWindow 的 setContentView()是怎么工作的

三、PhoneWindow 的 setContentView() 源码阅读

// PhoneWindow 类
ViewGroup mContentParent;
@Override
public void setContentView(int layoutResID) {
    // mContentParent 是一个 ViewGroup,第一次是 null,会执行
    if (mContentParent == null) {
        installDecor();
    } else if (!hasFeature(FEATURE_CONTENT_TRANSITIONS)) {
        mContentParent.removeAllViews();
    }

    if (hasFeature(FEATURE_CONTENT_TRANSITIONS)) {
        // 这里是判断是否做动画的,动画有起始和结束状态,抽象成了开始和结束的场景,回头专一写篇Android 里边的 场景动画
        final Scene newScene = Scene.getSceneForLayout(mContentParent, layoutResID,
                getContext());
        transitionTo(newScene);
    } else {
       // 将我们在 Activity 中设置的布局,就这样添加到了 ViewGroup 容器中了 
        mLayoutInflater.inflate(layoutResID, mContentParent);
    }
}

PhoneWindow 的 setContentView() 做了两件事

  1. 创建一个 mContentParent 容器,实际上就是一个 id 为 content 的FrameLayout容器
  2. 将我们设置的布局,通过 inflate 的方式,添加到了这个容器中

官方示意这个 mContentParent 就是存放 window 内容的容器,这个容器可能是 DecorView 本身,或者是 DecorView 的一个子类

至于 DecorView 是什么,马上就分析到了。

四、installDecor() 分析

是通过调用 PhoneWindow 里边的 installDecor() 方法来创建 DecorView 和 mContentParent 的。

// PhoneWindow 类,省略了 N 行代码,只留下重点代码
private void installDecor() {
    if (mDecor == null) {
        mDecor = generateDecor(-1);
    } else {
        mDecor.setWindow(this);
    }
    if (mContentParent == null) {
        mContentParent = generateLayout(mDecor);
        // ***
        }
    //***
}
  1. 创建一个 DecorView
  2. 创建了一个 mContentParent 容器

4.1 DecorView 的实例化

// PhoneWindow 类
protected DecorView generateDecor(int featureId) {
    Context context;
    //*** 省略 context 的初始化来源方式
    return new DecorView(context, featureId, this, getAttributes());
}

// Decorview 是一个 hide 的类,本质是一个 FrameLayout
public class DecorView extends FrameLayout{***}

DecorView 就是简单的通过 new 来实例化的。

4.2 mContentParent 的实例化

protected ViewGroup generateLayout(DecorView decor) {
    TypedArray a = getWindowStyle();
    // 这里是从系统主题里边读取一些配置(其实就是系统的自定义属性),设置进去的,比如说是否是悬浮窗口,
    //是否有 title,是否有 actionBar,是否全屏等等  省略
    //比如下面就是有无 ActionBar 的判断
    if (a.getBoolean(R.styleable.Window_windowNoTitle, false)) {
      requestFeature(FEATURE_NO_TITLE);
    } else if (a.getBoolean(R.styleable.Window_windowActionBar, false)) {
      // Don't allow an action bar if there is no title.
      requestFeature(FEATURE_ACTION_BAR);
    }
    // ***
    // 还省略了其它很多不关心的代码,比如会判断不同的 sdk 版本,走不同的配置,省略

    // Inflate the window decor.
    int layoutResource;
    int features = getLocalFeatures();
    // *** 省略一系列逻辑判断,最终会加载一个系统的布局文件,这个布局文件也即是添加到 DecorView 的布局 ID,
    // 我们以下面这两个系统的布局文件为例
    if ((features & (1 << FEATURE_ACTION_MODE_OVERLAY)) != 0) {
        layoutResource = R.layout.screen_simple_overlay_action_mode;
    } else {
        layoutResource = R.layout.screen_simple;
    }

    // 重点代码来了,SDK API23 和 28这里也有所不同了
    mDecor.startChanging();
    mDecor.onResourcesLoaded(mLayoutInflater, layoutResource); // API28多出来的方法

    // ID_ANDROID_CONTENT即 com.android.internal.R.id.content,就是一个 FrameLayout,
    // 就是刚添加进 DecorView 中的布局上的一个 id
    ViewGroup contentParent = (ViewGroup)findViewById(ID_ANDROID_CONTENT);
    
    
    // 接下来就是设置 DecorView 的背景 标题 标题颜色等等,省略部分代码

    mDecor.finishChanging();
    return contentParent; // 将布局容器返回
}

我们对 generateLayout(DecorView)方法做的事情做个总结:

  1. 获取系统的自定义属性作为配置,比如有没有标题栏啊,有没有状态栏啊,是否全屏等等
  2. 根据一系列的逻辑判断,最终会确定加载一个系统的 layout 布局
  3. 将该 layout 布局 xml 转化为一个 View 对象,添加到 DecorView 中
  4. 给 DecorView 设置一些属性,比如 background、title、titleColor 等等
  5. 将加载的系统的 layout布局文件中的,id 为 com.android.internal.R.id.content 的 FrameLayout 容器强转为 ViewGroup,并最终返回该实例

五、将系统的布局文件添加到 DecorView 上

在不同版本的 sdk 里边的处理逻辑是不一样的,之前看的 API23 的逻辑,更简单也更清晰一点:

mDecor.startChanging();

View in = mLayoutInflater.inflate(layoutResource, null);
decor.addView(in, new ViewGroup.LayoutParams(MATCH_PARENT, MATCH_PARENT));
mContentRoot = (ViewGroup) in;

ViewGroup contentParent = (ViewGroup)findViewById(ID_ANDROID_CONTENT);

直接将上一步确定下来的系统的布局资源文件转化为一个 view,然后通过 DecorView.add(View,LayoutParams)的方式,将一个系统的布局挂载到了 DecorView 上。

看下 API28 中的逻辑,通过mDecor.onResourcesLoaded(mLayoutInflater, layoutResource); 将逻辑单独提取到了 DecorView 类中,

// DecorView 类
void onResourcesLoaded(LayoutInflater inflater, int layoutResource) {
    // ***
    // 将最终确定下来的系统的布局,转为 View,添加到 DecorView 容器中
    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();
}

六、系统的布局文件

加载的系统布局资源文件可以在 AS 里边直接看,前提是关联好源码;也可以在 sdk/platforms/android-28/data/res/layout 路径下查看。

那么系统的布局文件 [screen_simple.xml]长什么样子呢?

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

可以看到就是一个简单的 线性布局,有一个 ViewStub 子元素和一个 android:id/content 的 FrameLayout,我们注意下一这个 id。

看下系统的布局文件[screen_simple_overlay_action_mode.xml]又是怎么样的。

<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:fitsSystemWindows="true">
    <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" />
    <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>  

其实多看几个系统的布局文件大致都差不多,都会有一个 ViewStub 和一个 id 为 android:id/content的 FrameLayout,只不过其它的布局文件中还有一些其它的 View 罢了,我们在 activity 中设置的布局 layout 其实就是加载到了 id 为 content 的 FrameLayout 容器中的。

上面我们已经分析过了,正是通过 ViewGroup contentParent = (ViewGroup)findViewById(ID_ANDROID_CONTENT);来将该 FrameLayout 强转成 ViewGroup 并返回的,即是加载到了 contentParent 上了。

七、总结

我们在 Activity 中通过 setContentView(layoutResID)设置的布局,通过下面几步加载出来的

  1. 在 Activity 的 onCreate()中设置我们的布局,实际上是设置给了 Window
  2. 在 Activity 的 attach()方法中,进行Window 的实例化,即实例化 PhoneWindow 对象
  3. 调用 PhoneWindow 的 setContentView() 方法
  4. 在 PhoneWindow 类中创建 DecorView,将系统的一个布局转为View,然后添加到 DecorView 上
  5. 将我们自己写的布局填充到,上一步确定下来的系统布局中 id 为 R.id.content 的 FrameLayout 容器上

根据源码,我们绘制出来的 Activity 的层级结构如下:

image

低版本的 Android Studio 里边有个很好用的工具,Android Device Monitor 工具,可以用来页面的布局层级结构,高版本 as 上已经没有该 tool 的入口了,但是可以在 sdk/tools/目录下找 monitor就可以了,但是我电脑上还是不能用,那么取而代之的是as 菜单里边(Tools ->Layout Inspector)提供了相对应的工具,但是只能查看到布局中 DecorView 节点的内容,下面简单放张图。

我们有一个布局如下:

<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context=".MainActivity">

    <TextView
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_centerInParent="true"
        android:text="Hello World!" />

</RelativeLayout>

该布局文件对应的 Layout Captures为:

image

最后放一张,早些年写的笔记中的一张图:

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