源码分析 merge 标签减少布局层级的秘密(Android Q)

源码分析 merge 标签减少布局层级的秘密(Android Q)


我在《Android 渲染性能优化——你需要知道的一切!》一文中介绍过,merge 标签用于减少 View 树的层次来优化 Android 的布局。

我们在布局中使用 merge 标签可以减少不必要的层级。

使用场景

merge 标签可以优化布局层级,那么它有哪些主要的使用场景呢?

  1. merge 最常见的是和 include 标签一块使用。这样在复用布局的时候,也就不会增加多余的布局嵌套了,解决了只有 include 标签带来的问题。
  2. 另外,在自定义组合 View 的时候(例如,继承自 FrameLayout、LinearLayout 等),我们通常会创建一个自定义一个布局,并且通过索引 id 添加到自定义 View 中,这时如果不用 merge 标签,无形中会增加了一层的嵌套。

例如,我们自定义了一个容器 View,类名为 TestLayout,继承自 LinearLayout,该自定义控件的填充布局就可以使用 merge 标签。

<?xml version="1.0" encoding="utf-8"?>
<merge xmlns:android="http://schemas.android.com/apk/res/android"
       android:layout_width="match_parent"
       android:layout_height="match_parent">

    <TextView
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"/>

    <ImageView
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"/>
</merge>

TestLayout 继承自 LinearLayout,所以它本身就是一个 LinearLayout,如果我们将布局中的 merge 修改为 LinearLayout,就会出现2个 LinearLayout 嵌套,其中一个就是多余的。

merge 标签原理&源码解析

那么,merge 标签减少层级的原理是什么呢?

要想知道它的原理,最直接的方法就是查看 Android 源码。接下来我们来通过源码来分析它的实现原理。

通过阅读前文《Android Q LayoutInflater布局生成View源码详解》中的分析可知,Android 布局的生成视图对象,是通过 LayoutInflater 服务来处理的,LayoutInflater 的 inflate 方法负责解析布局并生成 View 对象。merge 标签的处理过程,就包含在这个过程中,我们来看 LayoutInflater 的 inflate 方法。

LayoutInflater 的 inflate 方法

    public View inflate(XmlPullParser parser, @Nullable ViewGroup root, boolean attachToRoot) {
        synchronized (mConstructorArgs) {
            ……
            final AttributeSet attrs = Xml.asAttributeSet(parser);
            ……
            try {
                ……
                //如果根标签是 merge 标签
                if (TAG_MERGE.equals(name)) {
                    //如果根节点是 merge,并且 root 是 null 或者 attachToRoot == false,则抛出异常。
                    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 {
                    //非 merge 跟标签,则创建根标签所代表的 View 对象。
                    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);
                        }
                        //获取根标签 View 对象的视图属性,这里会使用根标签所设置的属性来生成属性对象。
                        params = root.generateLayoutParams(attrs);
                        if (!attachToRoot) {
                            //对根标签设置它的属性
                            temp.setLayoutParams(params);
                        }
                    }
                    ……

                    //以根标签对象为根,递归遍历它的 xml 子视图。
                    rInflateChildren(parser, temp, attrs, true);
                    ……
                }
            }
        }
    }

对于以 merge 标签为根标签的布局文件:

  1. 将 merge 标签的布局文件,生成为视图时,root 必须存在,并且 attachToRoot 必须为 true,否则就会抛出 InflateException 异常。
  2. 接下来,调用 rInflate 方法,遍历布局中的子标签并生成 View。
  3. 这里会把 root (该布局文件在视图树上的父视图)和 attrs 传递给 rInflate 方法,用来创建布局文件的子视图。

对于以非 merge 标签为根标签的布局文件:

  1. 创建根标签所代表的 View 对象。这在 merge 标签时,并没有创建,因为 merge 并不是一个视图组件。
  2. 获取根标签 View 对象的视图属性,这里会使用根标签所设置的属性来生成属性对象,然后对根标签设置它的属性。这在 merge 标签时,也没有执行,所以 merge 标签上设置的属性不会生效。
  3. 以根标签对象为根,递归遍历它的 xml 子视图。这在 merge 标签时,是以 root 为根(作为参数),对 merge 标签的子标签进行遍历的。这一点就是 merge 标签会少一层的秘密!

LayoutInflater 的 rInflate 方法

    void rInflate(XmlPullParser parser, View parent, Context context
            AttributeSet attrs, boolean finishInflate) throws XmlPullParserException, IOException {
        ……
        while (((type = parser.next()) != XmlPullParser.END_TAG ||
                parser.getDepth() > depth) && type != XmlPullParser.END_DOCUMENT) {
            ……
            final String name = parser.getName();

            if (TAG_REQUEST_FOCUS.equals(name)) {
                ……
            } 
            …… ……
            } 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);
            }
        }
        ……
    }

该方法的目的是:遍历并且创建 xml 布局文件中,根标签的子标签所代表的的 View 组件对象。

  1. rInflate 方法通过递归解析 xml 布局文件内容,创建 View,并添加到 parent 中。
  2. 这里要注意,merge 标签必须是布局文件的根节点!

好了,到了这里,merge 标签的秘密,我们已经知道了。

总结


我们来总结下:

  1. merge 标签可以用于减少 View 树的层次来优化 Android 的布局。

  2. Android 布局的生成视图对象,是通过 LayoutInflater 服务来处理的,LayoutInflater 的 inflate 方法负责解析布局并生成 View 对象。

  3. 使用 merge 标签时,merge 标签必须是布局文件的根节点!

  4. 将 merge 标签的布局文件,生成为视图时,root 必须存在,并且 attachToRoot 必须为 true,否则就会抛出 InflateException 异常。

  5. 在 merge 标签上设置的属性无效!

  6. 在解析布局文件时,正常情况下会以根标签对象为根,递归遍历它的 xml 子视图;而在使用 merge 标签时,则是以 root 为根(作为参数),传递给 rInflate 方法创建子视图。这一点就是 merge 标签会少一层的秘密!


PS:更多分析文章,请查看系列文章–>《Android底层原理解析》专栏。
PS:更多分析文章,请查看系列文章–>《Android底层原理解析》专栏。
PS:更多分析文章,请查看系列文章–>《Android底层原理解析》专栏。

猜你喜欢

转载自blog.csdn.net/u011578734/article/details/111494896