源碼分析 merge 标簽減少布局層級的秘密(Android Q)
我在《Android 渲染性能優化——你需要知道的一切!》一文中介紹過,merge 标簽用于減少 View 樹的層次來優化 Android 的布局。
我們在布局中使用 merge 标簽可以減少不必要的層級。
使用場景
merge 标簽可以優化布局層級,那麼它有哪些主要的使用場景呢?
- merge 最常見的是和 include 标簽一塊使用。這樣在複用布局的時候,也就不會增加多餘的布局嵌套了,解決了隻有 include 标簽帶來的問題。
- 另外,在自定義組合 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 标簽為根标簽的布局檔案:
- 将 merge 标簽的布局檔案,生成為視圖時,root 必須存在,并且 attachToRoot 必須為 true,否則就會抛出 InflateException 異常。
- 接下來,調用 rInflate 方法,周遊布局中的子标簽并生成 View。
- 這裡會把 root (該布局檔案在視圖樹上的父視圖)和 attrs 傳遞給 rInflate 方法,用來建立布局檔案的子視圖。
對于以非 merge 标簽為根标簽的布局檔案:
- 建立根标簽所代表的 View 對象。這在 merge 标簽時,并沒有建立,因為 merge 并不是一個視圖元件。
- 擷取根标簽 View 對象的視圖屬性,這裡會使用根标簽所設定的屬性來生成屬性對象,然後對根标簽設定它的屬性。這在 merge 标簽時,也沒有執行,是以 merge 标簽上設定的屬性不會生效。
- 以根标簽對象為根,遞歸周遊它的 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 元件對象。
- rInflate 方法通過遞歸解析 xml 布局檔案内容,建立 View,并添加到 parent 中。
- 這裡要注意,merge 标簽必須是布局檔案的根節點!
好了,到了這裡,merge 标簽的秘密,我們已經知道了。
總結
我們來總結下:
- merge 标簽可以用于減少 View 樹的層次來優化 Android 的布局。
- Android 布局的生成視圖對象,是通過 LayoutInflater 服務來處理的,LayoutInflater 的 inflate 方法負責解析布局并生成 View 對象。
- 使用 merge 标簽時,merge 标簽必須是布局檔案的根節點!
- 将 merge 标簽的布局檔案,生成為視圖時,root 必須存在,并且 attachToRoot 必須為 true,否則就會抛出 InflateException 異常。
- 在 merge 标簽上設定的屬性無效!
- 在解析布局檔案時,正常情況下會以根标簽對象為根,遞歸周遊它的 xml 子視圖;而在使用 merge 标簽時,則是以 root 為根(作為參數),傳遞給 rInflate 方法建立子視圖。這一點就是 merge 标簽會少一層的秘密!
PS:更多分析文章,請檢視系列文章–>《Android底層原了解析》專欄。
PS:更多分析文章,請檢視系列文章–>《Android底層原了解析》專欄。
PS:更多分析文章,請檢視系列文章–>《Android底層原了解析》專欄。