Android性能優(yōu)化(二)之布局優(yōu)化面面觀

一、初識布局優(yōu)化

通過《Android性能優(yōu)化(一)之啟動加速35%》我們獲得了閃電般的App啟動速度,那么在應用啟動完畢之后,UI布局也會對App的性能產(chǎn)生比較大的影響,如果布局寫得糟糕,顯而易見App的表現(xiàn)不可能流暢。

那么本文我同樣基于實際案例,針對應用的布局進行優(yōu)化進而提升App性能。

二、60fps VS 16ms

根據(jù)Google官方出品的Android性能優(yōu)化典范60幀每秒是目前最合適的圖像顯示速度,事實上絕大多數(shù)的Android設備也是按照每秒60幀來刷新的。為了讓屏幕的刷新幀率達到60fps,我們需要確保在時間16ms(1000/60Hz)內(nèi)完成單次刷新的操作(包括measure、layout以及draw),這也是Android系統(tǒng)每隔16ms就會發(fā)出一次VSYNC信號觸發(fā)對UI進行渲染的原因。

如果整個過程在16ms內(nèi)順利完成則可以展示出流暢的畫面;然而由于任何原因?qū)е陆邮盏絍SYNC信號的時候無法完成本次刷新操作,就會產(chǎn)生掉幀的現(xiàn)象,刷新幀率自然也就跟著下降(假定刷新幀率由正常的60fps降到30fps,用戶就會明顯感知到卡頓)。

Drop Frame Occur

作為開發(fā)人員,我們的目標只有一個:保證穩(wěn)定的幀率來避免卡頓。

三、Avoid Overdraw

理論上一個像素每次只繪制一次是最優(yōu)的,但是由于重疊的布局導致一些像素會被多次繪制,Overdraw由此產(chǎn)生。

我們可以通過調(diào)試工具來檢測Overdraw:設置——開發(fā)者選項——調(diào)試GPU過度繪制——顯示過度繪制區(qū)域。

overdraw

原色 – 沒有過度繪制 – 這部分的像素點只在屏幕上繪制了一次。
藍色 – 1次過度繪制– 這部分的像素點只在屏幕上繪制了兩次。
綠色 – 2次過度繪制 – 這部分的像素點只在屏幕上繪制了三次。
粉色 – 3次過度繪制 – 這部分的像素點只在屏幕上繪制了四次。
紅色 – 4次過度繪制 – 這部分的像素點只在屏幕上繪制了五次。

在實際項目中,一般認為藍色即是可以接受的顏色。

我們來看一個簡單卻隱藏了很多問題的界面,App的設置界面。在沒有優(yōu)化之前打開Overdraw調(diào)試,可以看到界面大多數(shù)是嚴重的紅色:見下圖。

設置界面初始

貼出這個布局的代碼

<?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="match_parent"
    android:background="#F1F0F0"
    android:orientation="vertical">

    <LinearLayout
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:layout_marginTop="10dp"
        android:background="@color/white"
        android:orientation="vertical">

        <RelativeLayout
            android:id="@+id/update_phone"
            android:layout_width="match_parent"
            android:layout_height="50dp"
            android:background="@color/white"
            android:paddingLeft="10dp"
            android:paddingRight="10dp">

            <TextView
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:layout_centerVertical="true"
                android:background="@color/white"
                android:text="修改手機號"
                android:textColor="#FF555555"
                android:textSize="14sp" />

            <ImageView
                android:id="@+id/update_phone_iv"
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:layout_alignParentRight="true"
                android:layout_centerVertical="true"
                android:src="@drawable/arrow_right" />

            <ImageView
                android:id="@+id/update_phone_dot"
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:layout_centerVertical="true"
                android:layout_marginRight="10dp"
                android:layout_toLeftOf="@id/update_phone_iv"
                android:src="@drawable/message_logo_red" />
        </RelativeLayout>

        <View
            android:layout_width="match_parent"
            android:layout_height="0.5dp"
            android:layout_marginLeft="10dp"
            android:background="#FFDDDDDD" />

        <RelativeLayout
            android:id="@+id/setting_lv_forgetPassword"
            android:layout_width="match_parent"
            android:layout_height="50dp"
            android:background="@color/white"
            android:paddingLeft="10dp"
            android:paddingRight="10dp">

            <TextView
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:layout_centerVertical="true"
                android:background="@color/white"
                android:text="找回密碼"
                android:textColor="#FF555555"
                android:textSize="14sp" />

            <ImageView
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:layout_alignParentRight="true"
                android:layout_centerVertical="true"
                android:src="@drawable/arrow_right" />
        </RelativeLayout>
    </LinearLayout>

    <LinearLayout
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:layout_marginTop="10dp"
        android:background="@color/white"
        android:orientation="vertical">

        <RelativeLayout
            android:id="@+id/privacy_setting"
            android:layout_width="match_parent"
            android:layout_height="50dp"
            android:background="@color/white"
            android:paddingLeft="10dp"
            android:paddingRight="10dp">

            <TextView
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:layout_centerVertical="true"
                android:background="@color/white"
                android:text="隱私設置"
                android:textColor="#FF555555"
                android:textSize="14sp" />

            <ImageView
                android:id="@+id/privacy_setting_iv"
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:layout_alignParentRight="true"
                android:layout_centerVertical="true"
                android:src="@drawable/arrow_right" />

            <ImageView
                android:id="@+id/privacy_setting_dot"
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:layout_centerVertical="true"
                android:layout_marginRight="10dp"
                android:layout_toLeftOf="@id/privacy_setting_iv"
                android:src="@drawable/message_logo_red" />
        </RelativeLayout>

        <View
            android:layout_width="match_parent"
            android:layout_height="0.5dp"
            android:layout_marginLeft="10dp"
            android:background="#FFDDDDDD" />

        <RelativeLayout
            android:id="@+id/setting_lv_messageSetting"
            android:layout_width="match_parent"
            android:layout_height="50dp"
            android:background="@color/white"
            android:paddingLeft="10dp"
            android:paddingRight="10dp">

            <TextView
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:layout_centerVertical="true"
                android:background="@color/white"
                android:text="@string/accountSetting_messageSetting"
                android:textColor="#FF555555"
                android:textSize="14sp" />

            <CheckBox
                android:id="@+id/setting_checkbox_c_messageSetting"
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:layout_alignParentRight="true"
                android:layout_centerVertical="true"
                android:checked="true" />
        </RelativeLayout>

    </LinearLayout>

    <LinearLayout
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:layout_marginTop="10dp"
        android:background="@color/white"
        android:orientation="vertical">

        <RelativeLayout
            android:id="@+id/setting_lv_feedback_m"
            android:layout_width="match_parent"
            android:layout_height="50dp"
            android:background="@color/white"
            android:paddingLeft="10dp"
            android:paddingRight="10dp">

            <TextView
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:layout_centerVertical="true"
                android:background="@color/white"
                android:text="@string/accountSetting_feedback"
                android:textColor="#FF555555"
                android:textSize="14sp" />

            <ImageView
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:layout_alignParentRight="true"
                android:layout_centerVertical="true"
                android:src="@drawable/arrow_right" />
        </RelativeLayout>

        <View
            android:layout_width="match_parent"
            android:layout_height="0.5dp"
            android:layout_marginLeft="10dp"
            android:background="#FFDDDDDD" />

        <RelativeLayout
            android:id="@+id/setting_lv_score"
            android:layout_width="match_parent"
            android:layout_height="50dp"
            android:background="@color/white"
            android:paddingLeft="10dp"
            android:paddingRight="10dp">

            <TextView
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:layout_centerVertical="true"
                android:background="@color/white"
                android:text="@string/accountSetting_score"
                android:textColor="#FF555555"
                android:textSize="14sp" />

            <ImageView
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:layout_alignParentRight="true"
                android:layout_centerVertical="true"
                android:src="@drawable/arrow_right" />
        </RelativeLayout>

        <View
            android:layout_width="match_parent"
            android:layout_height="0.5dp"
            android:layout_marginLeft="10dp"
            android:background="#FFDDDDDD" />

        <RelativeLayout
            android:id="@+id/setting_lv_aboutus"
            android:layout_width="match_parent"
            android:layout_height="50dp"
            android:background="@color/white"
            android:paddingLeft="10dp"
            android:paddingRight="10dp">

            <TextView
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:layout_centerVertical="true"
                android:background="@color/white"
                android:text="@string/about_us"
                android:textColor="#FF555555"
                android:textSize="14sp" />

            <ImageView
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:layout_alignParentRight="true"
                android:layout_centerVertical="true"
                android:src="@drawable/arrow_right" />
        </RelativeLayout>

    </LinearLayout>

    <LinearLayout
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:layout_marginTop="10dp"
        android:background="@color/white"
        android:orientation="vertical">

        <RelativeLayout
            android:id="@+id/setting_lv_changeStatus"
            android:layout_width="match_parent"
            android:layout_height="50dp"
            android:background="@color/white"
            android:paddingLeft="10dp"
            android:paddingRight="10dp">

            <TextView
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:layout_centerVertical="true"
                android:background="@color/white"
                android:text="我要招人"
                android:textColor="#FF555555"
                android:textSize="14sp" />

            <ImageView
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:layout_alignParentRight="true"
                android:layout_centerVertical="true"
                android:src="@drawable/arrow_right" />
        </RelativeLayout>
    </LinearLayout>

    <Button
        android:id="@+id/setting_btn_exitLogin"
        android:layout_width="match_parent"
        android:layout_height="50dp"
        android:layout_marginTop="30dp"
        android:background="@color/white"
        android:gravity="center"
        android:text="@string/me_exitbtn"
        android:textColor="#FFFF5A5A"
        android:textSize="16sp" />

</LinearLayout>

分析布局可知:多層布局重復設置了背景色導致Overdraw。
那么我們結(jié)合產(chǎn)品的需求(任何不結(jié)合具體場景優(yōu)化都是耍流氓):

  • 去掉每行RelativeLayout的背景色;
  • 去掉每行TextView的背景色;

備注:一個容易忽略的點是我們的Activity使用的Theme可能會默認的加上背景色,不需要的情況下可以去掉。

去掉背景色之后再看一下Overdraw;

設置界面優(yōu)化后

對比一下優(yōu)化后的布局的顏色,可以看出Overdraw降到了可以接受的程度。

備注:有些過度繪制都是不可避免的,需要結(jié)合具體的布局場景具體分析。

四、減少嵌套層次及控件個數(shù)

  • Android的布局文件的加載是LayoutInflater利用pull解析方式來解析,然后根據(jù)節(jié)點名通過反射的方式創(chuàng)建出View對象實例;
  • 同時嵌套子View的位置受父View的影響,類如RelativeLayout、LinearLayout等經(jīng)常需要measure兩次才能完成,而嵌套、相互嵌套、深層嵌套等的發(fā)生會使measure次數(shù)呈指數(shù)級增長,所費時間呈線性增長;

由此得到結(jié)論:那么隨著控件數(shù)量越多、布局嵌套層次越深,展開布局花費的時間幾乎是線性增長,性能也就越差。

幸運的是,我們有Hierarchy Viewer這個方便可視化的工具,可以得到:樹形結(jié)構(gòu)總覽、布局view、每一個View(包含子View)繪制所花費的時間及View總個數(shù)

備注: Hierarchy Viewer不能連接真機的問題可以通過ViewServer這個庫解決;

設置界面初始狀態(tài)
設置界面初始狀態(tài)View個數(shù)及繪制時間

使用Hierarchy Viewer來看查看一下設置界面,可以從下圖中得到設置界面的一些數(shù)據(jù)及存在的問題:

  • 嵌套共計7層(僅setContentView設置的布局),布局嵌套過深;
  • measure時間1.569ms,layout時間0.120ms,draw時間16.128ms,合計共計耗時17.871ms;
  • 共繪制85個View,5個多余定位,以及若干個無用布局。

優(yōu)化方案:

  • 將之前使用RelativeLayout來做的可以替換的行換為TextView;
  • 去掉之前多余的無用布局;

現(xiàn)在我們再使用Hierarchy Viewer來檢測一下:

優(yōu)化之后的布局層次
優(yōu)化之后的View個數(shù)及繪制時間

優(yōu)化后:
1. 控件數(shù)量從85個減少到26個,減少69%;
2. 繪制時間從17.8ms減少到14.756ms,降低17%;

總結(jié):
1. 同樣的UI效果可以使用不同的布局來完成,我們需要考慮使用少的嵌套層次以及控件個數(shù)來完成,例如設置界面的普通一行,可以像之前一樣使用RelativeLayout嵌套TextView以及ImageView來實現(xiàn),但是明顯只使用TextView來做:嵌套層次、控件個數(shù)都更少。
2. 優(yōu)化過程中使用低端手機更易發(fā)現(xiàn)瓶頸;

五、Profiling GPU Rendering

根據(jù)Android性能優(yōu)化典范,打開設備的GPU配置渲染工具——》在屏幕上顯示為條形圖,可以協(xié)助我們定位UI渲染問題。

GPU呈現(xiàn)模式分析

從Android M版本開始,GPU Profiling工具把渲染操作拆解成如下8個詳細的步驟進行顯示。

渲染八步驟
  1. Swap Buffers:表示處理任務的時間,也可以說是CPU等待GPU完成任務的時間,線條越高,表示GPU做的事情越多;
  1. Command Issue:表示執(zhí)行任務的時間,這部分主要是Android進行2D渲染顯示列表的時間,為了將內(nèi)容繪制到屏幕上,Android需要使用Open GL ES的API接口來繪制顯示列表,紅色線條越高表示需要繪制的視圖更多;
  2. Sync & Upload:表示的是準備當前界面上有待繪制的圖片所耗費的時間,為了減少該段區(qū)域的執(zhí)行時間,我們可以減少屏幕上的圖片數(shù)量或者是縮小圖片的大小;
  3. Draw:表示測量和繪制視圖列表所需要的時間,藍色線條越高表示每一幀需要更新很多視圖,或者View的onDraw方法中做了耗時操作;
  4. Measure/Layout:表示布局的onMeasure與onLayout所花費的時間,一旦時間過長,就需要仔細檢查自己的布局是不是存在嚴重的性能問題;
  5. Animation:表示計算執(zhí)行動畫所需要花費的時間,包含的動畫有ObjectAnimator,ViewPropertyAnimator,Transition等等。一旦這里的執(zhí)行時間過長,就需要檢查是不是使用了非官方的動畫工具或者是檢查動畫執(zhí)行的過程中是不是觸發(fā)了讀寫操作等等;
  6. Input Handling:表示系統(tǒng)處理輸入事件所耗費的時間,粗略等于對事件處理方法所執(zhí)行的時間。一旦執(zhí)行時間過長,意味著在處理用戶的輸入事件的地方執(zhí)行了復雜的操作;
  7. Misc Time/Vsync Delay:表示在主線程執(zhí)行了太多的任務,導致UI渲染跟不上vSync的信號而出現(xiàn)掉幀的情況;出現(xiàn)該線條的時候,可以在Log中看到這樣的日志:

備注:GPU配置渲染工具雖然可以定位出問題發(fā)生在某個步驟,但是并不能定位到具體的某一行;當我們定位到某個步驟之后可以使用工具TraceView進行更加詳細的定位。TraceView的使用可以參照《Android性能優(yōu)化(一)之啟動加速35%》

六、Use Tags

merge標簽

merge可以用來合并布局,減少布局的層級。merge多用于替換頂層FrameLayout或者include布局時,用于消除因為引用布局導致的多余嵌套。
例如:需要顯示一個Button,布局如下;

<?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="match_parent"
    android:orientation="vertical">

    <Button
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="Merge標簽演示" />
</LinearLayout>

我們通過UiAutoMatorViewer(無需root,相比Hierarchy Viewer只能查看布局層次,不能得到繪制時間)看一下布局的層次

頂級視圖下多了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">

    <Button
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="Merge標簽演示" />
</merge>

再看下布局的層次:

使用Merge之后少了LinearLayout嵌套

可以看到使用Merge標簽進行優(yōu)化之后布局嵌套就少了一層,Button作為父視圖第三層FrameLayout的直接子視圖。

注意:merge標簽常用于減少布局嵌套層次,但是只能用于根布局。

ViewStub標簽

推遲創(chuàng)建對象、延遲初始化,不僅可以提高性能,也可以節(jié)省內(nèi)存(初始化對象不被創(chuàng)建)。Android定義了ViewStub類,ViewStub是輕量級且不可見的視圖,它沒有大小,沒有繪制功能,也不參與measure和layout,資源消耗非常低。
1、

    <ViewStub
        android:id="@+id/mask"
        android:layout="@layout/b_me_mask"
        android:layout_width="match_parent"
        android:layout_height="match_parent" />
ViewStub viewStub = (ViewStub)view.findViewById(R.id.mask);
viewStub.inflate();

App里常見的視圖如蒙層、小紅點,以及網(wǎng)絡錯誤、沒有數(shù)據(jù)等公共視圖,使用頻率并不高,如果每一次都參與繪制其實是浪費資源的,都可以借助ViewStub標簽進行延遲初始化,僅當使用時才去初始化。

include標簽

include標簽和布局性能關系不大,主要用于布局重用,一般和merge標簽配合使用,因和本文主題關聯(lián)不大,此處不展開討論。

七、其它

  1. 自定義控件時,注意在onDraw不能進行復雜運算;以及對待三方UI庫選擇高性能;
  2. 內(nèi)存對布局的影響:如同Misc Time/Vsync Delay步驟產(chǎn)生的影響,在之后內(nèi)存優(yōu)化的篇章詳細講。

八、總結(jié)

布局優(yōu)化的通用套路

  1. 調(diào)試GPU過度繪制,將Overdraw降低到合理范圍內(nèi);
  2. 減少嵌套層次及控件個數(shù),保持view的樹形結(jié)構(gòu)盡量扁平(使用Hierarchy Viewer可以方便的查看),同時移除所有不需要渲染的view;
  3. 使用GPU配置渲染工具,定位出問題發(fā)生在具體哪個步驟,使用TraceView精準定位代碼;
  4. 使用標簽,Merge減少嵌套層次、ViewStub延遲初始化。

經(jīng)過這幾步的優(yōu)化之后,一般就不會再有布局的性能問題,同時還是要強調(diào):優(yōu)化是一個長期的工作,同時也必須結(jié)合具體場景:有取有舍!

參考:Android性能優(yōu)化典范

歡迎關注微信公眾號:定期分享Java、Android干貨!

歡迎關注
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內(nèi)容