Android APP性能優化之 ---- 佈局優化(一)

佈局優化的核心思想是優化佈局嵌套層級(層級越少,View繪製時越快)

一、Android系統屏幕UI刷新機制

首先需要明白一個概念,如果我們想要屏幕流暢的運行,就必須保證UI全部的測量、佈局和繪製的時間在16ms內 爲什麼是16ms? 因爲人眼與大腦之間的協作無法感知超過60fps的畫面更新,而16ms也就是每秒刷新60fps 16ms=1000/60Hz,也就是說超過16ms用戶就會感知到卡頓.

熟悉屏幕UI刷新機制,首先需要了解下刷新率和幀率:

刷新率(Refresh Rate): 指屏幕在一秒內刷新屏幕的次數,例如60HZ

幀率(Frame Rate): 指GPU在一秒內操作畫面的幀數,例如30fps,60fps

在一個典型的顯示系統中,一般包括CPU、GPU、display(可以理解爲屏幕或顯示器)三個部分,CPU負責計算數據,把計算好數據交給GPU,GPU會對圖形數據進行渲染,渲染好後放到buffer裏存起來,然後display負責把buffer裏的數據呈現到屏幕上.

顯示過程簡單的說就是 CPU/GPU準備好數據,存入buffer,display每隔一段時間去buffer裏取數據,然後顯示出來.但是刷新頻率和幀率並不是總能保持相同的節奏.

針對上述情況,Android系統中引入了VSYNC的機制(Vertical Synchronization 垂直同步,我們可以理解爲幀同步),
它爲了保證GPU生成幀的速度和display刷新的速度保持一致,Android系統會每16ms就會發出一次VSYNC信號觸發UI渲染更新.
VSYNC最重要的作用是防止出現畫面撕裂(screen tearing).所謂畫面撕裂 是指一個畫面上出現了兩幀畫面的內容(如下圖).
爲什麼會出現這種情況呢?這種情況一般是因爲顯卡輸出幀的速度高於顯示器的刷新速度,導致顯示器並不能及時處理輸出的幀,
而最終出現了多個幀的畫面都留在了顯示器上的問題.

畫面撕裂

畫面撕裂也就是幀率超過刷新率情況(圖一):

我們看下圖一,在沒有VSync的情況下屏幕刷新第二幀畫面的時候 GPU生成了2 3兩個畫面 導致畫面撕裂現象,如果引入VSYNC機制後,要求繪製只能在收到VSYNC信號之後才能進行,因此杜絕了GPU一直不停的進行繪製,幀的生成速度高於屏幕的刷新速度,導致生成的幀不能被正常顯示,只能丟棄,這樣就出現了丟幀的情況

其實android設備更多的情況是幀率小於刷新頻率情況(圖二):

我們看下圖二,GPU生成幀的速度從60fps突然掉到60fps以下,就會出現某些幀顯示的畫面內容就會與上一幀的畫面相同.這樣一來,用戶在兩個16ms看到的是同一幀畫面,因此會給用戶卡頓不流暢的感受. 圖一: 更多的情況是GPU生成的幀率小於刷新頻率情況(圖二):

幀率從60fps突然掉到60fps以下,就會出現某些幀顯示的畫面內容就會與上一幀的畫面相同.因此給用戶卡頓不流暢的感受.

二、佈局的選擇

首先FrameLayout能實現的優先使用FrameLayout,因爲Framelayout是最簡單高效的ViewGroup(爲什麼它是最高效的呢?最簡單的辦法就是我們可以通過查看它源碼的行數,FrameLayout,源代碼行數是最少的,代碼邏輯也是最簡單的)

其次優先選擇RelativeLayout,因爲RelativeLayout可以簡單實現LinearLayout嵌套才能實現的佈局(等下還會介紹一個ConstraintLayout,它可以在不嵌套佈局的情況下更簡單的完成更多複雜的佈局)

最後是當在RelativeLayout和LinearLayout在不嵌套情況下同時能夠滿足需求時,優先選擇LinearLayout,因爲RelativeLayout功能相對複雜,同時會有重複繪製的情況

什麼是重複繪製?
重複測量視圖並不一定是因爲錯誤。RelativeLayout就需要經常對它的子視圖測量兩次,以確
保所有子視圖被放置在了正確的位置。如果 LinearLayout 的子視圖設置了 layout weight屬
性,那麼 LinearLayou也需要測量兩次以確定子視圖的確切尺寸。如果是嵌套的LinearLayout
或者是RelativeLayout,測量的次數會呈指數增長(兩層嵌套會進行4次測量,3層嵌套會進行
8 次測量,等等)。
  • 避免過度繪製(Overdraw)

Overdraw: 指屏幕上某一個像素點在同一幀的時間內被繪製了多次.

在多層次的UI結構中,如果不可見的UI也在做繪製的操作,就會導致某些像素區域被繪製了多次,浪費大量的CPU以及GPU資源,我們可以在手機的設置—->開發者選項—->打開"調試GPU過度繪製" 查看Overdraw過度繪製情況.

如圖 通過4種顏色展示不同程度的Overdraw的情況:

暗紅: overdraw 4倍.像素繪製了五次或者更多.必須得優化了
淡紅: overdraw 3倍.像素繪製了四次.小範圍是可以接受,可以試着去優化.
綠色: overdraw 2倍.像素繪製了三次.中等大小的綠色區域是可以接受的
藍色: overdraw 1倍.像素繪製了兩次.大片的藍色是可以接受的
沒有顏色: 沒有overdraw.像素只繪製了一次

舉一個導致overdraw的場景: 當我們代碼中ViewPager和ViewPager中的fragment都設置了背景色,這樣會導致背景色在同一個像素點上重複繪製,而針對這種情況,我們可以去掉ViewPager的背景色來避免overdraw

  • ConstraintLayout

ConstraintLayout是Android Studio 2.2中主要的新增功能之一,它可以在不嵌套任何佈局的情況下構建複雜的佈局.它與RelativeLayout非常相似,所有的view都依賴於相鄰控件的相對關係.而ConstraintLayout比RelativeLayout更加靈活,在AndroidStudio中進行拖拽即可完成佈局.

以往 我們都是通過嵌套或者使用RelativeLayout來完成複雜的佈局,但是通過使用Systrace大量測試表明:嵌套式層次結構和 RelativeLayout(會對其每個子對象重複測量兩次)的特性導致性能低下.因此,針對複雜的佈局,我們毫無疑問優先選擇ConstraintLayout.

ConstraintLayout用法介紹及ConstraintLayout和RelativeLayout測試對比性能優勢

三、優化控件的使用

  • include標籤

include標籤可以在一個佈局中引入另外一個佈局.如果我們程序的所有界面都有一個公共的部分,這個時候最好的做法就是將這個公共的部分提取到一個獨立的佈局文件中,然後在每個界面的佈局文件中來引用這個公共的佈局

作用: 爲了提高代碼的複用性,減少代碼;將佈局中公共部分抽取供其他layout使用,但可能會導致多餘的佈局嵌套

用法如下:

    <!-- 1.定義公共部分佈局: include_layout.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">
        <TextView
            android:id="@+id/back"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_alignParentLeft="true"
            android:layout_centerVertical="true"
            android:text="Back" />
        <TextView
            android:id="@+id/title"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_centerInParent="true"
            android:text="Title"
            android:textSize="20sp" />
        <TextView
            android:id="@+id/done"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_alignParentRight="true"
            android:layout_centerVertical="true"
            android:text="Finish" />
    </RelativeLayout>

    <!-- 2.使用include_layout.xml -->
    <?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">
    
        <!--在<include>標籤中,我們也可以覆蓋layout中定義的所有屬性-->
        <include
            layout="@layout/include_layout"
            android:layout_width="match_parent"
            android:layout_height="wrap_content" />
    
        <TextView
            style="@style/textStyle"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:text="HAHA" />
    </LinearLayout>

    <!--定義style-->
    <style name="textStyle">
        <item name="android:layout_width">match_parent</item>
        <item name="android:layout_height">wrap_content</item>
        <item name="android:textSize">18sp</item>
        <item name="android:textColor">@android:color/black</item>
        <item name="android:gravity">center_vertical</item>
    </style>
 

如上代碼針對style的抽取,同樣也提高代碼的複用性

除了抽取公共部分代碼外,include還可以將我們佈局代碼進行模塊化,也就是當我們頁面邏輯非常複雜,單純佈局代碼就一千多行的時候,不管是誰來維護這樣的代碼,看着就會頭疼,這個時候我們可以將佈局進行分類,使用include標籤抽取分成不同模塊來引入,模塊化後的代碼更便於提高維護的效率

  • merge標籤

merge標籤是include標籤的輔助擴展,爲了防止在引用佈局文件時產生多餘的佈局嵌套

作用: 解決佈局層級的優化,減少佈局嵌套的層次,提高佈局加載的效率

用法如下:

    <!--定義merge_layout.xml-->
    <?xml version="1.0" encoding="utf-8"?>
    <merge xmlns:android="http://schemas.android.com/apk/res/android">
        <!--根標籤必須是merge-->
        <Button
            android:id="@+id/ok"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:layout_gravity="center_horizontal"
            android:text="OK" />
    
        <Button
            android:id="@+id/cancel"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:layout_gravity="center_horizontal"
            android:text="Cancel" />
    
    </merge>
    
    <!--使用merge_layout.xml-->
    <?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">
        <!--merge標籤使用的是父佈局的特性
        (也就是這裏是垂直的LinearLayout,merge中兩個button也就是垂直的LinearLayout屬性)-->
        <include layout="@layout/merge_layout" />
    
    </LinearLayout>

使用場景爲:當父佈局和子佈局相同時,可以利用merge標籤來減少一層佈局嵌套,merge標籤使用的是父佈局的特性

  • ViewStub標籤

ViewStub只有加載該佈局的時候才佔用資源,INVISIBLE狀態是不會繪製出來的(ViewStub雖說也是View的一種,但是它沒有大小,沒有繪製功能,也不參與佈局,資源消耗非常低,將它放置在佈局當中基本可以認爲是完全不會影響性能的)

用法如下:

    <!--viewstub_layout.xml-->
    <?xml version="1.0" encoding="utf-8"?>
    <FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
        android:layout_width="match_parent"
        android:layout_height="match_parent">
    
        <Button
            android:id="@+id/more"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_gravity="center_horizontal"
            android:text="Button2" />
    </FrameLayout>

    <!--使用ViewStub-->
    <?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:id="@+id/button1"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_gravity="center_horizontal"
            android:text="Button1" />
    
        <ViewStub
            android:id="@+id/view_stub"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:layout="@layout/viewstub_layout" />
    </LinearLayout>

四、原生View控件的優化:

android系統提供的一些原生控件並不是完美的,或者說結合我們實際複雜的開發環境來說,壓根就不會有絕對完美的控件,爲了提高我們使用控件的效率進而提升程序的質量,因此我們需要針對原生控件做一些局部優化.

  • ListView的優化

複用convertView(重用View可以減少重新分配緩存 避免內存頻繁分配/回收),

使用ViewHolder(原因是findViewById方法耗時較大,如果控件個數過多,會嚴重影響性能,而使用ViewHolder主要是爲了可以省去這個時間.通過setTag,getTag直接獲取這個View),

以及數據多的情況下進行分批加載等等

  • WebView的優化

在當下的Android開發中,Webview的身影隨處可見,尤其是在Hybrid App(混合模式移動應用)中,更是不可或缺,而Webview的性能卻是有待改善的(手機QQ中大概有70%以上的業務都是由H5開發).

Hybrid App(混合模式移動應用)是指介於web-app、native-app這兩者之間的app,兼具“Native App良好用戶交互體驗的優勢”和“Web App跨平臺開發的優勢”。

全局WebView(混合模式移動應用開發中,在客戶端剛啓動時,就初始化一個全局的WebView待用,並隱藏;當用戶訪問了WebView時,直接使用這個WebView加載對應網頁,並展示.這種方法可以比較有效的減少WebView在App中的首次打開時間,當用戶訪問頁面時,不需要等待初始化WebView的時間)

客戶端代理數據請求(在客戶端初始化WebView的同時,直接由native開始網絡請求數據;當頁面初始化完成後,向native獲取其代理請求的數據.這個方法雖然不能減小WebView初始化時間,但數據請求和WebView初始化可以並行進行,這樣總體的頁面加載時間就縮短了,這種方式是參考騰訊所分享的在手機QQ混合開發的做法)

優化網頁加載速度(先設置WebView禁止加載圖片,再覆寫WebViewClient的onPageFinished()方法,頁面加載結束後再加載圖片)

還有其他各種優化的方式,不再一一列舉,總結起來都是圍繞兩點:

1.在使用前預先初始化好WebView,從而減小耗時
2.在初始化的同時,通過Native來完成一些網絡請求等過程,使得WebView初始化不是完全的阻塞後續過程

摘選自美團技術團隊 -- WebView性能優化總結:

一個加載網頁的過程中,native、網絡、後端處理、CPU都會參與,各自都有必要的工作和依賴關係;讓他們相互並行處理而不是相互阻塞纔可以讓網頁加載更快:
    WebView初始化慢,可以在初始化同時先請求數據,讓後端和網絡不要閒着。
    後端處理慢,可以讓服務器分trunk輸出,在後端計算的同時前端也加載網絡靜態資源。
    腳本執行慢,就讓腳本在最後運行,不阻塞頁面解析。
    同時,合理的預加載、預緩存可以讓加載速度的瓶頸更小。
    WebView初始化慢,就隨時初始化好一個WebView待用。
    DNS和鏈接慢,想辦法複用客戶端使用的域名和鏈接。
    腳本執行慢,可以把框架代碼拆分出來,在請求頁面之前就執行好。
  • ViewPager的延遲加載

ViewPager有一個 “預加載”的機制,默認會把ViewPager當前位置的左右相鄰頁面預先初始化(預加載),這樣設計是爲了ViewPager左右滑動會更加流暢,但如果當前APP頁面數量不多,並且每個頁面資源佔用很大,用戶可能只在一個頁面使用,不需要切換頁面,這時,我們就沒必要使用預加載來消耗手機的資源了

怎樣去做到延遲加載呢?
    setOffscreenPageLimit(int limit)用來設置ViewPager預加載的數量,默認是1,
    1也就是會預加載左右相鄰頁面,所以我們設置的值應該小於1,但事實上設置小於1是不生效的.因此這種方式行不通.
    ViewPager中,預加載數量值的變量爲DEFAULT_OFFSCREEN_PAGES,這個值是private,因此子類繼承ViewPager也是行不通.

以下通過兩種方式來實現延遲加載:

1.從Fragment着手,只有Fragment可見的時候纔去加載數據

2.自定義一個ViewPager,把原生ViewPager代碼全拷過來,修改加載數變量DEFAULT_OFFSCREEN_PAGES值爲0; (我們這裏使用的是低版本ViewPager代碼,Android 4.0的代碼,低版本的代碼相對更少,邏輯相對更簡單)

    // 方式一:
public class LazyFragment extends Fragment {

    private boolean mIsInit;

    @Override
    public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
        mIsInit = true; // View 控件的初始化
        isLoadData();   // 這裏還需滿足條件1: 視圖對用戶可見
        return super.onCreateView(inflater, container, savedInstanceState);
    }

    /**
     * 這個方法是通知Fragment的UI是否可見,當參數isVisibleToUser爲true的時候,fragment的UI是可見的,爲false的時候爲不可見
     */
    @Override
    public void setUserVisibleHint(boolean isVisibleToUser) {
        super.setUserVisibleHint(isVisibleToUser);
        isLoadData();   // 還需滿足條件2: 視圖已經初始化
    }

    private void isLoadData() {
        if (getUserVisibleHint()) {
            if (mIsInit) {
                // 滿足以上兩個條件才加載數據
            }
        }
    }
}
    // 方式二:
https://github.com/ansen360/Sample/blob/master/app/src/main/java/com/ansen/sample/LazyViewPager.java

4. 其他優化點:

  • 刪除控件中無用屬性

  • 減少不必要的infalte

  • 佈局上的優化。移除 XML 中非必須的背景,移除 Window 默認的背景、按需顯示佔位背景圖片

  • 自定義View優化.避免onDraw方法聲明太多變量,使用canvas.clipRect()來幫助系統識別那些可見的區域,只有在這個區域內纔會被繪製


相關鏈接直達:

Android APP性能優化之 ---- 佈局優化(一)

Android APP性能優化之 ---- 內存優化(二)

Android APP性能優化之 ---- 代碼優化(三)

Android APP性能優化之 ---- 優化監測工具(四)

Android APP性能優化之 ---- APK瘦身 App啓動優化

Android內存泄露OOM異常處理優化

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章