1. 程式人生 > >Android佈局優化之merge標籤詳解

Android佈局優化之merge標籤詳解

我們都知道View的繪製流程需要經歷measure、layout、draw這個三個過程,如果佈局巢狀層次比較深的話,每一步都需要進行遍歷所有子View進行對應的measure、layout、draw過程,由此就會降低繪製效率,巢狀越多,耗時就越多;其實不光光只會影響view的繪製效率,同樣的也會影響xml佈局的解析效率。

針對上面的問題,Android為我們引入了merger標籤來降低佈局巢狀的問題。如果在佈局檔案中使用merge標籤,則需要在include標籤中引入,可以這麼說吧,merge標籤是include標籤一種輔助擴充套件。

merge_title_bar佈局

<?xml version="1.0" encoding="utf-8"?>
<merge xmlns:android="http://schemas.android.com/apk/res/android" xmlns:tools="http://schemas.android.com/tools" android:layout_width="match_parent" android:layout_height="wrap_content" tools:ignore="all"> <Button android:id="@+id/back" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_alignParentStart="true" android:textAllCaps="false" android:text="返回"/>
<Button android:id="@+id/title" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_centerHorizontal
="true" android:background="@android:color/white" android:textAllCaps="false" android:text="標題"/>
<Button android:id="@+id/share" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_alignParentEnd="true" android:text="分享" android:textAllCaps="false"/> </merge>

普通佈局title_bar

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

    <Button
        android:id="@+id/back"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:textAllCaps="false"
        android:layout_alignParentStart="true"
        android:text="返回"/>

    <Button
        android:id="@+id/title"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:background="@android:color/white"
        android:layout_centerInParent="true"
        android:textAllCaps="false"
        android:onClick="onClick"
        android:text="標題"/>


    <Button
        android:id="@+id/share"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:textAllCaps="false"
        android:layout_alignParentEnd="true"
        android:text="分享"/>

</RelativeLayout>

接下來我們在activity_ui_optimize佈局中分別引用merge_title_bar和title_bar這兩個佈局,看看之間的巢狀層級

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    android:id="@+id/ll_root_view"
    android:orientation="vertical"
    android:layout_width="match_parent"
    android:layout_height="wrap_content">

    <!--引入普通佈局-->
    <include
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        layout="@layout/title_bar"/>

    <!--引入merge標籤佈局1-->
    <include
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        layout="@layout/merge_title_bar"/>

    <!--引入merge標籤佈局2-->
    <RelativeLayout
        android:id="@+id/rl_merge_view"
        android:layout_width="match_parent"
        android:layout_height="wrap_content">
        <include
            layout="@layout/merge_title_bar"/>
    </RelativeLayout>


</LinearLayout>

下面看看這個三個區域的view巢狀層級是如何的

這裡寫圖片描述

紫色區域:就是使用普通佈局的效果
藍色區域:使用了merge標籤
紅色區域:同樣的使用了merge標籤,不同的是用RelativeLayout包裹了include標籤。
綠色區域:就是activity_ui_optimize佈局

從圖中可以看出紫色區域的button與藍色區域的button不在同一樣層級節點上,藍色區域的button少了一層巢狀,可見merge標籤可以減少view的巢狀;同樣的你會發現紫色區域與藍色區域的佈局位置有所不同,一個是橫排,另外一個是豎排,關於merge標籤元素佈局定位的問題,相信很多人在剛使用的時候可能會和我一樣產生疑問,merge標籤內元素的定位是受什麼影響,其實是由父佈局決定的,我們可以從原始碼的角度進行解釋:

首先我們要從Activity的setContentView方法講起

 public void setContentView(@LayoutRes int layoutResID) {
        getWindow().setContentView(layoutResID);
        initWindowDecorActionBar();
    }

可見真正負責Activity的檢視展示並不是Activity本身,而是交給Window物件處理,Window是一個抽象類,PhoneWindow是它的唯一實現類,接下來進入PhoneWindow的setContentView方法看看

    @Override
    public void setContentView(int layoutResID) {
        // Note: FEATURE_CONTENT_TRANSITIONS may be set in the process of installing the window
        // decor, when theme attributes and the like are crystalized. Do not check the feature
        // before this happens.
        if (mContentParent == null) {
            installDecor();
        } else if (!hasFeature(FEATURE_CONTENT_TRANSITIONS)) {
            mContentParent.removeAllViews();
        }

        if (hasFeature(FEATURE_CONTENT_TRANSITIONS)) {
            final Scene newScene = Scene.getSceneForLayout(mContentParent, layoutResID,
                    getContext());
            transitionTo(newScene);
        } else {
            mLayoutInflater.inflate(layoutResID, mContentParent);
        }
        mContentParent.requestApplyInsets();
        final Callback cb = getCallback();
        if (cb != null && !isDestroyed()) {
            cb.onContentChanged();
        }
        mContentParentExplicitlySet = true;
    }

從setContentView方法可知,首先會通過installDecor方法建立DecorView,同時會把DecorView放入Window中,可見DecorView物件是Window頂級的檢視,緊接著會呼叫mLayoutInflater.inflate方法,下面進入LayoutInfater中的inflate方法探知

private static final String TAG_MERGE = "merge";

 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("**************************");
                }
                //判斷是否是merge標籤
                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 {
                ……省略
                }

            return result;
        }
    }

在inflate方法中,通過Pull解析,parser.getName()方法得到xml佈局節點的屬性名稱,緊接著會判斷節點的是不是merge標籤,如果是merge標籤,首先會判斷是否有父檢視root,如果沒有會丟擲InflateException異常,可見merge標籤的引入使用必須依賴於父檢視(根檢視),緊接著才會進入rInflate方法

    void rInflate(XmlPullParser parser, View parent, Context context,
            AttributeSet attrs, boolean finishInflate) throws XmlPullParserException, IOException {

        final int depth = parser.getDepth();
        int type;
        boolean pendingRequestFocus = false;

        while (((type = parser.next()) != XmlPullParser.END_TAG ||
                parser.getDepth() > depth) && type != XmlPullParser.END_DOCUMENT) {

            if (type != XmlPullParser.START_TAG) {
                continue;
            }
            //獲取節點的屬性名稱
            final String name = parser.getName();

            if (TAG_REQUEST_FOCUS.equals(name)) {
                pendingRequestFocus = true;
                consumeChildElements(parser);
            } else if (TAG_TAG.equals(name)) {
                parseViewTag(parser, parent, attrs);
            } else if (TAG_INCLUDE.equals(name)) {
                if (parser.getDepth() == 0) {
                    throw new InflateException("<include /> cannot be the root element");
                }
                parseInclude(parser, context, parent, attrs);
            } else if (TAG_MERGE.equals(name)) {
                //在xml檔案中,merge必須是根節點,
                throw new InflateException("<merge /> must be the root element");
            } else {
                //建立merge標籤下的view
                final View view = createViewFromTag(parent, name, context, attrs);
                //得到父檢視佈局引數
                final ViewGroup viewGroup = (ViewGroup) parent;
                final ViewGroup.LayoutParams params = viewGroup.generateLayoutParams(attrs);
                //構建view下的子view
                rInflateChildren(parser, view, attrs, true);
                //將view新增到父檢視在中
                viewGroup.addView(view, params);
            }
        }

        if (pendingRequestFocus) {
            parent.restoreDefaultFocus();
        }

        if (finishInflate) {
            parent.onFinishInflate();
        }
    }

在rInflate方法中,定義一個while迴圈來遍歷解析佈局View每個子節點,得到每個節點屬性名稱,經過一系列的判斷,從中可知,在一個xml佈局檔案中,merge必須是根節點,在merge根標籤內不能再有其他的merge標籤。最後會根據merge根標籤內的每個標籤建立對應的view檢視,緊接著會得到父檢視的佈局引數,接著繪製父檢視下每個子view,最後將子View新增到父檢視中,到此可以發現merge標籤內容元素的定位最終還是由父檢視來決定的。

回過頭來,再去看看藍色區域與紅色區域的merge標籤佈局顯示,就會決定很清晰了,藍色區域下的button顯示為垂直豎排,是因為它的父檢視是垂直線性佈局LinearLayout(id:ll_root_view),而紅色區域下的button顯示水平橫排,是因為它的父檢視是相對佈局relativeLayout(id:rl_merge_view),在相對佈局中,為每個button設定了相應的定位屬性值。

這裡使用Android studio3.0的Layout Inspector工具,取代了之前的HierarchyViewer工具,下面就簡單介紹下使用方法:

1、首先找到Layout Inspector,可以通過搜尋方式找到

這裡寫圖片描述

2、點選進入Layout Inspector ,Choose Process 對話方塊中,選擇您想要檢查的應用程序,然後點選 OK

這裡寫圖片描述

3、在Select Window對話方塊中,選擇對應的Activity

這裡寫圖片描述

4、到此基本就可以展示出來了
這裡寫圖片描述

詳細的介紹可以參考官方介紹Layout Inspector,如果要使用之前的HierarchyViewer工具,需要到sdk目錄下tools下找到monitor.bat這個檔案點選就可以開啟,包括DDMS也在其中。

參考: