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

新媒体管家

点击上方“程序员大咖”,选择“置顶公众号”

关键时刻,第一时间送达!

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

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


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


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


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


一图胜千言


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


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


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


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


但是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树如下所示:


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

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树之间的关系。 


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

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

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

  • 来自:****-孙群

  • http://blog.****.net/iispring

  • 程序员大咖整理发布,转载请联系作者获得授权

Android源码解析Activity#setContentView()方法Android源码解析Activity#setContentView()方法【点击成为Python大神】