天天看點

源碼分析 merge 标簽減少布局層級的秘密(Android Q)源碼分析 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底層原了解析》專欄。