Android源码解析Activity#setContentView()方法

Posted 孙群

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Android源码解析Activity#setContentView()方法相关的知识,希望对你有一定的参考价值。

在Activity初始化的过程中,会调用Activity的attach方法,在该方法中会创建一个PhoneWindow的实例,将其作为Activity的mWindow成员变量。

在执行完了Activity#attach()方法之后,会执行Activity#onCreate()方法。

我们在Activity#onCreate()方法中会就调用setContentView()方法,我们将一个Layout的资源ID传入该方法,调用了该方法之后就将layout资源转换成ViewGroup了,之后就可以调用findViewById()查找ViewGroup中的各种View。


一图胜千言

为了让大家更清晰地理顺代码的调用过程,我做了一张图,如下所示:

我在上图中每一步都设置了一个超链接,如下图所示:

但是Markdown中内嵌的SVG不支持超链接,如果想看一下每一步代码在android源码中执行的位置,可以用浏览器打开链接 https://ispring.github.io/svg/setContentView_zh.svg,然后单击每一步的超链接就可以了。


源码解析

Activity#setContentView()源码如下所示:

public void setContentView(@LayoutRes int layoutResID) 
    getWindow().setContentView(layoutResID);
    initWindowDecorActionBar();

首先通过getWindow()方法得到了mWindow,其实它是一个PhoneWindow类型的对象,我们之前提到PhoneWindow是在Activity#attach()方法中被初始化的。然后调用了PhoneWindow#setContentView()方法。

PhoneWindow#setContentView()源码如下所示:

public void setContentView(int layoutResID) 
    if (mContentParent == null) 
        //installDecor()方法会调用generateDecor()和generateLayout()方法
        installDecor();
     else if (!hasFeature(FEATURE_CONTENT_TRANSITIONS)) 
        mContentParent.removeAllViews();
    

    if (hasFeature(FEATURE_CONTENT_TRANSITIONS)) 
        final Scene newScene = Scene.getSceneForLayout(mContentParent, layoutResID,
                getContext());
        transitionTo(newScene);
     else 
        mLayoutInflater.inflate(layoutResID, mContentParent);
    
    mContentParent.requestApplyInsets();
    final Callback cb = getCallback();
    if (cb != null && !isDestroyed()) 
        //最后触发内容变化的回调
        cb.onContentChanged();
    

该方法传入了一个Activity的layout资源layoutResID,该资源就代表了Activity中的content内容,理解了此处content所代表的意义之后,要说一下PhoneWindow中有两个比较重要的成员变量mContentParent和mContentRoot,这两个字段都是ViewGroup类型。mContentParent从字面上看就是content的parent,即Activity的layout是要放到mContentParent中去的。mContentRoot从字面上看就是content的root,即content的根结点,一般情况下,mContentParent是放置在mContentRoot中的。即mContentRoot > mContentParent > content。关于如何实例化mContentRoot 、mContentParent 和 content,后面会详细说明。

PhoneWindow#setContentView()方法中会调用installDecor()方法。

PhoneWindow#installDecor()方法的源码如下所示:

private void installDecor() 
    if (mDecor == null) 
        //创建DecorView
        mDecor = generateDecor();
        mDecor.setDescendantFocusability(ViewGroup.FOCUS_AFTER_DESCENDANTS);
        mDecor.setIsRootNamespace(true);
        if (!mInvalidatePanelMenuPosted && mInvalidatePanelMenuFeatures != 0) 
            mDecor.postOnAnimation(mInvalidatePanelMenuRunnable);
        
    
    if (mContentParent == null) 
        //根据features生成Activity的layout资源的父节点
        mContentParent = generateLayout(mDecor);
        ...
    

installDecor()中会调用generateDecor()和generateLayout()方法。

PhoneWindow#generateDecor()会创建DecorView,其源码如下所示:

protected DecorView generateDecor() 
    return new DecorView(getContext(), -1);

执行完了generateDecor()之后,就会执行generateLayout(),其源码如下所示:

protected ViewGroup generateLayout(DecorView decor) 
    // Apply data from current theme.

    TypedArray a = getWindowStyle();

    ...
    //根据Theme和Style计算features

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

    if (a.getBoolean(R.styleable.Window_windowActionBarOverlay, false)) 
        requestFeature(FEATURE_ACTION_BAR_OVERLAY);
    

    if (a.getBoolean(R.styleable.Window_windowActionModeOverlay, false)) 
        requestFeature(FEATURE_ACTION_MODE_OVERLAY);
    

    if (a.getBoolean(R.styleable.Window_windowSwipeToDismiss, false)) 
        requestFeature(FEATURE_SWIPE_TO_DISMISS);
    

    ...


    // Inflate the window decor.

    //根据features计算layoutResource,此处的layoutResource表示要插入到DecorView中的子节点
    int layoutResource;
    int features = getLocalFeatures();
    // System.out.println("Features: 0x" + Integer.toHexString(features));
    if ((features & (1 << FEATURE_SWIPE_TO_DISMISS)) != 0) 
        layoutResource = R.layout.screen_swipe_dismiss;
     else if ((features & ((1 << FEATURE_LEFT_ICON) | (1 << FEATURE_RIGHT_ICON))) != 0) 
        ...
        layoutResource = R.layout.screen_title_icons;
     else if ((features & ((1 << FEATURE_PROGRESS) | (1 << FEATURE_INDETERMINATE_PROGRESS))) != 0
            && (features & (1 << FEATURE_ACTION_BAR)) == 0) 
        layoutResource = R.layout.screen_progress;
     else if ((features & (1 << FEATURE_CUSTOM_TITLE)) != 0) 
        ...
    layoutResource = R.layout.screen_custom_title;
     else if ((features & (1 << FEATURE_NO_TITLE)) == 0) 
        ...
        layoutResource = R.layout.screen_action_bar 或 R.layout.screen_title;
     else if ((features & (1 << FEATURE_ACTION_MODE_OVERLAY)) != 0) 
        ...
        layoutResource = R.layout.screen_simple_overlay_action_mode;
     else 
        // 不需要装饰,直接用最简单的资源文件即可
        layoutResource = R.layout.screen_simple;
    

    mDecor.startChanging();

    //将计算到的layoutResource转换为实际的View,并将其插入到DecorView中,将其作为成员变量mContentRoot
    View in = mLayoutInflater.inflate(layoutResource, null);
    decor.addView(in, new ViewGroup.LayoutParams(MATCH_PARENT, MATCH_PARENT));
    mContentRoot = (ViewGroup) in;

    //计算出的layoutResource对应着mContentRoot,它其中肯定有一个ID叫做ID_ANDROID_CONTENT的ViewGroup
    //从中找到该Group,赋值给contentParent,contentParent就表示我们Actiivy的layout资源文件的父节点
    ViewGroup contentParent = (ViewGroup)findViewById(ID_ANDROID_CONTENT);
    if (contentParent == null) 
        throw new RuntimeException("Window couldn't find content container view");
    
    ...

    mDecor.finishChanging();

    //contentParent会赋值给成员变量mContentParent
    return contentParent;

generateDecor()这个方法从字面上看就知道是要产生layout,那么要产生哪些layout呢?其实这个方法就是为了创建我们之前说的mContentRoot和mContentParent。generateDecor()方法的返回值就是mContentParent,我们具体分析一下代码的执行过程。

generateDecor()方法会首先根据Theme和Style,会多次调用requestFeature()方法,计算特性features,点此查看对应源码。

然后会根据计算出的特性features,要计算一个layout资源layoutResource,layoutResource就是对应着mContentRoot。features具备的特性不同,layoutResource的值也就不同,layoutResource的可能取值有:
com.android.internal.R.layout.screen_swipe_dismiss.xml
com.android.internal.R.layout.screen_title_icons
com.android.internal.R.layout.screen_progress
com.android.internal.R.layout.screen_custom_title
com.android.internal.R.layout.screen_action_bar
com.android.internal.R.layout.screen_title
com.android.internal.R.layout.screen_simple_overlay_action_mode
com.android.internal.R.layout.screen_simple等。
根据features计算layoutResource的具体逻辑可参见源码

在计算出layoutResource之后,会将计算到的layoutResource转换为实际的View,将其作为成员变量mContentRoot,并将其插入到DecorView中,也就是说mDecor是mContentRoot的父节点,此时PhoneWindow中的View树:mDecor -> mContentRoot

之后会调用findViewById()方法查找ID为ID_ANDROID_CONTENT的View。PhoneWindow是继承自Window的,PhoneWindow的findViewById()是在Window中定义,其源码如下所示:

public View findViewById(@IdRes int id) 
    return getDecorView().findViewById(id);

由此我们可以看出PhoneWindow的findViewById()方法其实就是从mDecor中查找View。ID_ANDROID_CONTENT也是在Window类中定义的,如下所示:

public static final int ID_ANDROID_CONTENT = com.android.internal.R.id.content;

其实无论上面计算的layoutResource是哪个layout,该layout中都有一个id为content的ViewGroup。

举例来说,我们有一个MainActivity,其直接继承自Activity,Application和MainActivity都没有设置任何Theme和Style,当App运行在Android 6.0系统上的时候,得到的layoutResource是com.android.internal.R.layout.screen_action_bar,具体如下所示:

<?xml version="1.0" encoding="utf-8"?>
<com.android.internal.widget.ActionBarOverlayLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    android:id="@+id/decor_content_parent"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:splitMotionEvents="false"
    android:theme="?attr/actionBarTheme">
    <FrameLayout android:id="@android:id/content"
                 android:layout_width="match_parent"
                 android:layout_height="match_parent" />
    <com.android.internal.widget.ActionBarContainer
        android:id="@+id/action_bar_container"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:layout_alignParentTop="true"
        style="?attr/actionBarStyle"
        android:transitionName="android:action_bar"
        android:touchscreenBlocksFocus="true"
        android:gravity="top">
        <com.android.internal.widget.ActionBarView
            android:id="@+id/action_bar"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            style="?attr/actionBarStyle" />
        <com.android.internal.widget.ActionBarContextView
            android:id="@+id/action_context_bar"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:visibility="gone"
            style="?attr/actionModeStyle" />
    </com.android.internal.widget.ActionBarContainer>
    <com.android.internal.widget.ActionBarContainer android:id="@+id/split_action_bar"
                  android:layout_width="match_parent"
                  android:layout_height="wrap_content"
                  style="?attr/actionBarSplitStyle"
                  android:visibility="gone"
                  android:touchscreenBlocksFocus="true"
                  android:gravity="center"/>
</com.android.internal.widget.ActionBarOverlayLayout>

我们可以看到com.android.internal.R.layout.screen_action_bar中有一个android:id="@android:id/content"的FrameLayout,该FrameLayout就是mContentParent

当generateLayout()执行完毕后,installDecor()方法也就执行完了。

这样PhoneWindow#setContentView(layoutResID)中会执行下面的代码:

mLayoutInflater.inflate(layoutResID, mContentParent);

此处的layoutResID是我们Activity的资源文件,比如R.layout.activity_main,此处将该文件inflate成具体的View,并将其放入到mContentParent中。

之后还会通过代码cb.onContentChanged()触发内容变化回调的执行。

这样Activity#setContentView()也就执行完了,假设我们的R.layout.activity_main中只有一个RelativeLayout,那么通过hierarchyviewer查看到的View树如下所示:

View树的根结点是PhoneWindow$DecorView类型的,此处的$表示DecorView是PhoneWindow的一个内部类,该DecorView也就是PhoneWindow中的字段mDecor。screen_action_bar定义的ActionBarOverlayLayout就是PhoneWindow的mContentRoot,其是mDecor的子节点。screen_action_bar中内部id为content的FrameLayout就是PhoneWindow中的mContentParent,其是我们Activity的layout的父节点。

我们回过头来再思考一下DecorView这个类,英文decor的意思其实就是装饰,也就是说这是一个起到装饰的类,除了装饰,DecorView还要作为我们自己layout的容器。那到底装饰了什么东西呢?我个人认为,上图中除了绿色文本标识的其他的View都可以看做装饰,因为这些View是根据features特性的不同而创建的,如果需要有Action Bar的特性,那么就装饰上一个View作为Action Bar的容器;如果不需要Action Bar,但需要显示title,那么就装饰上一个View作为title的容器,等等。

最后我们再用一张图理顺Activity、PhoneWindow与View树之间的关系。

希望本文对大家理解setContentView()方法有所帮助!

相关阅读:
[GitHub开源]Android自定义View实现微信打飞机游戏
Android中View的量算、布局及绘图机制

以上是关于Android源码解析Activity#setContentView()方法的主要内容,如果未能解决你的问题,请参考以下文章

Android源码解析Activity#setContentView()方法

Android Activity启动过程源码解析

android源码解析(二十四)-->onSaveInstanceState执行时机

framework之Activity 生命周期解析(基于Android11源码)

framework之Activity 生命周期解析(基于Android11源码)

framework之Activity 生命周期解析(基于Android11源码)