Android----LayoutInflater 生成 View 原理

一.前言

在 Android 中,通常需要指定一个布局的时候,通常都是在 xml 文件中通过不同的标签指定我们需要的控件,但是在 Activity 中是可以直接通过 Java 对象获取到我们需要的 View 对象的,而从一个 xml 文件转换为一个 View 对象或者 ViewGroup 对象的这个过程,就是通过 LayoutInflater 完成的。

二.原理

1.setContentView

在一个 Activity 中,指定一个 Activity 的布局就是直接通过 setContentView 方法来完成的。

1
2
3
4
public void setContentView(View view) {
getWindow().setContentView(view);
initWindowDecorActionBar();
}

可知 Activity 的 setContentView 实际上又是调用 window 的 setContentView ,而 window 的实现类就是 PhoneWindow 。所以在 Phone 中可以看到下面的实现。

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

// This is the view in which the window contents are placed. It is either
// mDecor itself, or a child of mDecor where the contents go.
ViewGroup mContentParent;


@Override
public void setContentView(int layoutResID) {

if (mContentParent == null) {
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
mLayoutInflater.inflate(layoutResID, mContentParent);
}
mContentParent.requestApplyInsets();
final Callback cb = getCallback();
if (cb != null && !isDestroyed()) {
cb.onContentChanged();
}
mContentParentExplicitlySet = true;
}

从 PhoneWindow 中可以看到 setContentView 最后还是调用了 mLayoutInflater.inflate 方法,而 mContentParent 从注释中就可以这出这个要么是 DecorView 也就是 FrameLayout,要么就是 FrameLayout 的一个子布局。下面就看 LayoutInflater 的 inflate 方法。

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
public View inflate(@LayoutRes int resource, @Nullable ViewGroup root) {
return inflate(resource, root, root != null);
}

//调用下面的方法


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 的解析器
final XmlResourceParser parser = res.getLayout(resource);
try {
return inflate(parser, root, attachToRoot);
} finally {
parser.close();
}
}

// 再次调用下面的

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 {
// Look for the root node.
//首先寻找根节点
int type;
while ((type = parser.next()) != XmlPullParser.START_TAG &&
type != XmlPullParser.END_DOCUMENT) {
// Empty
}
//如果根节点的 标签和 start 和 end 不一致就抛出异常
if (type != XmlPullParser.START_TAG) {
throw new InflateException(parser.getPositionDescription()
+ ": No start tag found!");
}

//
final String name = parser.getName();

if (DEBUG) {
System.out.println("**************************");
System.out.println("Creating root view: "
+ name);
System.out.println("**************************");
}

//如果是 Merge 标签
if (TAG_MERGE.equals(name)) {
//但是 root 为 null 或者每一个 依附于一个 root
//就需要抛出异常,因为 Merge 不能单独存在
if (root == null || !attachToRoot) {
throw new InflateException("<merge /> can be used only with a valid "
+ "ViewGroup root and attachToRoot=true");
}
//对 Merge 标签去递归解析下面的 标签

rInflate(parser, root, inflaterContext, attrs, false);
} else {

//如果是普通的标签
//就通过 createViewFromTag 反射出来一个 View
// Temp is the root view that was found in the xml
final View temp = createViewFromTag(root, name, inflaterContext, attrs);

ViewGroup.LayoutParams params = null;

if (root != null) {
if (DEBUG) {
System.out.println("Creating params from root: " +
root);
}
// 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);
}
}

//然后再去递归 解析 子布局
if (DEBUG) {
System.out.println("-----> start inflating children");
}

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

if (DEBUG) {
System.out.println("-----> done inflating children");
}

// We are supposed to attach all the views we found (int temp)
// to root. Do that now.
//如果 root 不为 空并且 attachToRoot 为 true
//就把生成的 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.
//如果没有就 直接返回生成的 View
if (root == null || !attachToRoot) {
result = temp;
}
}

....
return result;
}
}

对于 setContentView 来说,因为指定了 root 为 FrameLayout ,所以上述过程实际上就是 将生成的View 添加到 FrameLayout。

2.LayoutInflate

在 RecyclerView 中添加一个 item 的时候经常可以见到类似下面这段代码。

1
LayoutInflater.from(mContext).inflate(R.layout.item_app, parent, false)

这实际上就是 Android 中获取 LayoutInflater 的一种方式,最后会调用到

1
2
LayoutInflater layoutInflater = (LayoutInflater) context
.getSystemService(Context.LAYOUT_INFLATER_SERVICE);

可见 layoutInflater 也是一个系统的服务。inflate 方法接收三个参数:

  • int resource:布局文件 xml 的资源 id
  • ViewGroup root:如果 attchToRoot 为 true 的话,root 作为父布局
  • boolean attachToRoot:是否加载到 root 布局中
    对于这个后面两个的作用就是:
  • 如果 root 为 null,就直接将生成的 View 返回
  1. 如果 root 不为 null,attachToRoot 为 true,则会给加载的布局文件的指定一个父布局,即 root,并将生成的 View 添加到 Root 中,但是当该 view 被添加到父 view 当中时, 布局文件中的最外层的 View 的 layout 属性就没作用。

  2. 如果 root 不为 null,attachToRoot 为 false,则当该 view 被添加到父 view 当中时,布局文件最外层的所有 layout 属性生效。

  3. 在不设置 attachToRoot 参数的情况下,attachToRoot = root != null;

以 RecyclerView 中添加一个 item 为例,最后的 attachToRoot 为 false 时

1
2
3
4
5
6
7
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="300dp"
android:layout_height="60dp"
android:orientation="vertical">

</LinearLayout>

layout_width 和 layout_height 都没有作用,这也就是说 layout_xx 属性的生效前提是 他们有一个 父布局,对于 setContentView 那肯定就是 FrameLayout ,因此如果要让这两个属性生效的话,attachToRoot 就应该置为 false ,这也就是为什么在 给 RecyclerView 添加 item 的时候需要设置为 false ,因为一个 item 并不需要填满整个 View .

三.总结

LayoutInflater 生成 View 树

  • 1.调用 XML 解析器将 xml 资源解析成 XmlResourceParser 对象作为参数传递给 inflate(parser, root, attachToRoot); 方法
  • 2.遍历查找当前 xml 资源文件的根节点
    如果没有就抛出异常,解析失败
  • 3.用 createViewFromTag 方法,该方法根据当前根节点的名称和属性名创建一个 root View。
  • 4.调用 rInflate 方法,遍历 xml 资源根布局 root view 下的子元素,并且将子元素view依次添加到 root view下面
  • 5.根据当前view的节点名称和属性调用 createViewFromTag 方法创建子元素 view,然后递归调用 rInflate 方法,遍历子元素 View 的所有子元素,依次类推。最终生成 View 树.

inflate 三个参数的作用:

    1. 如果 root 为 null,就直接将生成的 View 返回
    1. 如果 root 不为 null,attachToRoot 为 true,则会给加载的布局文件的指定一个父布局,即 root,并将生成的 View 添加到 Root 中,但是当该 view 被添加到父 view 当中时, 布局文件中的最外层的 View 的 layout 属性就没作用。
    1. 如果 root 不为 null,attachToRoot 为 false,则当该 view 被添加到父 view 当中时,布局文件最外层的所有 layout 属性生效。
    1. 在不设置 attachToRoot 参数的情况下,attachToRoot = root != null;
0%