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]长什么样子呢?


    
    

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

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


    
    
  

其实多看几个系统的布局文件大致都差不多,都会有一个 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 的层级结构如下:

Android布局加载之setContentView源码分析_第1张图片
image

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

我们有一个布局如下:




    


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

Android布局加载之setContentView源码分析_第2张图片
image

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

Android布局加载之setContentView源码分析_第3张图片
image

你可能感兴趣的:(Android布局加载之setContentView源码分析)