Android 效能優化(二)之佈局優化面面觀

頭條祁同偉發表於2017-02-17

一、初識佈局優化

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

那麼本文我同樣基於實際案例,針對應用的佈局進行優化進而提升App效能。

二、60fps VS 16ms

根據Google官方出品的Android效能優化典範60幀每秒是目前最合適的影象顯示速度,事實上絕大多數的Android裝置也是按照每秒60幀來重新整理的。為了讓螢幕的重新整理幀率達到60fps,我們需要確保在時間16ms(1000/60Hz)內完成單次重新整理的操作(包括measure、layout以及draw),這也是Android系統每隔16ms就會發出一次VSYNC訊號觸發對UI進行渲染的原因。

如果整個過程在16ms內順利完成則可以展示出流暢的畫面;然而由於任何原因導致接收到VSYNC訊號的時候無法完成本次重新整理操作,就會產生掉幀的現象,重新整理幀率自然也就跟著下降(假定重新整理幀率由正常的60fps降到30fps,使用者就會明顯感知到卡頓)。

Android 效能優化(二)之佈局優化面面觀
Drop Frame Occur

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

三、Avoid Overdraw

理論上一個畫素每次只繪製一次是最優的,但是由於重疊的佈局導致一些畫素會被多次繪製,Overdraw由此產生。

我們可以通過除錯工具來檢測Overdraw:設定——開發者選項——除錯GPU過度繪製——顯示過度繪製區域。

Android 效能優化(二)之佈局優化面面觀
overdraw

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

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

我們來看一個簡單卻隱藏了很多問題的介面,App的設定介面。在沒有優化之前開啟Overdraw除錯,可以看到介面大多數是嚴重的紅色:見下圖。

Android 效能優化(二)之佈局優化面面觀
設定介面初始

貼出這個佈局的程式碼

<?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。
那麼我們結合產品的需求(任何不結合具體場景優化都是耍流氓):

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

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

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

Android 效能優化(二)之佈局優化面面觀
設定介面優化後

對比一下優化後的佈局的顏色,可以看出Overdraw降到了可以接受的程度。

備註:有些過度繪製都是不可避免的,需要結合具體的佈局場景具體分析。

四、減少巢狀層次及控制元件個數

  • Android的佈局檔案的載入是LayoutInflater利用pull解析方式來解析,然後根據節點名通過反射的方式建立出View物件例項;
  • 同時巢狀子View的位置受父View的影響,類如RelativeLayout、LinearLayout等經常需要measure兩次才能完成,而巢狀、相互巢狀、深層巢狀等的發生會使measure次數呈指數級增長,所費時間呈線性增長;

由此得到結論:那麼隨著控制元件數量越多、佈局巢狀層次越深,展開佈局花費的時間幾乎是線性增長,效能也就越差。

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

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

Android 效能優化(二)之佈局優化面面觀
設定介面初始狀態

Android 效能優化(二)之佈局優化面面觀
設定介面初始狀態View個數及繪製時間

使用Hierarchy Viewer來看檢視一下設定介面,可以從下圖中得到設定介面的一些資料及存在的問題:

  • 巢狀共計7層(僅setContentView設定的佈局),佈局巢狀過深;
  • measure時間1.569ms,layout時間0.120ms,draw時間16.128ms,合計共計耗時17.871ms;
  • 共繪製85個View,5個多餘定位,以及若干個無用佈局。

優化方案:

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

現在我們再使用Hierarchy Viewer來檢測一下:

Android 效能優化(二)之佈局優化面面觀
優化之後的佈局層次

Android 效能優化(二)之佈局優化面面觀
優化之後的View個數及繪製時間

優化後:
1. 控制元件數量從85個減少到26個,減少69%;
2. 繪製時間從17.8ms減少到14.756ms,降低17%;

總結:
1. 同樣的UI效果可以使用不同的佈局來完成,我們需要考慮使用少的巢狀層次以及控制元件個數來完成,例如設定介面的普通一行,可以像之前一樣使用RelativeLayout巢狀TextView以及ImageView來實現,但是明顯只使用TextView來做:巢狀層次、控制元件個數都更少。
2. 優化過程中使用低端手機更易發現瓶頸;

五、Profiling GPU Rendering

根據Android效能優化典範,開啟裝置的GPU配置渲染工具——》在螢幕上顯示為條形圖,可以協助我們定位UI渲染問題。

Android 效能優化(二)之佈局優化面面觀
GPU呈現模式分析

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

Android 效能優化(二)之佈局優化面面觀
渲染八步驟

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

Android 效能優化(二)之佈局優化面面觀

備註:GPU配置渲染工具雖然可以定位出問題發生在某個步驟,但是並不能定位到具體的某一行;當我們定位到某個步驟之後可以使用工具TraceView進行更加詳細的定位。TraceView的使用可以參照《Android效能優化(一)之啟動加速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只能檢視佈局層次,不能得到繪製時間)看一下佈局的層次

Android 效能優化(二)之佈局優化面面觀
頂級檢視下多了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>複製程式碼

再看下佈局的層次:

Android 效能優化(二)之佈局優化面面觀
使用Merge之後少了LinearLayout巢狀

可以看到使用Merge標籤進行優化之後佈局巢狀就少了一層,Button作為父檢視第三層FrameLayout的直接子檢視。

注意:merge標籤常用於減少佈局巢狀層次,但是隻能用於根佈局。

ViewStub標籤

推遲建立物件、延遲初始化,不僅可以提高效能,也可以節省記憶體(初始化物件不被建立)。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裡常見的檢視如蒙層、小紅點,以及網路錯誤、沒有資料等公共檢視,使用頻率並不高,如果每一次都參與繪製其實是浪費資源的,都可以藉助ViewStub標籤進行延遲初始化,僅當使用時才去初始化。

include標籤

include標籤和佈局效能關係不大,主要用於佈局重用,一般和merge標籤配合使用,因和本文主題關聯不大,此處不展開討論。

七、其它

  1. 自定義控制元件時,注意在onDraw不能進行復雜運算;以及對待三方UI庫選擇高效能;
  2. 記憶體對佈局的影響:如同Misc Time/Vsync Delay步驟產生的影響,在之後記憶體優化的篇章詳細講。

八、總結

佈局優化的通用套路

  1. 除錯GPU過度繪製,將Overdraw降低到合理範圍內;
  2. 減少巢狀層次及控制元件個數,保持view的樹形結構儘量扁平(使用Hierarchy Viewer可以方便的檢視),同時移除所有不需要渲染的view;
  3. 使用GPU配置渲染工具,定位出問題發生在具體哪個步驟,使用TraceView精準定位程式碼;
  4. 使用標籤,Merge減少巢狀層次、ViewStub延遲初始化。

經過這幾步的優化之後,一般就不會再有佈局的效能問題,同時還是要強調:優化是一個長期的工作,同時也必須結合具體場景:有取有舍!

參考:Android效能優化典範

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

Android 效能優化(二)之佈局優化面面觀
歡迎關注

相關文章