初探Android中LayoutInflater原理

接触了Android的人也肯定不会对LayoutInflater陌生,至少在ListView等等这些常见控件中我们也经常会使用这个类来进行我们的item布局的解析,那么今天我们就来把LayoutInflater的工作流程仔细地分析一遍,争取达到知其然知其所以然的境界。本文分析的源代码均来自Android API 24。同时代码分析在上半部分,下半部分将用demo来进行验证。

我们在日常开发写代码时经常通过下面两种方式来获取LayoutInflater:

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

其中第一种方式中我们跟进from方法去看看就会发现,其实第一种方法无非就是对第二种方法进行封装(代码如下):
初探Android中LayoutInflater原理 - 阿里云

通过代码可以看出其实就是通过context.getSystemService方法来获取,然后进行了安全判断而已。
接下来我们看看inflate()方法来看看:

首先inflate()方法有四个:这里先整理出来:
初探Android中LayoutInflater原理 - 阿里云

初探Android中LayoutInflater原理 - 阿里云

初探Android中LayoutInflater原理 - 阿里云
图一调用的inflate方法只需要传入两个参数,第三个参数的值是通过判断root是否为空来设置。

图二中第一个是XmlPullParser对象,第二个参数是Root,第三个参数和图一同理。
图三我们可以看到,将传入的int类型resource转化为Resources类型,然后将它解析来获取XmlResourceParser对象,再最终调用inflate(parser,root,attachToRoot)方法。
第四个方法(其实已经发现前三个方法最终还是调用了第四个方法)代码过长直接将代码贴出在下方具体分析:

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
}
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("**************************");
}
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
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.
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;
}
}

上面是完整版本的源代码,接下来为了便于讲解,我把最关键的代码截图标记来进行说明:
初探Android中LayoutInflater原理 - 阿里云

如上图,首先将我们自定义的XML布局的根布局转化为View类型的temp,然后定义一个params。
接下来判断root是否为空,如果不为空,将自定义布局的参数解析出来赋值给params。再判断我们传入的attchToRoot是否为true,如果不是,意味着我们不把我们的自定义布局加入到某个父View中,因此将params设置给temp,将temp在xml设置的属性进行生效。
而rINflateChildren()方法是内部通过递归的方法用来不断解析我们自定义布局中的子View的,这不再做展开。
接下来在图中标注3处判断root是否为空,attachToRoot是否为true,如果是,那么将我们的temp添加到父布局中。上述代码都是在root不为空的场景下设置的,如果root为空呢?那么我们看看图中标注4处:如果root为空,或者attachToRoot为false,那么将temp直接赋值为result并返回。(result可以在上面的完整源码中看到创建时就是root).

总的解析布局的代码就分析完毕了,接下来我们来总结一下几个结论:
如果root为null,attachToRoot将没有作用,inflate()方法会直接返回temp(也就是自定义布局的View,直接执行上图4处代码);
如果root不为null,attachToRoot设为true,则会给自定义布局文件添加一个父布局,即root。方法返回root对象。(执行2,4处)。
如果root不为null,attachToRoot设为false,则会将布局文件最外层的所有layout属性进行设置,当该view被添加到父view当中时,这些layout属性会自动生效。返回自定义布局View。(执行2,3处)
不传入attachToRoot参数,如果root不为null,attachToRoot参数为true(参考上文几个方法参数说明图)。

(ps:个人理解:总之就是返回我们布局的根布局,如果我们没有传入root,那么明显我们自定义布局根布局就是最顶级,返回它。如果我们传入了root,但是attchToRoot为false,说明我们不想把自定义布局加入到root中,所以我们想要的布局的根布局也是自定义布局的根布局。而我们传入root,attchToRoot为true,说明我们要把自定义布局加入到root中,所以root成了最顶级布局,所以返回root。)
至此代码和结论就都已经分析完毕了,按照常理我们就该撒花结束,但是呢?说了半天,没啥直接的效果看看啊!这干说半天没意思是吧?因此现在我们来写个Demo实际看一下效果:

首先我们定义一个布局(名字叫buttonLayout):
初探Android中LayoutInflater原理 - 阿里云

可以看到就是一个简单的Button,Button就是根布局。
我们接下来在Activity中解析一下:

@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
//mainLayout就是MainActivity的布局,就是一个空的LinearLayout
LinearLayout mainLayout = (LinearLayout) LayoutInflater.from(this).inflate(R.layout.activity_main3, null);
setContentView(mainLayout);
//获取layoutInflater
LayoutInflater layoutInflater = LayoutInflater.from(this);
//解析出buttonLayout
View buttonLayout = layoutInflater.inflate(R.layout.inlate_button, null);
//将buttonLayout添加到mainLayout中
mainLayout.addView(buttonLayout);
Log.d("result", "inflate返回的view为:" + buttonLayout.toString());
}

来看看实际效果:
初探Android中LayoutInflater原理 - 阿里云

 首先当root为空时,会把自定义布局的根布局返回给我们(如上图),将Button返回,没有问题。然后……
初探Android中LayoutInflater原理 - 阿里云
发现好像Button的大小好像远远没有定义的400dp宽高??我们再改一下布局,把button宽高设置为match_parent看看:
初探Android中LayoutInflater原理 - 阿里云
再运行看看效果:
初探Android中LayoutInflater原理 - 阿里云

我曹???干啥呢?设置了没用啊?还是一样。先别慌别流汗。我们仔细来回忆一下之前分析的代码顺便来求证一下前面的分析是否正确。

首先我们在调用inflate()方法的时候传入root为null,因此在inflate()方法中不会执行之前分析的一系列代码,只会执行下图(就是上文图,拿过来避免翻上去看)中1,5处代码,因此实际上我们填写的参数都没有被设置,因此我们的按钮不管怎么设置都是默认状态。
初探Android中LayoutInflater原理 - 阿里云

接下来我们改一下调用代码,将我们的mainLayout作为root传入,attachToRoot传为false:

@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
LinearLayout mainLayout = (LinearLayout) LayoutInflater.from(this).inflate(R.layout.activity_main3, null);
setContentView(mainLayout);
LayoutInflater layoutInflater = LayoutInflater.from(this);
View buttonLayout = layoutInflater.inflate(R.layout.inlate_button, mainLayout, false);
mainLayout.addView(buttonLayout);
Log.d("result", "inflate返回的view为:" + buttonLayout.toString());
}

运行看看效果:
初探Android中LayoutInflater原理 - 阿里云

有效果啦!(请原谅400dp效果太明显了。。。)。这是为什么呢?其实layout_width和layout_height是用于设置View在布局中的大小的,也就是说,首先View必须存在于一个布局中,之后如果将layout_width设置成match_parent表示让View的宽度填充满布局,如果设置成wrap_content表示让View的宽度刚好可以包含其内容,如果设置成具体的数值则View的宽度会变成相应的数值。

因为这次我们把mainLayout作为了button的父布局。button存在于一个父布局中,因此参数设置才生效了。顺便来看一下方法返回的结果是什么,看,直接将button返回给我们,和上面的结论一致。
初探Android中LayoutInflater原理 - 阿里云
接下来我们再改代码,把attachToRoot改为true传入看看:
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
LinearLayout mainLayout = (LinearLayout) LayoutInflater.from(this).inflate(R.layout.activity_main3, null);
setContentView(mainLayout);
LayoutInflater layoutInflater = LayoutInflater.from(this);
View buttonLayout = layoutInflater.inflate(R.layout.inlate_button, mainLayout, true);
mainLayout.addView(buttonLayout);
Log.d("result", "inflate返回的view为:" + buttonLayout.toString());
}

运行,等待。
emmm…
emmm…
emm….

初探Android中LayoutInflater原理 - 阿里云

????哈玩意儿??几行代码还能挂了??这是为啥??还是别慌,其实报错才是正确姿势,我们来看看报错信息:
初探Android中LayoutInflater原理 - 阿里云
image.png
仔细看看报错信息,意思就是说我们的布局已经有了一个父布局了,不能再addView了。其实这也解决了我们在ListView中getView中经常遇到的错误(具体不再阐述)。当我们传入root,并且设置attachToRoot为true时,inflate()方法内部(上文源代码的标注4处)就为我们addView了。我们如果在这之后再手动调用addView,那么就会报错,因此我们无需在手动addView,去掉手动add代码再看看:

初探Android中LayoutInflater原理 - 阿里云
首先效果是正确的!button大小正确显示,。再看看方法返回的结果:

初探Android中LayoutInflater原理 - 阿里云
果不其然,这次变成了返回MainLayout,这也印证了之前的结论如果有root,attachToRoot为ture会返回root给我们。至此我们就一起分析源码, 验证源码的效果也就差不多啦。

ps:如果有时候我们一定要root为null,但是又要动态改变button的大小呢?(瞎想的场景,毕竟大家都喜欢没事瞎想)。那么我们就可以在button外面套一个布局,这样的话解析时外布局参数设置虽然失效,但是button的参数已经是相对于外布局了,就可以有效。修改buttonLayout布局代码如下:
初探Android中LayoutInflater原理 - 阿里云

执行结果:
初探Android中LayoutInflater原理 - 阿里云
可以看出button的效果又回来啦。

至此本文的分析就差不多结束啦,其中还有一些可以深究的地方如果感兴趣的同学可以继续深入探索!
ps:还是老话,本人萌新,如果文中有错误或者模糊的地方,希望大家能多多指正,还望多多包涵。

再ps:写本文和分析源码的过程中也参考了郭霖大神的文章,也算是站在前辈的肩膀上继续进行自己的探索把。附上链接:
http://blog.csdn.net/guolin_blog/article/details/12921889