Android 性能優化系列之布局優化

在Android開發中,UI布局可以說是每個App使用頻率很高的,隨著UI越來越多,布局的重復性、復雜度也會隨之增長,這樣使得UI布局的優化,顯得至關重要,UI布局不慎,就會引起過度繪制,從而造成UI卡頓的情況,本篇文章,我就來總結一下UI布局優化的相關技巧。

學會使用布局標簽優化布局

(1) <include> 標簽

include標簽常用于將布局中的公共部分提取出來供其他layout共用,以實現布局模塊化,這在布局編寫方便提供了大大的便利。例如我們在進行App開發時基本每個頁面都會有標題欄,在不使用include的情況下你在每個界面都需要重新在xml里面寫一個頂部標題欄,工作量無疑是巨大的,使用include標簽,我們只需要把這個會被多次使用的頂部欄獨立成一個xml文件,然后在需要使用的地方通過include標簽引入即可。

下面以在一個布局main.xml中用include引入另一個布局foot.xml為例。main.mxl代碼如下:

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

    <ListView
        android:id="@+id/simple_list_view"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:layout_marginBottom="80dp" />

    <include
        android:id="@+id/my_foot_ly"
        layout="@layout/foot" />

</RelativeLayout>

其中include引入的foot.xml為公用的頁面底部,代碼如下:

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

    <Button
        android:id="@+id/button"
        android:layout_width="match_parent"
        android:layout_height="@dimen/dp_40"
        android:layout_above="@+id/title_tv"/>

    <TextView
        android:id="@+id/title_tv"
        android:layout_width="match_parent"
        android:layout_height="@dimen/dp_40"
        android:layout_alignParentBottom="true"
        android:text="@string/app_name" />

</RelativeLayout>

<include> 標簽唯一需要的屬性是layout屬性,指定需要包含的布局文件。可以定義android:id和android:layout_*屬性來覆蓋被引入布局根節點的對應屬性值。注意重新定義android:id后,子布局的頂結點i就變化了。

注意:

使用include最常見的問題就是findViewById查找不到目標控件,這個問題出現的前提是在include時設置了id,而在findViewById時卻用了被include進來的布局的根元素id。例如上述例子中,include時設置了該布局的id為my_foot_ly

,而my_foot_ly.xml中的根視圖的id為my_foot_parent_id。此時如果通過findViewById來找my_foot_parent_id這個控件,然后再查找my_foot_parent_id下的子控件則會拋出空指針。代碼如下 :

View titleView = findViewById(R.id.my_foot_parent_id) ; // 此時 titleView 為空,找不到。此時空指針 
TextView titleTextView = (TextView)titleView.findViewById(R.id.title_tv) ; titleTextView.setText("new Title");

其正確的使用形式應該如下:

// 使用include時設置的id,即R.id.my_title_ly
 View titleView = findViewById(R.id.my_foot_ly) ; 
// 通過titleView找子控件 TextView titleTextView = (TextView)titleView.findViewById(R.id.title_tv) ; titleTextView.setText("new Title");

或者更簡單的直接查找它的子控件

TextView titleTextView = (TextView)findViewById(R.id.title_tv) ; titleTextView.setText("new Title");

那么使用findViewById(R.id.my_foot_parent_id)為什么會報空指針呢? 我們來分析它的源碼看看吧。對于布局文件的解析,最終都會調用到LayoutInflater的inflate方法,該方法最終又會調用rInflate方法,我們看看這個方法。

inflate方法中關鍵代碼

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 {
    // Temp is the root view that was found in the xml
    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);
        }
        // Create layout params that match root, if supplied
        params = root.generateLayoutParams(attrs);
        if (!attachToRoot) {
            // Set the layout params for temp if we are not
            // attaching. (If we are, we use addView, below)
            temp.setLayoutParams(params);
        }
    }

rInflate方法關鍵代碼

void rInflate(XmlPullParser parser, View parent, Context context,
        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_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)) {
            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);
        }
    }

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

這個方法其實就是遍歷xml中的所有元素,然后挨個進行解析。例如解析到一個標簽,那么就根據用戶設置的一些layout_width、layout_height、id等屬性來構造一個TextView對象,然后添加到父控件(ViewGroup類型)中。標簽也是一樣的,我們看到遇到include標簽時,會調用parseInclude函數,這就是對標簽的解析,我們看看吧。

private void parseInclude(XmlPullParser parser, Context context, View parent,
        AttributeSet attrs) throws XmlPullParserException, IOException {
    int type;

    if (parent instanceof ViewGroup) {
        // Apply a theme wrapper, if requested. This is sort of a weird
        // edge case, since developers think the <include> overwrites
        // values in the AttributeSet of the included View. So, if the
        // included View has a theme attribute, we'll need to ignore it.
        final TypedArray ta = context.obtainStyledAttributes(attrs, ATTRS_THEME);
        final int themeResId = ta.getResourceId(0, 0);
        final boolean hasThemeOverride = themeResId != 0;
        if (hasThemeOverride) {
            context = new ContextThemeWrapper(context, themeResId);
        }
        ta.recycle();

        // If the layout is pointing to a theme attribute, we have to
        // massage the value to get a resource identifier out of it.
        int layout = attrs.getAttributeResourceValue(null, ATTR_LAYOUT, 0);
        if (layout == 0) {
            final String value = attrs.getAttributeValue(null, ATTR_LAYOUT);
            if (value == null || value.length() <= 0) {
                throw new InflateException("You must specify a layout in the"
                        + " include tag: <include layout=\"@layout/layoutID\" />");
            }

            // Attempt to resolve the "?attr/name" string to an identifier.
            layout = context.getResources().getIdentifier(value.substring(1), null, null);
        }

        // The layout might be referencing a theme attribute.
        if (mTempValue == null) {
            mTempValue = new TypedValue();
        }
        if (layout != 0 && context.getTheme().resolveAttribute(layout, mTempValue, true)) {
            layout = mTempValue.resourceId;
        }

        if (layout == 0) {// include標簽中沒有設置layout屬性,會拋出異常
            final String value = attrs.getAttributeValue(null, ATTR_LAYOUT);
            throw new InflateException("You must specify a valid layout "
                    + "reference. The layout ID " + value + " is not valid.");
        } else {
            final XmlResourceParser childParser = context.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!");
                }
         // 1、解析include中的第一個元素 
                final String childName = childParser.getName();
         // 如果第一個元素是merge標簽,那么調用rInflate函數解析 
                if (TAG_MERGE.equals(childName)) {
                    // The <merge> tag doesn't support android:theme, so
                    // nothing special to do here.
// 2、我們例子中的情況會走到這一步,首先根據include的屬性集創建被include進來的xml布局的根view // 這里的根view對應為my_foot_layout.xml中的RelativeLayout 
                    rInflate(childParser, parent, context, childAttrs, false);
                } else {
                    final View view = createViewFromTag(parent, childName,
                            context, childAttrs, hasThemeOverride);
                    final ViewGroup group = (ViewGroup) parent;

                    final TypedArray a = context.obtainStyledAttributes(
                            attrs, R.styleable.Include);
                    final int id = a.getResourceId(R.styleable.Include_id, View.NO_ID);
                    final int visibility = a.getInt(R.styleable.Include_visibility, -1);
                    a.recycle();

                    // We try to load the layout params set in the <include /> tag.
                    // If the parent can't generate layout params (ex. missing width
                    // or height for the framework ViewGroups, though this is not
                    // necessarily true of all ViewGroups) then we expect it to throw
                    // a runtime exception.
                    // 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 {
            // 獲3、取布局屬性 
                        params = group.generateLayoutParams(attrs);
                    } catch (RuntimeException e) {
                        // Ignore, just fail over to child attrs.
                    }
                    if (params == null) {
                        params = group.generateLayoutParams(childAttrs);
                    }
                    view.setLayoutParams(params);

                    // Inflate all children.解析所有子控件
                    rInflateChildren(childParser, view, childAttrs, true);
// 5、將include中設置的id設置給根view,因此實際上my_foot_layout.xml中的RelativeLayout的id會變成include標簽中的id,include不設置id,那么也可以通過relative的找到. 
                    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;
                    }

                    group.addView(view);
                }
            } finally {
                childParser.close();
            }
        }
    } else {
        throw new InflateException("<include /> can only be used inside of a ViewGroup");
    }

    LayoutInflater.consumeChildElements(parser);
}

整個過程就是根據不同的標簽解析不同的元素,首先會解析include元素,然后再解析被include進來的布局的root view元素。在我們的例子中對應的root view就是id為my_foot_parent_id的RelativeLayout,然后再解析root view下面的所有元素,這個過程是從上面注釋的2~4的過程,然后是設置布局參數。我們注意看注釋5處,這里就解釋了為什么include標簽和被引入的布局的根元素都設置了id的情況下,通過被引入的根元素的id來查找子控件會找不到的情況。我們看到,注釋5處的會判斷include標簽的id如果不是View.NO_ID的話會把該id設置給被引入的布局根元素的id,即此時在我們的例子中被引入的id為my_foot_parent_id的根元素RelativeLayout的id被設置成了include標簽中的id,即RelativeLayout的id被動態修改成了”my_foot_ly”。因此此時我們再通過“my_foot_parent_id”這個id來查找根元素就會找不到了!

所以結論就是: 如果include中設置了id,那么就通過include的id來查找被include布局根元素的View;如果include中沒有設置Id, 而被include的布局的根元素設置了id,那么通過該根元素的id來查找該view即可。拿到根元素后查找其子控件都是一樣的。

(2) <viewstub> 標簽

viewstub標簽同include標簽一樣可以用來引入一個外部布局,不同的是,viewstub引入的布局默認不會擴張,即既不會占用顯示也不會占用位置,從而在解析layout時節省cpu和內存。

viewstub常用來引入那些默認不會顯示,只在特殊情況下顯示的布局,如進度布局、網絡失敗顯示的刷新布局、信息出錯出現的提示布局等。

下面以在一個布局main.xml中加入網絡錯誤時的提示頁面network_error.xml為例。main.mxl代碼如下:

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

……
    <ViewStub
        android:id="@+id/network_error_layout"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:layout="@layout/network_error" />

</RelativeLayout>

其中network_error.xml為只有在網絡錯誤時才需要顯示的布局,默認不會被解析,示例代碼如下:

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

    <Button
        android:id="@+id/network_setting"
        android:layout_width="@dimen/dp_160"
        android:layout_height="wrap_content"
        android:layout_centerHorizontal="true"
        android:text="@string/network_setting" />

    <Button
        android:id="@+id/network_refresh"
        android:layout_width="@dimen/dp_160"
        android:layout_height="wrap_content"
        android:layout_below="@+id/network_setting"
        android:layout_centerHorizontal="true"
        android:layout_marginTop="@dimen/dp_10"
        android:text="@string/network_refresh" />

</RelativeLayout>

在java中通過(ViewStub)findViewById(id)找到ViewStub,通過stub.inflate()展開ViewStub,然后得到子View,如下:

private View networkErrorView;

private void showNetError() {
// not repeated infalte
if (networkErrorView != null) {
networkErrorView.setVisibility(View.VISIBLE);
return;
}

ViewStub stub = (ViewStub)findViewById(R.id.network_error_layout);
if(stub !=null){

networkErrorView = stub.inflate();
Button networkSetting = (Button)networkErrorView.findViewById(R.id.network_setting);
Button refresh = (Button)findViewById(R.id.network_refresh);
}
}

private void showNormal() {
if (networkErrorView != null) {
networkErrorView.setVisibility(View.GONE);
}
}

在上面showNetError()中展開了ViewStub,同時我們對networkErrorView進行了保存,這樣下次不用繼續inflate。這就是后面第三部分提到的減少不必要的infalte。

注意這里我對ViewStub的實例進行了一個非空判斷,這是因為ViewStub在XML中定義的id只在一開始有效,一旦ViewStub中指定的布局加載之后,這個id也就失敗了,那么此時findViewById()得到的值也會是空

viewstub標簽大部分屬性同include標簽類似。

上面展開ViewStub部分代碼

ViewStub stub = (ViewStub)findViewById(R.id.network_error_layout);
networkErrorView = stub.inflate();

也可以寫成下面的形式

View viewStub = findViewById(R.id.network_error_layout);
viewStub.setVisibility(View.VISIBLE);   // ViewStub被展開后的布局所替換
networkErrorView =  findViewById(R.id.network_error_layout); // 獲取展開后的布局

注意:

View 的可見性設置為 gone 后,在inflate 時,這個View 及其子View依然會被解析的。使用ViewStub就能避免解析其中指定的布局文件,從而節省布局文件的解析時間,及內存的占用。另外需要提醒大家一點,ViewStub所加載的布局是不可以使用 <merge> 標簽的

(3) <merge> 標簽 在使用了include后可能導致布局嵌套過多,多余不必要的layout節點,從而導致解析變慢,不必要的節點和嵌套可通過hierarchy viewer(下面布局調優工具中有具體介紹)或設置->開發者選項->顯示布局邊界查看。

merge標簽可用于兩種典型情況:

a. 布局頂結點是FrameLayout且不需要設置background或padding等屬性,可以用merge代替,因為Activity內容試圖的parent view就是個FrameLayout,所以可以用merge消除只剩一個。

b. 某布局作為子布局被其他布局include時,使用merge當作該布局的頂節點,這樣在被引入時頂結點會自動被忽略,而將其子節點全部合并到主布局中。

以(1) 標簽的示例為例,用hierarchy viewer查看main.xml布局如下圖:

可以發現多了一層沒必要的RelativeLayout,將foot.xml中RelativeLayout改為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" >

    <Button
        android:id="@+id/button"
        android:layout_width="match_parent"
        android:layout_height="@dimen/dp_40"
        android:layout_above="@+id/text"/>

    <TextView
        android:id="@+id/text"
        android:layout_width="match_parent"
        android:layout_height="@dimen/dp_40"
        android:layout_alignParentBottom="true"
        android:text="@string/app_name" />

</merge>

運行后再次用hierarchy viewer查看main.xml布局如下圖:

這樣就不會有多余的RelativeLayout節點了。

去除不必要的嵌套和View節點

(1) 首次不需要使用的節點設置為GONE或使用viewstub

(2) 使用RelativeLayout代替LinearLayout

大約在Android4.0之前,新建工程的默認main.xml中頂節點是LinearLayout,而在之后已經改為RelativeLayout,因為RelativeLayout性能更優,且可以簡單實現LinearLayout嵌套才能實現的布局。

4.0及以上Android版本可通過設置->開發者選項->顯示布局邊界打開頁面布局顯示,看看是否有不必要的節點和嵌套。4.0以下版本可通過hierarchy viewer查看。

減少不必要的infalte

(1)對于inflate的布局可以直接緩存,用全部變量代替局部變量,避免下次需再次inflate

如上面ViewStub示例中的

if (networkErrorView != null) {
networkErrorView.setVisibility(View.VISIBLE);
return;
}

布局調優工具

(1) hierarchy viewer

hierarchy viewer可以方便的查看Activity的布局,各個View的屬性、measure、layout、draw的時間,如果耗時較多會用紅色標記,否則顯示綠色。

Hierarchy Viewer是隨Android SDK發布的工具,位于Android SDK/tools/hierarchyviewer.bat (Windows操作系統,mac上顯示的為hierarchyviewer),使用起來也是超級簡單,通過此工具可以詳細的理解當前界面的控件布局以及某個控件的屬性(name、id、height等)。

1)連接設備真機或者模擬器

2)啟動你要觀察的應用。

3)打開Hierarchyviewer,點擊hierarchyviewer文件即可。

4)雙擊最上面的,如下圖的 <Focused Window> ,這個是當前窗口,加載完畢后會顯示當前界面層次結構。

5)觀察層次結構圖,這個圖有點大,可以拖動。View Hierarchy窗口顯示了Activity的所有View對象,選中某個View還可以查看View的具體信息,最好選擇工具中的Show Extras選項。

View Hierarcy 同時能幫助你識別渲染性能比較低的部分。View節點中帶有紅色或黃色的點代表速度較慢的View對象。如單步運行應用程序那樣,你可以這樣來判斷某個View 速度一直很慢,還是只在某個特定環境下速度才慢。

請注意,低性能并不表示一定有問題,特別像是ViewGroup對象,View的子節點越多,結構越復雜,性能越差。

View Hierarchy 窗口還可以幫助你找到性能問題。只要看每個View節點的性能指標(顏色點)就可以,你可以看到測量(布局或繪制)最慢的View對象是哪個,這樣你就能快速確定,要優先察看哪個問題。

(2)Lint先來段developer的官方引用:

Android Studio provides a code scanning tool called Lint that can help you to easily identify and correct problems with the structural quality of your code, without having to execute the app or write any test cases.

該圖詮釋了Lint工具是如何檢測應用源代碼的:

Android Lint是Google提供給Android開發者的靜態代碼檢查工具。使用Lint對Android工程代碼進行掃描和檢查,可以發現代碼潛在的問題,提醒程序員及早修正。

Android Lint使用Lint簡要來說,有以下的作用:

布局性能(以前是 layoutopt工具,可以解決無用布局、嵌套太多、布局太多)

未使用到資源

不一致的數組大小

國際化問題(硬編碼)

圖標的問題(重復的圖標,錯誤的大小)

可用性問題(如不指定的文本字段的輸入型)

manifest文件的錯誤

內存泄露 — 如:handle的不當使用 。

占內存的資源及時回收 — 如:cursor未關閉等

Analyze”菜單中選擇“Inspect Code”,其中可以選擇scope,即檢測范圍,也可以選擇不同的檢測配置,我們先進行默認的配置檢測吧。檢測需要一定的時間,結果會在EventLog顯示:

可以看到,我們的項目有很多問題,比如我選擇了Correctness中的Using dp instead of sp for text sizes屬性,發現應用中有2處在textSize中誤用了dp,其給出了類的具體位置和解決方案。可能你覺得這些無關大雅,那么可以查看Probable bugs項,在其中找到一項 String comparison using ‘==’,instead of ‘equals()’,可以看到SecurityBankCardListActivity類中的有一行代碼:

this.mBankCard.getCardId() == mBankCard.getCardId()//cardId為String類型

在此就不一一列舉。

可能你會覺得Lint分析的太過詳細,我無法迅速找到問題,那么你可以點擊

,其分為四類,我們應只關注前2類。

AS的Lint配置

打開設置對話框,找到Editor,然后是Inspections,選擇某一個Lint選項,修改嚴重等級,如圖:

最后貼一下Lint檢查的常見類型:

最后貼一下Lint檢查的常見類型:

1.Correctness:Messeges

(1)字符串國際化不完全

(2)國際化的字符串,在默認位置(default locale),沒有定義

2.Correctness

(1)Xml中view的id重名

(2)代碼中使用的某些API高于Manifest中的Min SDK

(3)字符串國際化中,同一名字的的String-Array對應的item值不相同 (4)Activity沒有注冊到Manifest

(5)使用已經廢棄的api

(6)避免使用px,使用dp

(7)添加不需要的權限

3.Performance

(1) 避免在繪制或者解析布局(draw/layout)時,分配對象。eg,Ondraw()中實例化Paint().

(2)Layout中無用的參數。

(3)可優化的布局:如一個線性布局(一個Imageview和一個TextView),可被TextView和一個Compound Drawable代替。

(4)可優化的代碼:如SparseArray可代替一個Interger2Object的Hashmap

(5)優化layout,比如如果子view都是wrap_content,則設置android:baselineAligned為false,則When set to false, prevents the layout from aligning its children’s baselines.

(6)使用FloatMath代替Math,執行sin()和ceil(),以避免float的兩次轉換。

(7)Nested weight (內外均有weight)將拖累執行效果

(8)未被使用的資源

(9)Overdraw 即指定theme的activity會自己繪制背景,但是布局中會再一次設置背景

(10)View或view的父親沒有用

4.Security

(1)設置setJavascriptEnable將導致腳本攻擊漏洞(XSS vulnerabilities)

5.Usability:Icons

(1) 圖片尺寸在轉換成不同dpi時,存在不能整除的問題,比如2*24px

(2)顯示有些base 和browser的資源名不同,但圖片內容完全相同。

6.Usability

(1)自定義view缺少默認的構造方法

7.Usability:Typography

(1)特殊字符需用編碼代替,如“_”需要用“–”

8.Accessibility

(1)ImageView缺少src內容

檢查Overdraw

Overdraw(過度繪制)描述的是屏幕上的某個像素在同一幀的時間內被繪制了多次。在多層次重疊的UI結構里面,如果不可見的UI也在做繪制的操作,會導致某些像素區域被繪制了多次。這樣就會浪費大量的CPU以及GPU資源。

手機原本為了保持視覺的流暢度,其屏幕刷新頻率是60hz,即在1000/60=16.67ms內更新一幀。如果沒有完成任務,就會發生掉幀的現象,也就是我們所說的卡頓。

debug GPU overdraw

在Android系統內部也有一個神器可以查看app的UI的過度繪制情況,在開發者選項中有個debug GPU overdraw(調試GPU過度繪制),打開之后有off(關閉),show overdraw areas(顯示過度繪制區域),show areas for Deuteranomaly(為紅綠癥患者顯示過度繪制區域)

我們選擇show overdraw areas,發現整個手機界面的顏色變了,在打開過度繪制選項后,其中的藍色,淡綠,淡紅,深紅代表了4種不同程度的Overdraw情況,我們的目標就是盡量減少紅色Overdraw,看到更多的藍色區域。

Profile GPU rendering

其次android系統還內置了Profile GPU rendering工具,這個工具也是在開發者選項中打開,它能夠以柱狀圖的方式顯示當前界面的渲染時間

藍色代表測量繪制的時間,或者說它代表需要多長時間去創建和更新你的DisplayList.在Android中,一個視圖在可以實際的進行渲染之前,它必須被轉換成GPU所熟悉的格式,簡單來說就是幾條繪圖命令,復雜點的可能是你的自定義的View嵌入了自定義的Path. 一旦完成,結果會作為一個DisplayList對象被系統送入緩存,藍色就是記錄了需要花費多長時間在屏幕上更新視圖(說白了就是執行每一個View的onDraw方法,創建或者更新每一個View的Display List對象).

橙色部分表示的是處理時間,或者說是CPU告訴GPU渲染一幀的地方,這是一個阻塞調用,因為CPU會一直等待GPU發出接到命令的回復,如果柱狀圖很高,那就意味著你給GPU太多的工作,太多的負責視圖需要OpenGL命令去繪制和處理.

紅色代表執行的時間,這部分是Android進行2D渲染 Display List的時間,為了繪制到屏幕上,Android需要使用OpenGl ES的API接口來繪制Display List.這些API有效地將數據發送到GPU,最總在屏幕上顯示出來.

下面我們通過一個小demo來實踐一下

剛打開這個項目,我們就發現了在第一個有過度繪制問題,效果如下

存在問題

在按鈕overdraw上面就有個紅色的過度繪制區域

在文本框This is test的布局中也是紅色過度繪制區域

解決方法

要解決這個問題,我們首先需要分析這是怎么引起的。分析到activity_main.xml的布局文件時,發現這里使用了多個嵌套的LinearLayout布局,而且每個LinearLayout都會使用一次android:background設置一次自己的背景顏色,他們造成了過度繪制。

仔細分析在其中一個嵌套ImageView的LinearLayout布局背景顏色與最外層的背景顏色是一樣的,屬于不需要的背景色,因此將這個LinearLayout中的android:background屬性刪除,這時發現文本框布局已經不再是紅色了

咋看之下一切都很完美,但其實整個ui其實還含有一個隱含的繪制效果,那邊是在activity中,使用setContentView(R.layout.activity_main)設置布局的時候,android會自動填充一個默認的背景,而在這個UI中,我們使用了填充整個app的背景,因此不需要默認背景,取消也很簡單,只需要在activity中的onCreate方法中添加這么一句就行了

現在看最終優化效果

OVERDRAWVIEW頁面的問題

在overdrawviewactivity中只有一個自定義的圖案,而這個自定義的圖案引起了過度繪制的問題

解決方法

首先這個也是填充了整個ui界面的繪制圖片,因此我們也在activity中的onCreate方法中添加getWindow().setBackgroundDrawable(null);取消默認繪制。

繼續研究,發現過度繪制問題是由于OverDrawView類中的ondraw方法中多次繪制了矩形導致的,代碼如下:

@Override
protected void onDraw(Canvas canvas) {
  super.onDraw(canvas);
  int width = getWidth();
  int height = getHeight();
  mPaint.setColor(Color.GRAY);
  canvas.drawRect(0, 0, width, height, mPaint);
  mPaint.setColor(Color.CYAN);
  canvas.drawRect(0, height/4, width, height, mPaint);
  mPaint.setColor(Color.DKGRAY);
  canvas.drawRect(0, height/3, width, height, mPaint);
  mPaint.setColor(Color.LTGRAY);
  canvas.drawRect(0, height/2, width, height, mPaint);
}

通過分析得知,顏色為GRAY的矩形的高度其實不需要設置為整個屏幕的高度,它的高度只需要設置為它所顯示范圍的高度就可以了,因此可以設為height/4。

其他的矩形也是同樣的道理,因此更改這里的代碼為:

@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
     int width = getWidth();
      int height = getHeight();
    mPaint.setColor(Color.GRAY);
      canvas.drawRect(0, 0, width, height/4, mPaint);
    mPaint.setColor(Color.CYAN);
      canvas.drawRect(0, height/4, width, height/3, mPaint);
     mPaint.setColor(Color.DKGRAY);
      canvas.drawRect(0, height/3, width, height/2, mPaint);
      mPaint.setColor(Color.LTGRAY);
    canvas.drawRect(0, height/2, width, height, mPaint);
}

優化的界面

至此,布局優化的內容就到此結束了,有不足的地方,歡迎大家評論指出

 

來自:http://blog.csdn.net/u012124438/article/details/54564659

 

 本文由用戶 mghc5548 自行上傳分享,僅供網友學習交流。所有權歸原作者,若您的權利被侵害,請聯系管理員。
 轉載本站原創文章,請注明出處,并保留原始鏈接、圖片水印。
 本站是一個以用戶分享為主的開源技術平臺,歡迎各類分享!