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

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

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

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

二、60fps VS 16ms

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

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

Drop Frame Occur

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

三、Avoid Overdraw

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

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

overdraw

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

在實(shí)際項(xiàng)目中,一般認(rèn)為藍(lán)色即是可以接受的顏色。

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

設(shè)置界面初始

貼出這個(gè)布局的代碼

<?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="修改手機(jī)號(hào)"
                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="隱私設(shè)置"
                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>

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

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

備注:一個(gè)容易忽略的點(diǎn)是我們的Activity使用的Theme可能會(huì)默認(rèn)的加上背景色,不需要的情況下可以去掉。

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

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

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

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

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

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

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

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

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

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

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

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

優(yōu)化方案:

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

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

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

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

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

五、Profiling GPU Rendering

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

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

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

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

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

六、Use Tags

merge標(biāo)簽

merge可以用來合并布局,減少布局的層級(jí)。merge多用于替換頂層FrameLayout或者include布局時(shí),用于消除因?yàn)橐貌季謱?dǎo)致的多余嵌套。
例如:需要顯示一個(gè)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標(biāo)簽演示" />
</LinearLayout>

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

頂級(jí)視圖下多了LinearLayout

我們使用Merge標(biāo)簽對代碼進(jìn)行修改;

<?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標(biāo)簽演示" />
</merge>

再看下布局的層次:

使用Merge之后少了LinearLayout嵌套

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

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

ViewStub標(biāo)簽

推遲創(chuàng)建對象、延遲初始化,不僅可以提高性能,也可以節(jié)省內(nèi)存(初始化對象不被創(chuàng)建)。Android定義了ViewStub類,ViewStub是輕量級(jí)且不可見的視圖,它沒有大小,沒有繪制功能,也不參與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里常見的視圖如蒙層、小紅點(diǎn),以及網(wǎng)絡(luò)錯(cuò)誤、沒有數(shù)據(jù)等公共視圖,使用頻率并不高,如果每一次都參與繪制其實(shí)是浪費(fèi)資源的,都可以借助ViewStub標(biāo)簽進(jìn)行延遲初始化,僅當(dāng)使用時(shí)才去初始化。

include標(biāo)簽

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

七、其它

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

八、總結(jié)

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

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

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

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

歡迎關(guān)注微信公眾號(hào):定期分享Java、Android干貨!

歡迎關(guān)注
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容