1. 程式人生 > >【UI佈局優化】Android佈局優化的幾種方式

【UI佈局優化】Android佈局優化的幾種方式

在Android中,佈局優化越來越受到重視,下面將介紹佈局優化的幾種方式,這幾種方式一般可能都見過,因為現在用的還比較多,我們主要從兩個方面來進行介紹,一方面是用法,另一方面是從原始碼來分析,為什麼它能起到優化的效果。

一、幾種方式的用法
1、佈局重用<include />

這個標籤的主要作用就是它能夠重用佈局檔案,如果一些佈局在許多佈局檔案中都需要被使用,我們就可以把它單獨寫在一個佈局中,然後使用這個標籤在需要使用它的地方把這個佈局加進去,這樣就達到了重用的目的,最典型的一個用法就是,如果我們自定義了一個TitleBar,這個TitleBar可能需要在每個Activity的佈局檔案中都使用到,這樣我們就可以使用這個標籤來實現,下面來舉個例子。

<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"  
    android:orientation="vertical"   
    android:layout_width=”match_parent”  
    android:layout_height=”match_parent”  
    android:background="@color/app_bg"  
    android:gravity="center_horizontal">  

    <include  android:id="@+id/titlebar"
layout="@layout/titlebar"/> <TextView android:layout_width=”match_parent” android:layout_height="wrap_content" android:text="@string/hello" android:padding="10dp" /> ... </LinearLayout>

上面就代表一個Activity的佈局檔案,我們自己寫了一個titleBar佈局,直接使用inclue標籤的layout來指定就可以把這個titleBar的佈局檔案加入進去,這樣在每個Activity中我們就可以使用include標籤來重用這個titleBar佈局了,不需要在每個裡面都重複寫一個titleBar的佈局了,下面我們來看看這個titleBar的佈局檔案。

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

    <TextView
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="首頁"/>

</LinearLayout>

上面只是我們簡單的寫了一個titleBar的佈局檔案,我們可以根據需要自己來寫一個。

在程式碼中,如果我們希望得到這個titlebar的View,我們只需要跟其他控制元件一樣,使用findViewById來得到這個titleBar佈局的View並且可以對其進行相應的操作。

總結一點:這個標籤主要是做到佈局的重用,使用這個標籤可以把公共佈局嵌入到所需要嵌入的地方。

2、減少檢視層級<merge />

這個標籤的作用就是刪減多餘的層級,優化UI,具體什麼意思呢?還是來是例子來說明,下面我們來看一個佈局檔案。

<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="fill_parent"
    android:layout_height="fill_parent">

    <ImageView  
        android:layout_width="fill_parent" 
        android:layout_height="fill_parent" 

        android:scaleType="center"
        android:src="@drawable/golden_gate" />

    <TextView
        android:layout_width="wrap_content" 
        android:layout_height="wrap_content" 
        android:layout_marginBottom="20dip"
        android:layout_gravity="center_horizontal|bottom"

        android:padding="12dip"

        android:background="#AA000000"
        android:textColor="#ffffffff"

        android:text="Golden Gate" />

</FrameLayout>

這個佈局檔案比較簡單,就是一個FrameLayout裡面放了一個ImageView和一個TextView。下面我們來使用HierarchyViewer來檢視它的佈局層次。

這裡寫圖片描述

從這個佈局層次,就可以看到我們的FrameLayout的父佈局仍然是一個FrameLayout,其實它們是重複的,我們其實不需要使用一個FrameLayout,而是直接將我們的內容掛載上層的那個FrameLayout下面就可以,這樣怎麼做呢?使用merge標籤就可以了,我們使用merge就代表merge裡面的內容的父佈局就是merge這個標籤的父佈局,這樣就重用了父佈局。

<merge xmlns:android="http://schemas.android.com/apk/res/android">

    <ImageView  
        android:layout_width="fill_parent" 
        android:layout_height="fill_parent" 

        android:scaleType="center"
        android:src="@drawable/golden_gate" />

    <TextView
        android:layout_width="wrap_content" 
        android:layout_height="wrap_content" 
        android:layout_marginBottom="20dip"
        android:layout_gravity="center_horizontal|bottom"

        android:padding="12dip"

        android:background="#AA000000"
        android:textColor="#ffffffff"

        android:text="Golden Gate" />

</merge>

上面就是具體的程式碼,我們使用merge就表示我們的merge標籤裡面的ImageView和TextView的父佈局就是merge的父佈局FrameLayout,merge它不屬於一個佈局層次。下面我們再來看看整個佈局層次。

這裡寫圖片描述

從上圖應該就一目瞭然了,總結一點:如果可以重用父佈局,我們就可以使用merge,這樣就減少了一個佈局層次,這樣可以加快UI的解析速度。

3、延遲載入<ViewStub />
<ViewStub />標籤最大的優點是當你需要時才會載入,使用他並不會影響UI初始化時的效能,它 是一個不可見的,大小為0的View,最佳用途就是實現View的延遲載入,避免資源浪費,在需要的時候才載入View。

<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical">

    <TextView
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="內容1"/>

    <ViewStub
        android:id="@+id/pic_stub"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_centerInParent="true"
        android:inflatedId="@+id/pic_view_id_after_inflate"
        android:layout="@layout/pic_view" />

    <TextView
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="內容2"/>

    <Button
        android:text="載入ViewStub"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:onClick="startService"/>

</LinearLayout>

最開始使用setContentView(R.layout.activity_main)的時候,ViewStub只是起到一個佔位符的作用,它並不會佔用空間,所以對其他的佈局沒有影響。

當我們點選Button的時候,我們就可以把ViewStub的layout屬性指定的佈局載入進來,用它來替換ViewStub,這樣就把我們需要載入的內容載入進來了。具體的使用方式有兩種:
1、通過findViewById找到ViewStub,然後直接呼叫setVisibility,這樣它就會把layout裡面指定的佈局新增進來。

((ViewStub) findViewById(R.id.stub_import)).setVisibility(View.VISIBLE);  

2、通過findViewById找到ViewStub,然後直接呼叫inflate函式,使用這樣方式的好處就是它可以將載入的佈局View返回去,這樣我們就可以拿到這個View進行相應的操作了。

View importPanel = ((ViewStub) findViewById(R.id.stub_import)).inflate();  

我們需要主要的是,在載入之前,我們通過pic_stub這個id來找到ViewStub,在載入之後,如果我們再希望獲取到載入進來的這個佈局的View,我們需要使用inflatedId這個屬性指定的id來獲取,因為在載入了佈局之後,原來ViewStub的id會被inflatedId指定的這個id覆蓋。

二、原始碼分析上面三種方式的過程

我們知道它是通過Pull解析器來解析佈局檔案的,它在解析一個佈局檔案的時候,最終會執行rInflate函式,在Android獲取到inflate服務的方式及inflate的解析過程這篇文章具體講解它的過程,我們主要來分析分析這個函式。

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

    final int depth = parser.getDepth();
    int type;

    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)) {
            parseRequestFocus(parser, parent);
        } else if (TAG_INCLUDE.equals(name)) {
            if (parser.getDepth() == 0) {
                throw new InflateException("<include /> cannot be the root element");
            }
            parseInclude(parser, parent, attrs);
        } else if (TAG_MERGE.equals(name)) {
            throw new InflateException("<merge /> must be the root element");
        } else if (TAG_1995.equals(name)) {
            final View view = new BlinkLayout(mContext, attrs);
            final ViewGroup viewGroup = (ViewGroup) parent;
            final ViewGroup.LayoutParams params = viewGroup.generateLayoutParams(attrs);
            rInflate(parser, view, attrs, true);
            viewGroup.addView(view, params);                
        } else {
            final View view = createViewFromTag(parent, name, attrs);
            final ViewGroup viewGroup = (ViewGroup) parent;
            final ViewGroup.LayoutParams params = viewGroup.generateLayoutParams(attrs);
            rInflate(parser, view, attrs, true);
            viewGroup.addView(view, params);
        }
    }

    if (finishInflate) parent.onFinishInflate();
}

在解析標籤的時候,它會根據不同的標籤進行不同的處理,我們來看看它的過程。
1、如果這個標籤為include標籤

if (TAG_INCLUDE.equals(name)) {
    if (parser.getDepth() == 0) {
        throw new InflateException("<include /> cannot be the root element");
    }
    parseInclude(parser, parent, attrs);
}

它會執行parseInclude函式,我們來看看它的處理。

private void parseInclude(XmlPullParser parser, View parent, AttributeSet attrs)
        throws XmlPullParserException, IOException {

    int type;
    // 1、判斷父佈局是否為一個ViewGroup例項
    if (parent instanceof ViewGroup) {
        // 2、得到include標籤中layout屬性的值,它就是重用佈局
        final int layout = attrs.getAttributeResourceValue(null, "layout", 0);
        if (layout == 0) {
            final String value = attrs.getAttributeValue(null, "layout");
            if (value == null) {
                throw new InflateException("You must specifiy a layout in the"
                        + " include tag: <include layout=\"@layout/layoutID\" />");
            } else {
                throw new InflateException("You must specifiy a valid layout "
                        + "reference. The layout ID " + value + " is not valid.");
            }
        } else {
            // 3、解析重用佈局檔案
            final XmlResourceParser childParser =
                    getContext().getResources().getLayout(layout);

            try {
                final AttributeSet childAttrs = Xml.asAttributeSet(childParser);

                while ((type = childParser.next()) != XmlPullParser.START_TAG &&
                        type != XmlPullParser.END_DOCUMENT) {
                    // Empty.
                }

                if (type != XmlPullParser.START_TAG) {
                    throw new InflateException(childParser.getPositionDescription() +
                            ": No start tag found!");
                }

                final String childName = childParser.getName();

                if (TAG_MERGE.equals(childName)) {
                    // Inflate all children.
                    rInflate(childParser, parent, childAttrs, false);
                } else {
                    final View view = createViewFromTag(parent, childName, childAttrs);
                    final ViewGroup group = (ViewGroup) parent;

                    // We try to load the layout params set in the <include /> tag. If
                    // they don't exist, we will rely on the layout params set in the
                    // included XML file.
                    // During a layoutparams generation, a runtime exception is thrown
                    // if either layout_width or layout_height is missing. We catch
                    // this exception and set localParams accordingly: true means we
                    // successfully loaded layout params from the <include /> tag,
                    // false means we need to rely on the included layout params.
                    ViewGroup.LayoutParams params = null;
                    try {
                        params = group.generateLayoutParams(attrs);
                    } catch (RuntimeException e) {
                        params = group.generateLayoutParams(childAttrs);
                    } finally {
                        if (params != null) {
                            view.setLayoutParams(params);
                        }
                    }

                    // Inflate all children.
                    rInflate(childParser, view, childAttrs, true);

                    // Attempt to override the included layout's android:id with the
                    // one set on the <include /> tag itself.
                    TypedArray a = mContext.obtainStyledAttributes(attrs,
                        com.android.internal.R.styleable.View, 0, 0);
                    int id = a.getResourceId(com.android.internal.R.styleable.View_id, View.NO_ID);
                    // While we're at it, let's try to override android:visibility.
                    int visibility = a.getInt(com.android.internal.R.styleable.View_visibility, -1);
                    a.recycle();

                    if (id != View.NO_ID) {
                        view.setId(id);
                    }

                    switch (visibility) {
                        case 0:
                            view.setVisibility(View.VISIBLE);
                            break;
                        case 1:
                            view.setVisibility(View.INVISIBLE);
                            break;
                        case 2:
                            view.setVisibility(View.GONE);
                            break;
                    }
                    // 4、把解析處理的View加入到父佈局中
                    group.addView(view);
                }
            } finally {
                childParser.close();
            }
        }
    } else {
        throw new InflateException("<include /> can only be used inside of a ViewGroup");
    }

    final int currentDepth = parser.getDepth();
    while (((type = parser.next()) != XmlPullParser.END_TAG ||
            parser.getDepth() > currentDepth) && type != XmlPullParser.END_DOCUMENT) {
        // Empty
    }
}

上面展示它的整個過程,它就是將layout指定的這個佈局檔案進行解析,然後加入父佈局中.

2、如果這個標籤為merge標籤

if (TAG_MERGE.equals(name)) {
    throw new InflateException("<merge /> must be the root element");
}

它裡面丟擲了一個異常,對應merge的使用,我們要具體的根據場合而定,具體要看父佈局是否能夠被重用,並且它要為根佈局。在上面include的標籤的解析中可以看到merge標籤的處理過程。

if (TAG_MERGE.equals(childName)) {
    // Inflate all children.
    rInflate(childParser, parent, childAttrs, false);
}

從這裡可以可以看到,如果是merge標籤,就直接解析它的所有子元素,也就是說merge的父佈局就是它內部子元素的父佈局。

3、對於ViewStub,它會跟其他控制元件一樣,例項化一個ViewStub物件

下面我來重點看看ViewStub類的setVisibility和inflate函式

首先我們需要看的是ViewStub的建構函式:

public ViewStub(Context context, AttributeSet attrs, int defStyle) {
    TypedArray a = context.obtainStyledAttributes(attrs, com.android.internal.R.styleable.ViewStub,
            defStyle, 0);

    //得到屬性inflatedId的值
    mInflatedId = a.getResourceId(R.styleable.ViewStub_inflatedId, NO_ID);
    //得到屬性layout的值
    mLayoutResource = a.getResourceId(R.styleable.ViewStub_layout, 0);

    a.recycle();

    a = context.obtainStyledAttributes(attrs, com.android.internal.R.styleable.View, defStyle, 0);
    mID = a.getResourceId(R.styleable.View_id, NO_ID);
    a.recycle();

    initialize(context);
}

private void initialize(Context context) {
    mContext = context;
    // 從這裡可以看到最開始這個控制元件的可見性為GONE
    setVisibility(GONE);
    // 這裡先對它不進行繪製
    setWillNotDraw(true);
}

上面的工作就是兩點:
1、獲取各個屬性的值
2、設定ViewStub的可見性為GONE,也就是它不佔位置,並且也不繪製,因為它不是真正要顯示的View

下面看看ViewStub的inflate函式:

public View inflate() {
    final ViewParent viewParent = getParent();

    if (viewParent != null && viewParent instanceof ViewGroup) {
        if (mLayoutResource != 0) {
            final ViewGroup parent = (ViewGroup) viewParent;
            final LayoutInflater factory;
            if (mInflater != null) {
                factory = mInflater;
            } else {
                factory = LayoutInflater.from(mContext);
            }
            // 這裡直接解析mLayoutResource這個佈局,也就是上面得到的layout屬性值
            final View view = factory.inflate(mLayoutResource, parent,
                    false);
            // 這裡會對這個佈局設定id,也就是inflatedId的屬性值
            if (mInflatedId != NO_ID) {
                view.setId(mInflatedId);
            }

            // 這裡從父佈局中找到這個viewstub的index
            final int index = parent.indexOfChild(this);
            //這裡將viewstub這個佔位view移除
            parent.removeViewInLayout(this);

            // 這裡會把這個view新增到父佈局指定的index中去,也就實現了對viewstub的替換
            final ViewGroup.LayoutParams layoutParams = getLayoutParams();
            if (layoutParams != null) {
                parent.addView(view, index, layoutParams);
            } else {
                parent.addView(view, index);
            }

            //這裡會把這個view弱引用到mInflatedViewRef
            mInflatedViewRef = new WeakReference<View>(view);

            // 如果設定了回撥,就會呼叫回撥函式
            if (mInflateListener != null) {
                mInflateListener.onInflate(this, view);
            }

            return view;
        } else {
            throw new IllegalArgumentException("ViewStub must have a valid layoutResource");
        }
    } else {
        throw new IllegalStateException("ViewStub must have a non-null ViewGroup viewParent");
    }
}

上面的工作可以總結為以下幾點:
1、解析出layout屬性賦值的佈局,得到對應的View
2、為這個View指定id
3、找到ViewStub在父佈局的索引,然後將ViewStub移除
4、將上面解析的View加入到父佈局的指定索引處

上面的整個過程總結一點就是:使用給定的佈局來替換ViewStub,達到動態載入的目的,ViewStub僅僅只是一個佔位View.

下面看看setVisibility函式的原始碼。

public void setVisibility(int visibility) {
    if (mInflatedViewRef != null) {
        View view = mInflatedViewRef.get();
        if (view != null) {
            view.setVisibility(visibility);
        } else {
            throw new IllegalStateException("setVisibility called on un-referenced view");
        }
    } else {
        super.setVisibility(visibility);
        if (visibility == VISIBLE || visibility == INVISIBLE) {
            inflate();
        }
    }
}

它的處理可以看到,首先看mInflatedViewRef是否為空,上面在inflate中,我們看到它會把解析處理的view弱引用到mInflatedViewRef,如果不為空,就可以直接得到這個View,然後設定它為可見。如果為空,這樣就會執行inflate方法。就是上面的那個方法。

參考文章: