为什么说setContentView熟悉呢?因为该方法是我们从入门Android开发就接触的一个方法,在我们写过的每一个Activity中都有他的身影。但为什么又说setContentView陌生呢?因为我们在日常的开发中只知道用,并没有深入分析该方法是怎么将我们传入的layout资源id变为一个可视的界面的。今天我们就来揭开这层神秘的面纱,扒一扒其中的原理(注:源码基于Android12)。

因为我们日常开发的Activity最终都会继承自android.app.Activity,所以先看Activity类的setContentView方法:

android.app.Activity

1
2
3
4
public void setContentView(@LayoutRes int layoutResID) {
getWindow().setContentView(layoutResID); //1
initWindowDecorActionBar();
}

可以看到注释1处其实是调用了getWindow()setContentView方法,我们再看getWindow()做了什么。

android.app.Activity

1
2
3
public Window getWindow() {
return mWindow;
}

返回了一个mWindow,那mWindow又是什么,看他的声明代码:

android.app.Activity

1
2
@UnsupportedAppUsage
private Window mWindow;

mWindow是一个Window类,查看源码在Activityattach方法中mWindow被赋值:

android.app.Activity

1
2
3
4
5
6
7
8
9
10
11
12
13
14
final void attach(Context context, ActivityThread aThread,
Instrumentation instr, IBinder token, int ident,
Application application, Intent intent, ActivityInfo info,
CharSequence title, Activity parent, String id,
NonConfigurationInstances lastNonConfigurationInstances,
Configuration config, String referrer, IVoiceInteractor voiceInteractor,
Window window, ActivityConfigCallback activityConfigCallback, IBinder assistToken,
IBinder shareableActivityToken) {
attachBaseContext(context);

mFragments.attachHost(null /*parent*/);

mWindow = new PhoneWindow(this, window, activityConfigCallback);
...

看到这里,我们知道ActivitysetContentView最终调用的是PhoneWindowsetContentView方法,我们继续看PhoneWindowsetContentView方法:

com.android.internal.policy.PhoneWindow

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
@Override
public void setContentView(int layoutResID) {
if (mContentParent == null) {
installDecor();//1
} 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();
}
mContentParentExplicitlySet = true;
}

看注释1处的installDecor方法:

com.android.internal.policy.PhoneWindow

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
private void installDecor() {
mForceDecorInstall = false;
if (mDecor == null) {
mDecor = generateDecor(-1);//1
mDecor.setDescendantFocusability(ViewGroup.FOCUS_AFTER_DESCENDANTS);
mDecor.setIsRootNamespace(true);
if (!mInvalidatePanelMenuPosted && mInvalidatePanelMenuFeatures != 0) {
mDecor.postOnAnimation(mInvalidatePanelMenuRunnable);
}
} else {
mDecor.setWindow(this);
}
if (mContentParent == null) {
mContentParent = generateLayout(mDecor);//2
...

看注释1处的generateDecor方法:

com.android.internal.policy.PhoneWindow

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
protected DecorView generateDecor(int featureId) {
Context context;
if (mUseDecorContext) {
Context applicationContext = getContext().getApplicationContext();
if (applicationContext == null) {
context = getContext();
} else {
context = new DecorContext(applicationContext, this);
if (mTheme != -1) {
context.setTheme(mTheme);
}
}
} else {
context = getContext();
}
return new DecorView(context, featureId, this, getAttributes());
}

generateDecor最终new了一个DecorView对象,并把该对象返回并赋值给installDecor方法中的mDecor引用。

返回到installDecor方法继续看注释2的generateLayout方法:

com.android.internal.policy.PhoneWindow

1
2
3
4
5
6
7
8
9
10
protected ViewGroup generateLayout(DecorView decor) {
...
int layoutResource;
...
layoutResource = R.layout.screen_title;
...
mDecor.onResourcesLoaded(mLayoutInflater, layoutResource);
ViewGroup contentParent = (ViewGroup)findViewById(ID_ANDROID_CONTENT);
...
return contentParent;

由于generateLayout的代码很长,此处只贴出关键的代码。generateLayout的主要工作是将系统中的R.layout.screen_title布局资源添加到DecorView中,然后在PhoneWindow中调用findViewById(ID_ANDROID_CONTENT),找到R.layout.screen_titleidID_ANDROID_CONTENTFrameLayout,并返回给installDecor方法中的mContentParent引用。
R.layout.screen_title代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:orientation="vertical"
android:fitsSystemWindows="true">
<!-- Popout bar for action modes -->
<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:layout_width="match_parent"
android:layout_height="?android:attr/windowTitleSize"
style="?android:attr/windowTitleBackgroundStyle">
<TextView android:id="@android:id/title"
style="?android:attr/windowTitleStyle"
android:background="@null"
android:fadingEdge="horizontal"
android:gravity="center_vertical"
android:layout_width="match_parent"
android:layout_height="match_parent" />
</FrameLayout>
<FrameLayout android:id="@android:id/content"
android:layout_width="match_parent"
android:layout_height="0dip"
android:layout_weight="1"
android:foregroundGravity="fill_horizontal|top"
android:foreground="?android:attr/windowContentOverlay" />
</LinearLayout>

注意最下面的FrameLayoutid是为:content,再看源码中的ID_ANDROID_CONTENT的值:

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

最后再回到PhoneWindowsetContentView方法中:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
@Override
public void setContentView(int layoutResID) {
if (mContentParent == null) {
installDecor();//1
} 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);//2
}
mContentParent.requestApplyInsets();
final Callback cb = getCallback();
if (cb != null && !isDestroyed()) {
cb.onContentChanged();
}
mContentParentExplicitlySet = true;
}

注释1处的代码之前已经讲完了,然后看注释2处的代码,这句代码就是将setContentView传入的布局资源id加载到mContentParent,而mContentParent就是之前讲过的idcontentFrameLayout

至此一个Activity的结构也清楚了,结构如下图所示:

Activity中包含一个PhoneWindowPhoneWindow中包含一个DecorViewDecorView中包含系统的布局资源R.layout.screen_title。而setContentView就是将在Activity中传入的布局资源文件加载到idcontentFrameLayout中。

关注我