初探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()方法去看源代码就会发现,其实第一种方法无非就是对第二种方法进行封装(代码如下):

从上图中标注处可以看出其实也是通过context.getSystemService方法来获取,只不过增加了安全判断更加安全而已。

我们获取LayoutInflater对象后,就可以通过以下方法来进行布局解析:

 layoutInflater.inflate(resourceId, root,attachToRoot); 

其中第一个参数就是要加载的布局id,第二个参数是传入一个布局做为要解析布局的父布局。如果不需要就直接传null。第三个参数指的是加载的布局是否添加到我们传入的父布局中。
接下来我们再来接着具体分析一下inflate()方法。

首先这里先整理出来inflate()的四个调用方法:
调用方法一
调用方法二
调用方法三
这里来一一说明一下三幅图的大概意思。

图一调用的inflate方法只需要传入布局id,root父布局view对象,而第三个参数的值是通过判断root是否为空来设置,再调用图三方法。
图二中第一个是XmlPullParser对象(是一种通过pull方式来解析xml的对象,有兴趣的同志可以自行了解,只需要知道用来解析xml的对象即可),第二个参数,第三个参数和图一同理。
图三我们可以看到,将传入的布局id转化为Resources类型,然后将它解析来获取XmlResourceParser对象,再最终调用图二方法。
至此其实已经很清晰了,其实上面几个不同的方法都只是做了一点预处理的过程,最终的实现的源代码并没有出现在这三个方法中,而是最终调用了:

 public View inflate(XmlPullParser parser, @Nullable ViewGroup root, boolean 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;
  }    
}

上面是完整版本的源代码,去除其中一些打印,异常处理和预处理无关代码后。我们将关键代码提取出来进行分析,关键代码如下:
参数定义
关键处理过程

第一幅图中我们可以在标注处看到,一开始定义了一个View类型的result变量一开始默认就是我们传入的root对象。

而第二幅图就是处理的关键代码。

首先在图中标注1处通过注释可看到:temp是我们xml布局中定义的根布局。因此在这里将我们的布局的根布局解析出来转化为View类型的temp对象,然后定义一个params。

接下来在标注2处判断root是否为空,如果不为空,将自定义布局的参数解析出来赋值给params。

然后在标注3处判断我们传入的attchToRoot是否为true,如果不是,意味着我们不把我们的自定义布局加入到某个父View中,因此将params设置给temp,将temp在xml设置的属性进行生效。

标注3处和标注4处之间的rInflateChildren()方法是通过递归的方法用来不断解析我们自定义布局中的子View,这不再做展开。

接下来在图中标注4处判断root是否为空,attachToRoot是否为true。如果root不为空并且attachToRoot为true,那么将我们的temp添加到父布局中。

上述代码都是在root不为空的场景下设置的,如果root为空呢?
我们看看图中标注5处:如果root为空,或者attachToRoot为false,那么将temp直接赋值为result并返回。(result可以在上面的完整源码中看到创建时就是root).

------------------------------一条假装很华丽的分割线------------------------------

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

(ps:个人理解:总之就是返回我们布局的根布局,如果我们没有传入root,那么明显我们自定义布局根布局就是最顶级,返回它。如果我们传入了root,但是attchToRoot为false,说明我们不想把自定义布局加入到root中,所以我们想要的布局的根布局也是自定义布局的根布局。而我们传入root,attchToRoot为true,说明我们要把自定义布局加入到root中,所以root成了最顶级布局,所以返回root。)

------------------------------又是一条假装很华丽的分割线----------------------------

至此代码和结论就都已经分析完毕了,按照常理我们就该撒花结束,但是呢?说了半天,没啥直接的效果看看啊!这干说半天没意思是吧?因此现在我们来写个Demo实际看一下效果验证一下我们上述的分析和结论是否正确:

首先我们定义一个布局(名字叫buttonLayout):

可以看到就是一个简单的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());
}

来看看实际效果:
方法返回结果日志

 首先当root为空时,会把自定义布局的根布局返回给我们(如上图),将Button返回,也验证了上面的结论一。然后......
发现好像Button的大小好像远远没有定义的400dp宽高??我们再改一下布局,把button宽高设置为match_parent看看:
再运行看看效果:

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

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

接下来我们改一下调用代码,将我们的mainLayout作为root传入,attachToRoot传为false(其他代码不变):

//修改前代码
 View buttonLayout = layoutInflater.inflate(R.layout.inlate_button, null);       
//修改后前代码
 View buttonLayout = layoutInflater.inflate(R.layout.inlate_button, mainLayout, false); 

运行一下。

因为这次我们把mainLayout作为了button的父布局。button存在于一个父布局中,因此参数设置才生效了。先来看一下方法返回的结果是什么。看,因为attachToRoot为false,所以直接将button返回给我们,和上面的结论三一致。
方法返回结果日志
然后来看看运行效果:

有效果啦!(请原谅400dp效果太明显了。。。)。这是为什么呢?(来来来,科普一下)其实安卓中layout_width和layout_height是用于设置View在布局中的大小的(这种官方话我听着都别扭)。

简单来说,就是首先View必须存在于一个父布局中layout_width等参数才会生效(给一个小孩子一包糖说你可以吃,去找你爹给你打开,如果孩子连爹都没有,空有一包糖也打不开呀)。

例如如果将layout_width设置成match_parent表示让View的宽度填充满父布局,如果设置成wrap_content表示让View的宽度在父布局中的宽度刚好可以包含自身内容,如果设置成具体的数值则View的宽度会变成相应的数值。
接下来我们再改代码,把attachToRoot改为true传入看看(其他代码不变):

//修改前代码
 View buttonLayout = layoutInflater.inflate(R.layout.inlate_button, mainLayout, false);      
 mainLayout.addView(buttonLayout);
//修改后前代码
 View buttonLayout = layoutInflater.inflate(R.layout.inlate_button, mainLayout, true); 
 mainLayout.addView(buttonLayout);

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


????哈玩意儿??几行代码还能挂了??这是为啥??还是别慌,其实报错才是正确姿势,我们来看看报错信息:
报错信息
仔细看看报错信息,意思就是说我们的布局已经有了一个父布局了,不能再addView了。其实这也解决了我们在ListView中getView中经常遇到的错误(具体不再阐述)。当我们传入root,并且设置attachToRoot为true时,inflate()方法内部(上文源代码的标注4处)就为我们addView了。我们如果在这之后再手动调用addView,那么就会报错,因此我们无需在手动addView,去掉手动add代码再看看:
首先效果是正确的!button大小正确显示,。再看看方法返回的结果:
方法返回结果日志
果不其然,这次变成了返回mainLayout,这也印证了之前的结论如果有root,attachToRoot为true会返回root给我们。也就证明了结论二。至此我们就一起分析源码, 验证源码的效果也就差不多啦。

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

执行结果:
可以看出button的效果又回来啦。

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


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

推荐阅读更多精彩内容