Android 复盘——你真的了解 setContentView 吗?

# 1. AppCompatDelegate 的 setContentView()

分析 Android 中的 View,我们先从进入应用的看到的的一个 View 入手,第一个 View 就是 通过 setContentView() 这个方法进行加载的。我们来看 setContentView() 的源码:

1
2
3
public void setContentView(@LayoutRes int layoutResID) {
this.getDelegate().setContentView(layoutResID);
}

AppCompatActivity 中的 setContentView() 又调用了 AppCompatDelegate 中的 setContentView() 方法,那 AppCompatDelegate 是做什么的呢?

AppCompat 出现在 v7 包,它的作用是让 API 等级在 7 之上的设备也能使用 ActionBar,在 v7:21 之后,AppCompat 可以让 API 在 7 之上的设备使用 MD、ToolBar 等效果。之前的 ActionBarActivity 也被取代为 AppCompatActivity。但 AppCompatActivity 的内部回调是由 AppCompatDelegate 来实现的。

AppCompatDelegate 是一个抽象类,它的实现类是 AppCompatDelegateImpl,现在看 AppCompatDelegateImpl 中的 setContentView() 方法:

1
2
3
4
5
6
7
8
9
10
public void setContentView(int resId) {
// 创建 DecorView,DecorView 是视图中的顶级 View
this.ensureSubDecor();
// 获取 DecorView 中的 content 部分
ViewGroup contentParent = (ViewGroup)this.mSubDecor.findViewById(16908290);
contentParent.removeAllViews();
// 将我们编写的界面填充到 content 中
LayoutInflater.from(this.mContext).inflate(resId, contentParent);
this.mOriginalWindowCallback.onContentChanged();
}

# 2. DecorView

在 AppCompatDelegateImpl 的 setContentView() 中,通过 ensureSubDecor() 方法为视图创建 DecorView,

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
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
private void ensureSubDecor() {
if (!this.mSubDecorInstalled) {
// DecorView 不存在,调用 createSubDecor() 创建 DecorView
this.mSubDecor = this.createSubDecor();
CharSequence title = this.getTitle();
if (!TextUtils.isEmpty(title)) {
if (this.mDecorContentParent != null) {
this.mDecorContentParent.setWindowTitle(title);
} else if (this.peekSupportActionBar() != null) {
this.peekSupportActionBar().setWindowTitle(title);
} else if (this.mTitleView != null) {
this.mTitleView.setText(title);
}
}

this.applyFixedSizeWindow();
this.onSubDecorInstalled(this.mSubDecor);
this.mSubDecorInstalled = true;
AppCompatDelegateImpl.PanelFeatureState st = this.getPanelState(0, false);
if (!this.mIsDestroyed && (st == null || st.menu == null)) {
this.invalidatePanelMenu(108);
}
}
}

private ViewGroup createSubDecor() {
// 获取设置的主题属性
TypedArray a = this.mContext.obtainStyledAttributes(styleable.AppCompatTheme);
// 如果使用的主题不是 Theme.AppCompat,或者没又继承自 Theme.AppCompat,抛出异常。
if (!a.hasValue(styleable.AppCompatTheme_windowActionBar)) {
a.recycle();
throw new IllegalStateException("You need to use a Theme.AppCompat theme (or descendant) with this activity.");
} else {
// 根据主题的属性进行设置
if (a.getBoolean(styleable.AppCompatTheme_windowNoTitle, false)) {
// 在 requestWindowFeature() 方法中
// 设置 this.mWindowNoTitle = true
this.requestWindowFeature(1);
} else if (a.getBoolean(styleable.AppCompatTheme_windowActionBar, false)) {
this.requestWindowFeature(108);
}

if (a.getBoolean(styleable.AppCompatTheme_windowActionBarOverlay, false)) {
this.requestWindowFeature(109);
}

if (a.getBoolean(styleable.AppCompatTheme_windowActionModeOverlay, false)) {
this.requestWindowFeature(10);
}

// 记录是否为浮动的主题
this.mIsFloating = a.getBoolean(styleable.AppCompatTheme_android_windowIsFloating, false);
a.recycle();
this.mWindow.getDecorView();
LayoutInflater inflater = LayoutInflater.from(this.mContext);
ViewGroup subDecor = null;

// 根据不同的设置,给 subDecor 填充内容
if (!this.mWindowNoTitle) {
if (this.mIsFloating) {
// Dialog 的主题
subDecor = (ViewGroup)inflater.inflate(layout.abc_dialog_title_material, (ViewGroup)null);
this.mHasActionBar = this.mOverlayActionBar = false;
} else if (this.mHasActionBar) {
// 添加 ActionBar
TypedValue outValue = new TypedValue();
this.mContext.getTheme().resolveAttribute(attr.actionBarTheme, outValue, true);
Object themedContext;
if (outValue.resourceId != 0) {
themedContext = new ContextThemeWrapper(this.mContext, outValue.resourceId);
} else {
themedContext = this.mContext;
}

subDecor = (ViewGroup)LayoutInflater.from((Context)themedContext).inflate(layout.abc_screen_toolbar, (ViewGroup)null);
this.mDecorContentParent = (DecorContentParent)subDecor.findViewById(id.decor_content_parent);
this.mDecorContentParent.setWindowCallback(this.getWindowCallback());
if (this.mOverlayActionBar) {
this.mDecorContentParent.initFeature(109);
}

if (this.mFeatureProgress) {
this.mDecorContentParent.initFeature(2);
}

if (this.mFeatureIndeterminateProgress) {
this.mDecorContentParent.initFeature(5);
}
}
} else {
if (this.mOverlayActionMode) {
subDecor = (ViewGroup)inflater.inflate(layout.abc_screen_simple_overlay_action_mode, (ViewGroup)null);
} else {
subDecor = (ViewGroup)inflater.inflate(layout.abc_screen_simple, (ViewGroup)null);
}

if (VERSION.SDK_INT >= 21) {
ViewCompat.setOnApplyWindowInsetsListener(subDecor, new OnApplyWindowInsetsListener() {
public WindowInsetsCompat onApplyWindowInsets(View v, WindowInsetsCompat insets) {
int top = insets.getSystemWindowInsetTop();
int newTop = AppCompatDelegateImpl.this.updateStatusGuard(top);
if (top != newTop) {
insets = insets.replaceSystemWindowInsets(insets.getSystemWindowInsetLeft(), newTop, insets.getSystemWindowInsetRight(), insets.getSystemWindowInsetBottom());
}

return ViewCompat.onApplyWindowInsets(v, insets);
}
});
} else {
((FitWindowsViewGroup)subDecor).setOnFitSystemWindowsListener(new OnFitSystemWindowsListener() {
public void onFitSystemWindows(Rect insets) {
insets.top = AppCompatDelegateImpl.this.updateStatusGuard(insets.top);
}
});
}
}

// 把 DecorView 添加到 Window 上 并且返回 DecorView
if (subDecor == null) {
throw new IllegalArgumentException("AppCompat does not support the current theme features: { windowActionBar: " + this.mHasActionBar + ", windowActionBarOverlay: " + this.mOverlayActionBar + ", android:windowIsFloating: " + this.mIsFloating + ", windowActionModeOverlay: " + this.mOverlayActionMode + ", windowNoTitle: " + this.mWindowNoTitle + " }");
} else {
if (this.mDecorContentParent == null) {
this.mTitleView = (TextView)subDecor.findViewById(id.title);
}

ViewUtils.makeOptionalFitsSystemWindows(subDecor);
ContentFrameLayout contentView = (ContentFrameLayout)subDecor.findViewById(id.action_bar_activity_content);
ViewGroup windowContentView = (ViewGroup)this.mWindow.findViewById(16908290);
if (windowContentView != null) {
while(windowContentView.getChildCount() > 0) {
View child = windowContentView.getChildAt(0);
windowContentView.removeViewAt(0);
contentView.addView(child);
}

windowContentView.setId(-1);
contentView.setId(16908290);
if (windowContentView instanceof FrameLayout) {
((FrameLayout)windowContentView).setForeground((Drawable)null);
}
}

// 把 DecorView 添加到 Window 上
this.mWindow.setContentView(subDecor);
contentView.setAttachListener(new OnAttachListener() {
public void onAttachedFromWindow() {
}

public void onDetachedFromWindow() {
AppCompatDelegateImpl.this.dismissPopups();
}
});
return subDecor;
}
}
}

创建好 DecorView 之后,DecorView 会被添加到 Windows(实现类是 PhoneWindow) 中,然后返回 DecorView。
并且 DecorView 是视图的顶级容器,我们可以通过 Android Studio 的 Layout Inspector 来查看一个界面的 View Tree。
View tree

一个 DecorView 包含这一个纵向的 LinearLayout,LinearLayout 内部是一个 FrameLayout,FrameLayout 是一个包含了内容部分和标题部分的容器。

在 AppCompatDelegateImpl 中的 setContentView() 方法中还有一句:

1
ViewGroup contentParent = (ViewGroup)this.mSubDecor.findViewById(16908290);

这句代码得到的 contentParent 就是刚刚创建的 DecorView 中的 内容根部局(id/content (ContentFrameLayout))。

ContentFrameLayout 是 FrameLayout 的子类,我们编写的 xml 布局就是被添加到它的内部。

然后查看为内容根布局添加视图的过程。

# 3. LayoutInflater 的 inflate()

inflate() 的代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public View inflate(@LayoutRes int resource, @Nullable ViewGroup root, boolean attachToRoot) {
final Resources res = getContext().getResources();
if (DEBUG) {
Log.d(TAG, "INFLATING from resource: \"" + res.getResourceName(resource) + "\" ("
+ Integer.toHexString(resource) + ")");
}

// 获取解析当前布局 xml 文件的 parser 对象
final XmlResourceParser parser = res.getLayout(resource);
try {
// 调用 inflate() 方法,开始解析 xml 文件,并返回得到界面
return inflate(parser, root, attachToRoot);
} finally {
parser.close();
}
}

在上述方法中,会先获取解析 xml 文件的 parser 对象,然后调用另一个 infalte() 方法进行解析。

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
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
public View inflate(XmlPullParser parser, @Nullable ViewGroup root, boolean attachToRoot) {
synchronized (mConstructorArgs) {
Trace.traceBegin(Trace.TRACE_TAG_VIEW, "inflate");

final Context inflaterContext = mContext;
final AttributeSet attrs = Xml.asAttributeSet(parser);
Context lastContext = (Context) mConstructorArgs[0];
mConstructorArgs[0] = inflaterContext;
View result = root;

try {
// 通过 while 循环遍历 xml 中的节点,直到找到 root
int type;
while ((type = parser.next()) != XmlPullParser.START_TAG &&
type != XmlPullParser.END_DOCUMENT) {
// Empty
}

if (type != XmlPullParser.START_TAG) {
throw new InflateException(parser.getPositionDescription()
+ ": No start tag found!");
}

final String name = parser.getName();

// 如果是 merge 节点,执行 rInflate() 方法,按照层次递归的去实例化 xml 文件的子项
if (TAG_MERGE.equals(name)) {
if (root == null || !attachToRoot) {
throw new InflateException("<merge /> can be used only with a valid "
+ "ViewGroup root and attachToRoot=true");
}

rInflate(parser, root, inflaterContext, attrs, false);
} else {
// Temp is the root view that was found in the xml
// 不是 merge 节点,就通过 tag 标签创建一个 view
final View temp = createViewFromTag(root, name, inflaterContext, attrs);

ViewGroup.LayoutParams params = null;

if (root != null) {
// Create layout params that match root, if supplied
params = root.generateLayoutParams(attrs);
if (!attachToRoot) {
// Set the layout params for temp if we are not
// attaching. (If we are, we use addView, below)
temp.setLayoutParams(params);
}
}

// Inflate all children under temp against its context.
rInflateChildren(parser, temp, attrs, true);

// We are supposed to attach all the views we found (int temp)
// to root. Do that now.
// 将创建的 View 添加到 root 视图中
if (root != null && attachToRoot) {
root.addView(temp, params);
}

// Decide whether to return the root that was passed in or the
// top view found in xml.
if (root == null || !attachToRoot) {
result = temp;
}
}

} catch (XmlPullParserException e) {
final InflateException ie = new InflateException(e.getMessage(), e);
ie.setStackTrace(EMPTY_STACK_TRACE);
throw ie;
} catch (Exception e) {
final InflateException ie = new InflateException(parser.getPositionDescription()
+ ": " + e.getMessage(), e);
ie.setStackTrace(EMPTY_STACK_TRACE);
throw ie;
} finally {
// Don't retain static reference on context.
mConstructorArgs[0] = lastContext;
mConstructorArgs[1] = null;

Trace.traceEnd(Trace.TRACE_TAG_VIEW);
}

return result;
}
}

在这个 inflate() 方法中,会先寻找 xml 文件中的起始节点,如果起始节点是 merge,就执行 rInflate() 方法,如果不是 merge,就执行 createViewFromTag() 方法去创建一个新的 View,最后把 View 添加到内容根部局中。

# 4. createViewFromTag()

现在看 createViewFromTag() 的源码:

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
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
View createViewFromTag(View parent, String name, Context context, AttributeSet attrs,
boolean ignoreThemeAttr) {
if (name.equals("view")) {
name = attrs.getAttributeValue(null, "class");
}

// Apply a theme wrapper, if allowed and one is specified.
if (!ignoreThemeAttr) {
final TypedArray ta = context.obtainStyledAttributes(attrs, ATTRS_THEME);
final int themeResId = ta.getResourceId(0, 0);
if (themeResId != 0) {
context = new ContextThemeWrapper(context, themeResId);
}
ta.recycle();
}

// 如果 name 的值为 blink,返回一个 BlinkLayout
if (name.equals(TAG_1995)) {
// Let's party like it's 1995!
return new BlinkLayout(context, attrs);
}

try {
View view;
// 依次寻找合适的 Factory 对象去创建 View
if (mFactory2 != null) {
view = mFactory2.onCreateView(parent, name, context, attrs);
} else if (mFactory != null) {
view = mFactory.onCreateView(name, context, attrs);
} else {
view = null;
}

if (view == null && mPrivateFactory != null) {
view = mPrivateFactory.onCreateView(parent, name, context, attrs);
}

if (view == null) {
final Object lastContext = mConstructorArgs[0];
mConstructorArgs[0] = context;
try {
if (-1 == name.indexOf('.')) {
view = onCreateView(parent, name, attrs);
} else {
view = createView(name, null, attrs);
}
} finally {
mConstructorArgs[0] = lastContext;
}
}

return view;
} catch (InflateException e) {
throw e;

} catch (ClassNotFoundException e) {
final InflateException ie = new InflateException(attrs.getPositionDescription()
+ ": Error inflating class " + name, e);
ie.setStackTrace(EMPTY_STACK_TRACE);
throw ie;

} catch (Exception e) {
final InflateException ie = new InflateException(attrs.getPositionDescription()
+ ": Error inflating class " + name, e);
ie.setStackTrace(EMPTY_STACK_TRACE);
throw ie;
}
}

我们看一看代码中的 mFactory2mFactorymPrivateFactory 是什么。

在 LayoutInflater.java 的属性中,有如下几个变量:

1
2
3
private Factory mFactory;
private Factory2 mFactory2;
private Factory2 mPrivateFactory;

Factory 是一个接口,Factory2 是继承了 Factory 的接口,它们都有个一个 onCreateView() 的方法。

# 5. onCreateView()

我们去看它们在 AppCompatDelegateImpl 中的实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
public View createView(View parent, String name, @NonNull Context context, @NonNull AttributeSet attrs) {
if (this.mAppCompatViewInflater == null) {
TypedArray a = this.mContext.obtainStyledAttributes(styleable.AppCompatTheme);
String viewInflaterClassName = a.getString(styleable.AppCompatTheme_viewInflaterClass);
if (viewInflaterClassName != null && !AppCompatViewInflater.class.getName().equals(viewInflaterClassName)) {
try {
Class viewInflaterClass = Class.forName(viewInflaterClassName);
this.mAppCompatViewInflater = (AppCompatViewInflater)viewInflaterClass.getDeclaredConstructor().newInstance();
} catch (Throwable var8) {
Log.i("AppCompatDelegate", "Failed to instantiate custom view inflater " + viewInflaterClassName + ". Falling back to default.", var8);
this.mAppCompatViewInflater = new AppCompatViewInflater();
}
} else {
this.mAppCompatViewInflater = new AppCompatViewInflater();
}
}

boolean inheritContext = false;
if (IS_PRE_LOLLIPOP) {
inheritContext = attrs instanceof XmlPullParser ? ((XmlPullParser)attrs).getDepth() > 1 : this.shouldInheritContext((ViewParent)parent);
}

return this.mAppCompatViewInflater.createView(parent, name, context, attrs, inheritContext, IS_PRE_LOLLIPOP, true, VectorEnabledTintResources.shouldBeUsed());
}

方法的最后可以看出创建视图的工作交给了 AppCompatViewInflater 的 createView() 去完成

# 6. createView()

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
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
final View createView(View parent, String name, @NonNull Context context, @NonNull AttributeSet attrs, boolean inheritContext, boolean readAndroidTheme, boolean readAppTheme, boolean wrapContext) {
Context originalContext = context;
if (inheritContext && parent != null) {
context = parent.getContext();
}

if (readAndroidTheme || readAppTheme) {
context = themifyContext(context, attrs, readAndroidTheme, readAppTheme);
}

if (wrapContext) {
context = TintContextWrapper.wrap(context);
}

View view = null;
byte var12 = -1;
switch(name.hashCode()) {
case -1946472170:
if (name.equals("RatingBar")) {
var12 = 11;
}
break;
case -1455429095:
if (name.equals("CheckedTextView")) {
var12 = 8;
}
break;
case -1346021293:
if (name.equals("MultiAutoCompleteTextView")) {
var12 = 10;
}
break;
case -938935918:
if (name.equals("TextView")) {
var12 = 0;
}
break;
case -937446323:
if (name.equals("ImageButton")) {
var12 = 5;
}
break;
case -658531749:
if (name.equals("SeekBar")) {
var12 = 12;
}
break;
case -339785223:
if (name.equals("Spinner")) {
var12 = 4;
}
break;
case 776382189:
if (name.equals("RadioButton")) {
var12 = 7;
}
break;
case 1125864064:
if (name.equals("ImageView")) {
var12 = 1;
}
break;
case 1413872058:
if (name.equals("AutoCompleteTextView")) {
var12 = 9;
}
break;
case 1601505219:
if (name.equals("CheckBox")) {
var12 = 6;
}
break;
case 1666676343:
if (name.equals("EditText")) {
var12 = 3;
}
break;
case 2001146706:
if (name.equals("Button")) {
var12 = 2;
}
}

switch(var12) {
case 0:
view = this.createTextView(context, attrs);
this.verifyNotNull((View)view, name);
break;
case 1:
view = this.createImageView(context, attrs);
this.verifyNotNull((View)view, name);
break;
case 2:
view = this.createButton(context, attrs);
this.verifyNotNull((View)view, name);
break;
case 3:
view = this.createEditText(context, attrs);
this.verifyNotNull((View)view, name);
break;
case 4:
view = this.createSpinner(context, attrs);
this.verifyNotNull((View)view, name);
break;
case 5:
view = this.createImageButton(context, attrs);
this.verifyNotNull((View)view, name);
break;
case 6:
view = this.createCheckBox(context, attrs);
this.verifyNotNull((View)view, name);
break;
case 7:
view = this.createRadioButton(context, attrs);
this.verifyNotNull((View)view, name);
break;
case 8:
view = this.createCheckedTextView(context, attrs);
this.verifyNotNull((View)view, name);
break;
case 9:
view = this.createAutoCompleteTextView(context, attrs);
this.verifyNotNull((View)view, name);
break;
case 10:
view = this.createMultiAutoCompleteTextView(context, attrs);
this.verifyNotNull((View)view, name);
break;
case 11:
view = this.createRatingBar(context, attrs);
this.verifyNotNull((View)view, name);
break;
case 12:
view = this.createSeekBar(context, attrs);
this.verifyNotNull((View)view, name);
break;
default:
view = this.createView(context, name, attrs);
}

if (view == null && originalContext != context) {
view = this.createViewFromTag(context, name, attrs);
}

if (view != null) {
this.checkOnClickListener((View)view, attrs);
}

return (View)view;
}

通过 createTextView() 等源码可以发现,creatView() 方法把常用的组件都变成了 AppCompat 的类型,从而达到了兼容的目的。

至此,setContentView() 的流程就走完了。但是添加好布局文件之后,视图并不会显示到界面上,还需要通过 WindowsManagerService 去渲染界面才能使界面显示。这部分到内容在后面会讲到。

零碎的东西很多,为了方便大家记忆,我把上面的内容做成了思维导图,需要的朋友可以保存下来,偶尔看一下,帮助自己记忆。

未命名.png

欢迎关注本文作者:

setContentView() 的流程

扫码关注并回复「干货」,获取我整理的千G Android、iOS、JavaWeb、大数据、人工智能等学习资源。