Android LayoutInflater源码分析

概述

在初学Android的时候LayoutInflater是经常用到的一个类,例如在ListView的getView方法,LayoutInflater的作用主要是用来加载布局。上篇在介绍到Activity通过调用setContentView加载布局,通过阅读源码发现其内部还是调用LayoutInflater的inflate方法,继续阅读源码看看内部具体的实现原理。

Demo

MainActivity.java

1
2
3
4
5
6
7
8
9
10
11
12
public class MainActivity extends AppCompatActivity {
private ListView mListView;
private MyAdapter mMyAdapter;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
mListView = (ListView) findViewById(R.id.listview);
mMyAdapter = new MyAdapter(this);
mListView.setAdapter(mMyAdapter);
}
}

MyAdapter.java

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
public class MyAdapter extends BaseAdapter {
private LayoutInflater mLayoutInflater;
public MyAdapter(Context context){
mLayoutInflater = LayoutInflater.from(context);
}
@Override
public int getCount() {
return 8;
}
@Override
public Object getItem(int position) {
return null;
}
@Override
public long getItemId(int position) {
return 0;
}
@Override
public View getView(int position, View convertView, ViewGroup parent) {
View v0 = mLayoutInflater.inflate(R.layout.item_listview, null);
View v1 = mLayoutInflater.inflate(R.layout.item_listview, null, false);
View v2 = mLayoutInflater.inflate(R.layout.item_listview, null, true);
//View v3 = mLayoutInflater.inflate(R.layout.item_listview, parent);
View v3 = mLayoutInflater.inflate(R.layout.item_listview, parent, false);
//View v3 = mLayoutInflater.inflate(R.layout.item_listview, parent, true);
View v4 = mLayoutInflater.inflate(R.layout.item_listview_parent, null);
View v5 = mLayoutInflater.inflate(R.layout.item_listview_parent, null, false);
View v6 = mLayoutInflater.inflate(R.layout.item_listview_parent, null, true);
//View v7 = mLayoutInflater.inflate(R.layout.item_listview_parent, parent);
View v7 = mLayoutInflater.inflate(R.layout.item_listview_parent, parent, false);
//View v7 = mLayoutInflater.inflate(R.layout.item_listview_parent, parent, true);
View[] viewLists = {v0, v1, v2, v3, v4, v5, v6, v7};
convertView = viewLists[position];
ViewGroup.LayoutParams layoutParams = convertView.getLayoutParams();
if (layoutParams == null) {
Log.d("hcy", "position " + position + " layoutParams is null" );
} else {
Log.d("hcy", "position " + position + " layoutParams.width is: " + layoutParams.width + " layoutParams.height is: " + layoutParams.height);
}
return convertView;
}
}

activity_main.xml

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<?xml version="1.0" encoding="utf-8"?>
<android.support.constraint.ConstraintLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context="com.hcy.layoutinflaterdemo.MainActivity">
<ListView
android:id="@+id/listview"
android:layout_width="match_parent"
android:layout_height="match_parent"
/>
</android.support.constraint.ConstraintLayout>

item_listview.xml

1
2
3
4
5
6
7
<?xml version="1.0" encoding="utf-8"?>
<TextView android:id="@+id/list_item"
android:layout_width="100dp"
android:layout_height="50dp"
android:text="Hello World"
android:background="@android:color/holo_purple"
xmlns:android="http://schemas.android.com/apk/res/android" />

item_listview_parent.xml

1
2
3
4
5
6
7
8
9
10
11
12
13
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:background="@android:color/holo_purple">
<TextView
android:id="@+id/text"
android:layout_height="50dp"
android:layout_width="100dp"
android:text="Hello World"
android:background="@android:color/holo_orange_dark"/>
</LinearLayout>

该Demo比较简单,写这个Demo的主要目的是要搞清楚inflate方法的参数设置给布局带来的影响,这也是自己一直没记清楚的地方。

运行结果

问题

显示的结果很奇怪,疑问如下:

  • position0-2item我明明指定的width和height分别是100dp和50dp,为什么width显示成match_parent,高度也不对?而且第三个参数有没有好像不影响显示
  • position4-6item的LinearLayout的width和height分别是wrap_content,为什么宽显示成match_parent
  • 在调试的时候若把MyAdapter中注释的几行打开会报错
    1
    Caused by: java.lang.UnsupportedOperationException: addView(View, LayoutParams) is not supported in AdapterView

Why?

LayoutInflater源码分析

首先看下获取LayoutInflater实例的地方,在MyAdapter中通过
LayoutInflater.from(context)获取,那这里的from方法具体又干了什么?

1
2
3
4
5
6
7
8
9
10
11
12
源码路径:sdk/sources/android-25/android/view/LayoutInflater.java
/**
* Obtains the LayoutInflater from the given context.
*/
public static LayoutInflater from(Context context) {
LayoutInflater LayoutInflater =
(LayoutInflater) context.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
if (LayoutInflater == null) {
throw new AssertionError("LayoutInflater not found.");
}
return LayoutInflater;
}

从这里的源码看到获取LayoutInflater实例的另外一种写法

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

获取实例之后调用inflate方法,传入xml布局让它帮忙加载,inflate方法如下

1
2
3
public View inflate(@LayoutRes int resource, @Nullable ViewGroup root) {
return inflate(resource, root, root != null);
}

发现该方法会调用包含3个参数的方法,第三个参数的值取决于第二参数root是否为null

1
2
3
4
5
6
7
8
9
10
11
12
13
14
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) + ")");
}
final XmlResourceParser parser = res.getLayout(resource);
try {
return inflate(parser, root, attachToRoot);
} finally {
parser.close();
}
}

通过Resources获取一个XmlResourceParser(LayoutInflater是使用pull解析方式来解析xml),接着又把第一个参数由int改成XmlResourceParser,继续调用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
public View inflate(XmlPullParser parser, @Nullable ViewGroup root, boolean attachToRoot) {
synchronized (mConstructorArgs) {
final Context inflaterContext = mContext;
final AttributeSet attrs = Xml.asAttributeSet(parser);
Context lastContext = (Context) mConstructorArgs[0];
mConstructorArgs[0] = inflaterContext;
//该方法的返回值,初始化为我们传入的第二个参数root
View result = root;
try {
// Look for the root node.
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();
if (TAG_MERGE.equals(name)) {
//如果标签是merge则root不能为空且attachToRoot必须为true,否则会抛异常(merge标签用于xml性能优化)
if (root == null || !attachToRoot) {
throw new InflateException("<merge /> can be used only with a valid "
+ "ViewGroup root and attachToRoot=true");
}
//遍历加载子元素view
rInflate(parser, root, inflaterContext, attrs, false);
} else {
// Temp is the root view that was found in the xml
//根据tag名称生成xml中的root View对象
final View temp = createViewFromTag(root, name, inflaterContext, attrs);
ViewGroup.LayoutParams params = null;
if (root != null) {
// Create layout params that match root, if supplied
//生成root的LayoutParams
params = root.generateLayoutParams(attrs);
if (!attachToRoot) {
// Set the layout params for temp if we are not
// attaching. (If we are, we use addView, below)
//第三个参数attachToRoot为false时执行View的setLayoutParams
temp.setLayoutParams(params);
}
}
// Inflate all children under temp against its context.
//遍历解析temp下的子View
rInflateChildren(parser, temp, attrs, true);
// We are supposed to attach all the views we found (int temp)
// to root. Do that now.
if (root != null && attachToRoot) {
//第二个参数root非空且第三个参数attachToRoot为true则把xml解析完成的view添加到root中
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) {
//第二个参数root为空或第三个参数attachToRoot为false时返回xml中解析的root view
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;
}
return result;
}
}

删掉一些不影响阅读的DEBUG日志后开始分析该方法,在37行传入节点名称通过createViewFromTag方法生成根布局的实例,createViewFromTag()方法内部又会去调用createView()方法,然后通过反射的方式创建出View的实例并返回。根布局解析完成后,55行调用rInflateChildren方法解析根布局下的子View

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
final void rInflateChildren(XmlPullParser parser, View parent, AttributeSet attrs,
boolean finishInflate) throws XmlPullParserException, IOException {
rInflate(parser, parent, parent.getContext(), attrs, finishInflate);
}
void rInflate(XmlPullParser parser, View parent, Context context,
AttributeSet attrs, boolean finishInflate) throws XmlPullParserException, IOException {
final int depth = parser.getDepth();
int type;
while (((type = parser.next()) != XmlPullParser.END_TAG ||
parser.getDepth() > depth) && type != XmlPullParser.END_DOCUMENT) {
if (type != XmlPullParser.START_TAG) {
continue;
}
final String name = parser.getName();
if (TAG_REQUEST_FOCUS.equals(name)) {
parseRequestFocus(parser, parent);
} else if (TAG_TAG.equals(name)) {
parseViewTag(parser, parent, attrs);
} else if (TAG_INCLUDE.equals(name)) {
if (parser.getDepth() == 0) {
throw new InflateException("<include /> cannot be the root element");
}
parseInclude(parser, context, parent, attrs);
} else if (TAG_MERGE.equals(name)) {
throw new InflateException("<merge /> must be the root element");
} else {
final View view = createViewFromTag(parent, name, context, attrs);
final ViewGroup viewGroup = (ViewGroup) parent;
final ViewGroup.LayoutParams params = viewGroup.generateLayoutParams(attrs);
rInflateChildren(parser, view, attrs, true);
viewGroup.addView(view, params);
}
}
if (finishInflate) {
parent.onFinishInflate();
}
}

根据不同的tag做不同的解析,如果不是requestFocus 、tag 、include、merge就调用 createViewFromTag()方法,然后循环rInflateChildren(),直到解析出所有View,最后会把最顶层的根布局返回,至此inflate()完成。

问题分析

源码也看完了,现在回头来看下我们的例子显示的原因。

分析1:position0-2item的布局是一个TextView,inflate方法的第二个参数为null,根据上面的分析,可以简化成下面的代码流程

1
2
3
4
5
6
final View temp = createViewFromTag(root, name, inflaterContext, attrs);
if (root == null || !attachToRoot) {
result = temp;
}
return result;

这里temp的LayoutParams并没有实例化,所以为null,因此不管设置宽高为多少都不能正常显示,那为什么它默认显示宽为match_parent呢?
ListView extends AbsListView,AbsListView extends AdapterView,在AbsListView 中有这几个方法

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
/**
* Get a view and have it show the data associated with the specified
* position. This is called when we have already discovered that the view is
* not available for reuse in the recycle bin. The only choices left are
* converting an old view or making a new one.
*
* @param position The position to display
* @param isScrap Array of at least 1 boolean, the first entry will become true if
* the returned view was taken from the "temporary detached" scrap heap, false if
* otherwise.
*
* @return A view displaying the data associated with the specified position
*/
View obtainView(int position, boolean[] isScrap) {
......
setItemViewLayoutParams(child, position);
......
return child;
}
private void setItemViewLayoutParams(View child, int position) {
final ViewGroup.LayoutParams vlp = child.getLayoutParams();
LayoutParams lp;
if (vlp == null) {
lp = (LayoutParams) generateDefaultLayoutParams();
} else if (!checkLayoutParams(vlp)) {
lp = (LayoutParams) generateLayoutParams(vlp);
} else {
lp = (LayoutParams) vlp;
}
if (mAdapterHasStableIds) {
lp.itemId = mAdapter.getItemId(position);
}
lp.viewType = mAdapter.getItemViewType(position);
lp.isEnabled = mAdapter.isEnabled(position);
if (lp != vlp) {
child.setLayoutParams(lp);
}
}
protected ViewGroup.LayoutParams generateDefaultLayoutParams() {
return new AbsListView.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT,
ViewGroup.LayoutParams.WRAP_CONTENT, 0);
}

通过这里的代码发现如果Item的layoutParams属性为null,默认为其设置一个宽度为match_parent,高度为wrap_content的layoutParams属性,因此position0-2item显示成那样就说得通了。

分析2:position3item布局是一个TextView,第二个参数root不为null,第三个参数为false,可以简化成下面的代码流程

1
2
3
4
5
6
7
8
9
10
11
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);
}
}

这里的root就是ListView,在它的父类AbsListView中找到generateLayoutParams方法

1
2
3
protected ViewGroup.LayoutParams generateLayoutParams(ViewGroup.LayoutParams p) {
return new LayoutParams(p);
}

其实就是对LayoutParams初始化,后面temp再根据这里的LayoutParams加载布局,因此能够正常显示。

分析3:position4-6item由LinearLayout包裹TextView组成,同样第二个参数root为null,与上面0-2item一样,LayoutParams没有实例化,所以最外层的layout_width、layout_height属性失效,但是它内部的TextView的属性是有效的,从图显示也可以看出TextView大小符合我们设置的大小。
分析4:position7item由LinearLayout包裹TextView组成,第二个参数root不为null,第三个参数为false,与上面分析2相同,设置了LayoutParams,宽高取决于TextView的layout_width、layout_height大小
分析5:打开注释会报错分析,这种情况是第二个参数不为null,第三个参数为true,可以简化成下面的流程

1
2
3
4
5
final View temp = createViewFromTag(root, name, inflaterContext, attrs);
ViewGroup.LayoutParams params = root.generateLayoutParams(attrs);
if (root != null && attachToRoot) {
root.addView(temp, params);
}

这里的root就是ListView,在它里面没有找到addView方法,再向上找,在AdapterView中的addView方法

1
2
3
4
public void addView(View child, LayoutParams params) {
throw new UnsupportedOperationException("addView(View, LayoutParams) "
+ "is not supported in AdapterView");
}

所以会报错

日志

程序中的日志如下

1
2
3
4
5
6
7
8
05-17 00:15:55.504 18458 18458 D hcy : position 0 layoutParams is null
05-17 00:15:55.516 18458 18458 D hcy : position 1 layoutParams is null
05-17 00:15:55.522 18458 18458 D hcy : position 2 layoutParams is null
05-17 00:15:55.525 18458 18458 D hcy : position 3 layoutParams.width is: 263 layoutParams.height is: 131
05-17 00:15:55.534 18458 18458 D hcy : position 4 layoutParams is null
05-17 00:15:55.538 18458 18458 D hcy : position 5 layoutParams is null
05-17 00:15:55.542 18458 18458 D hcy : position 6 layoutParams is null
05-17 00:15:55.544 18458 18458 D hcy : position 7 layoutParams.width is: -2 layoutParams.height is: -2

这也印证了当第二个参数为null时,layoutParams为null,其次item3的宽高根据不同设备显示值也有所不同,最后item7显示-2是几个意思?在ViewGroup中定义

1
public static final int WRAP_CONTENT = -2;

随便提一下

在Activity中指定布局文件的时候,最外层的那个布局是可以指定大小的,layout_width和layout_height都是有作用的,根据上一篇的分析知道Android会自动在布局文件的最外层再嵌套一个FrameLayout,代码其实是这样的

1
mLayoutInflater.inflate(layoutResID, mContentParent);

这里的mContentParent是FrameLayout不为null,又回到上面的分析,那它为啥不报错呢?
在FrameLayout中没找到addView方法,继续向上查找,在它爹ViewGroup中

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public void addView(View child) {
addView(child, -1);
}
public void addView(View child, int index) {
if (child == null) {
throw new IllegalArgumentException("Cannot add a null child view to a ViewGroup");
}
LayoutParams params = child.getLayoutParams();
if (params == null) {
params = generateDefaultLayoutParams();
if (params == null) {
throw new IllegalArgumentException("generateDefaultLayoutParams() cannot return null");
}
}
addView(child, index, params);
}

所以不会报错。

总结

  • 如果root为null,attachToRoot将失去作用,设置任何值都没有意义,加载的布局文件最外层的所有layout属性会失效,由父布局来默认指定.
  • 如果root不为null,未设置attachToRoot,attachToRoot默认为true
  • 如果root不为null,attachToRoot为true,会调用root的addView方法并返回root;attachToRoot为false,则根据传入的root为根标签temp设置LayoutParams并返回temp;
听说打赏的人运气都不会太差