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源码解析(二十四)-->onSaveInstanceState执行时机
framework之Activity 生命周期解析(基于Android11源码)