Android 性能優化之界面優化

前言

Android的性能優化是一個持續持續的過程,以發現問題、解決問題或者是組織Code Review爲推動力去實施。性能優化涉及到的方面很多,比如啓動優化、卡頓優化、內存優化、界面佈局優化、穩定性優化、耗電優化、安裝包大小優化等等。性能優化是每個開發者都需要關注的功課,本文從界面佈局優化做一個總結。

在查閱大量相關資料後,對界面優化的在此做個總結。本文會介紹一下卡頓產生原因、什麼是過度繪製和渲染機制,然後介紹如何定位問題和解決問題,最後會總結出在實際開發過程中的使用建議。如有不足之處,歡迎提出寶貴建議。

一、卡頓原因

一個App的用戶體驗好不好,是否流暢不卡頓是一個很直觀的感受。導致Android卡頓場景的原因有很多,比如界面繪製、應用啓動、頁面跳轉、事件響應等等。

這幾種卡頓場景的根本原因可以分爲兩大類:

  • 界面繪製。主要原因是繪製的層級深、頁面複雜、刷新不合理,由於這些原因導致卡頓的場景更多出現在 UI 和啓動後的初始界面以及跳轉到頁面的繪製上。
  • 數據處理。導致這種卡頓場景的原因是數據處理量太大,一般分爲三種情況,一是數據在處理 UI 線程,二是數據處理佔用 CPU 高,導致主線程拿不到時間片,三是內存增加導致 GC 頻繁,從而引起卡頓。

引起卡頓的原因很多,但不管怎麼樣的原因和場景,最終都是通過設備屏幕上顯示來達到用戶,歸根到底就是顯示有問題,所以,要解決卡頓,就要先了解 Android 系統的顯示原理。

二、Android 系統的顯示原理

Android 顯示過程可以簡單概括爲:Android 應用程序把經過測量、佈局、繪製後的 surface 緩存數據,通過 SurfaceFlinger把數據渲染到顯示屏幕上, 通過 Android 的刷新機制來刷新數據。也就是說應用層負責繪製,系統層負責渲染,通過進程間通信把應用層需要繪製的數據傳遞到系統層服務,系統層服務通過刷新機制把數據更新到屏幕上。

我們都知道在 Android 的每個 View 繪製中有三個核心步驟:MeasureLayoutDraw。具體實現是從 ViewRootImp類的performTraversals() 方法開始執行,MeasureLayout都是通過遞歸來獲取 View的大小和位置,並且以深度作爲優先級,可以看出層級越深、元素越多、耗時也就越長。

三、什麼是渲染機制

渲染操作通常依賴於兩個核心組件:CPU與GPU。CPU負責包括Measure,Layout,Record,Execute的計算操作,GPU負責Rasterization(柵格化)操作。CPU通常存在的問題的原因是存在非必需的視圖組件,它不僅僅會帶來重複的計算操作,而且還會佔用額外的GPU資源。
渲染問題

從上圖可以得出結論:

  • CPU產生的問題:不必要的佈局和失效(Layouts、Invalidations)
  • GPU產生的問題:過度繪製(overdraw)

小結: 瞭解渲染機制,有助於我們瞭解卡頓的最終原因,方便找到解決問題的方向。

爲了能夠使得App流暢,我們需要在每幀16ms以內處理完所有的CPU與GPU的計算,繪製,渲染等等操作。

接下來介紹什麼是“過度繪製”,避免了過度繪製,界面優化也就做到了。

四、什麼是過度繪製(定位問題)

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

在這裏插入圖片描述

在Android開發人員選項中,找到“調試GPU過度繪製”,開啓顯示之後,手機會顯示出藍色、綠色、紅色等色塊。
在這裏插入圖片描述

其中藍色、綠色、紅色顯示的就是過度繪製的區域。

在官網的 Debug GPU Overdraw Walkthrough 說明中對過度重繪做了簡單的介紹,其中屏幕上顯示不同色塊的具體含義如下所示:

overdraw

每個顏色的說明如下:

  • 原色:沒有過度繪製(正常的繪製,只繪製了1次)
  • 藍色:1 次過度繪製 (超過1次繪製是會顯示)
  • 綠色:2 次過度繪製
  • 粉色:3 次過度繪製
  • 紅色:4 次及以上過度繪製

我們優化的目標是,減少紅色,看到更多的藍色。

五、優化方法(解決問題)

優化原則:減少佈局層級、減少過度繪製、佈局複用

下面結合項目中的實際使用情況做的優化,同時也在Code Review後發現的做一個總結,Code Review時結合Android studio中的工具檢測到的一些佈局優化建議提示。(注:記一次CodeReview實例,請點擊前往)

1.移除默認的 Window 背景

一般應用默認繼承的主題都會有一個默認的 windowBackground ,比如默認的AppTheme主題:

   <style name="AppTheme" parent="Theme.AppCompat.Light.NoActionBar">
        <!--TODO:移除主題背景色,減少一次界面過度繪製-->
        <item name="android:windowBackground">@android:color/white</item>
    </style>

但是一般界面都會自己設置界面的背景顏色或者列表頁則由 item 的背景來決定,所以默認的 Window 背景基本用不上,如果不移除就會導致所有界面都多 1 次繪製。

可以在應用的主題中添加如下的一行屬性來移除默認的 Window 背景:

<item name="android:windowBackground">@android:color/transparent</item>
<!-- 或者 -->
<item name="android:windowBackground">@null</item>

或者在 BaseActivityonCreate() 方法中使用下面的代碼移除:

getWindow().setBackgroundDrawable(null);
// 或者
getWindow().setBackgroundDrawableResource(android.R.color.transparent);

移除默認的 Window 背景的工作在項目初期做最好,因爲有可能有的界面未設置背景色,這就會導致該界面顯示成黑色的背景,如下圖所示,如果是後期移除的,就需要檢查移除默認 Window 背景之後的界面是否顯示正常。

在這裏插入圖片描述
在這裏插入圖片描述
原先的系統主題是白色,移除後就變爲黑色,此時渲染的顏色也變了,減少一次過度繪製。只是這時需要在子佈局中添加相應的背景色即可。

2.移除XML佈局文件中不必需的背景

例如在佈局文件中嵌套了RecyclerView,注意在item中需要用到背景色時再考慮添加背景色,這樣可以減少一次過度繪製。簡單的佈局出現顏色上出現了過度繪製,可以先好排查是否在xml中或者代碼中調用了多餘的繪製。

3.自定義控件使用 clipRect()quickReject() 優化

當某些控件不可見時,如果還繼續繪製更新該控件,就會導致過度繪製。但是通過 Canvas clipRect() 方法可以設置需要繪製的區域,當某個控件或者 View 的部分區域不可見時,就可以減少過度繪製。

先看一下 clipRect() 方法的說明:

Intersect the current clip with the specified rectangle, which is expressed in local coordinates.

顧名思義就是給 Canvas 設置一個裁剪區,只有在這個裁剪矩形區域內的纔會被繪製,區域之外的都不繪製。

這個項目中暫時沒有用到,先記錄於此,後期用到了再完善使用注意細節。

可以參考一下其他人總結的使用方式:Android性能優化之渲染篇

4.使用合理高效的佈局

(1)RecyclerView中的item的分割線處理

一般的寫法如下:

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:paddingBottom="8dp"
    android:background="@color/divider_gray">

    <LinearLayout
        android:padding="@dimen/mid_dp"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:orientation="horizontal"
        android:background="@color/white">

        <ImageView
            android:id="@+id/iv_app_icon"
            android:layout_width="40dp"
            android:layout_height="40dp"
            tools:src="@mipmap/ic_launcher"/>

        <TextView
            android:id="@+id/tv_app_label"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_gravity="center_vertical"
            android:layout_marginLeft="@dimen/mid_dp"
            android:textColor="@color/text_gray_main"
            android:textSize="@dimen/mid_sp"
            tools:text="test"/>
    </LinearLayout>

</LinearLayout>

這種改變佈局實現分割線的方式雖然很快捷方便,但是存在不少問題的:

(1)加深了佈局層級,和之前的佈局相比多了一級

在這裏插入圖片描述

(2)多了 2 次過度繪製

解決方式有兩種:

  1. 一種是使用 RelativeLayout 將分割線添加在 item 的佈局中,但是這樣會導致佈局複雜度增加,同時因爲 RelativeLayout 佈局的兩次測量,也會延長 View 測量的時間,在解決這種需求時並不是一個好的方式。
  2. 另一種是使用 RecyclerViewaddItemDecoration(ItemDecoration decor) 方法添加分割線,這種方式在你自定義好一個分割線 ItemDecoration 時是很方便的,網上有很多關於這方面的例子(如果你使用 ListView 的話,則使用 setDivider(Drawable divider) 方法)。

我們採用第二種解決方法,優化前後的對比如下:

在這裏插入圖片描述

優化後的佈局 ImageView 和 item 背景區域均比優化前少了 2 次過度重繪,佈局層級也沒增加,需求也實現了。

(2)使用TextView本身的屬性同時顯示圖片和文字
<TextView
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="@string/batman"
        android:drawableLeft="@drawable/batman"
        android:drawableStart="@drawable/batman"
        android:drawablePadding="5dp">
</TextView>

在界面中有圖片和文字的佈局時,不一定要用LinearLayout或RelativeLayout來嵌套,直接用TextVeiw也能實現,這樣減少一層嵌套,也更優雅。

(3)用LinearLayout自帶的分割線

分割線在App經常會用到的,使用頻率高到讓你驚訝。但是LinearLayout有一個屬性可以幫你添加分割線。下面的例子中,LinearLayout包含2個TextView和基於他們中間的分割線。

LinearLayout自帶的分割線
1.創建分割線(用shape實現)

下面是一個簡單的shape divider_horizontal.xml用來當做分割線。

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

    <size android:width="@dimen/divider_width"/>
    <solid android:color="@color/colorPrimaryDark"/>

</shape>

2.將分割線放到佈局屬性divider

<?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="wrap_content"
        android:orientation="horizontal"
        android:divider="@drawable/divider_horizontal"  //添加分割線圖片
        android:dividerPadding="5dp"  //設置padding
        android:showDividers="middle"> //居中顯示


    <TextView android:layout_width="0dp"
              android:layout_weight="1"
              android:layout_height="wrap_content"
              android:gravity="center"
              android:text="@string/batman"/>

    <TextView android:layout_width="0dp"
              android:layout_height="wrap_content"
              android:layout_weight="1"
              android:gravity="center"
              android:text="@string/superman"/>
</LinearLayout>

上面用到了三個xml屬性:

  • divider -用來定義一個drawable或者color作爲分割線

  • showDividers -設置分隔線的顯示位置,有四個flag,分別是:begining(開始位置),end(結束位置),middle(中間,最常見的),none(不顯示,也是默認值)

  • dividerPadding -給divider添加padding

    注:RadioGroup繼續自LinearLayout,同時也具有上述屬性。

    <RadioGroup  
    	android:id="@+id/rgSize"  
    	android:layout_width="wrap_content"  
        android:layout_height="wrap_content"  
    	android:divider="@drawable/shape_space"  
    	android:showDividers="middle"  
    	android:orientation="horizontal" >  
    </RadioGroup>  
    

    shape_space.xml

    <?xml version="1.0" encoding="utf-8"?>  
    <shape xmlns:android="http://schemas.android.com/apk/res/android" android:shape="rectangle">  
      
        <solid android:color="@android:color/transparent" />  
      
        <stroke  
            android:width="0dp"  
            android:color="@android:color/transparent" />  
      
        <size  
            android:height="8dp"  
            android:width="8dp" />  
      
    </shape> 
    

    實現效果爲:
    RadioGroup

六、總結

最後總結一下在實際開發中 ,在寫佈局界面時的建議:

  1. 使用合適的佈局

    三種常見的ViewGroup的繪製速度:FrameLayout> LinerLayout> RelativeLayout

    • ConstraintLayout是一個更高性能的消滅佈局層級的神器

    • RelativeLayout會讓子View調用2次onMeasureLinearLayout 在有weight時,也會調用子View2次onMeasure

    • RelativeLayout的子View如果高度和RelativeLayout不同,則會引發效率問題,當子View很複雜時,這個問題會更加嚴重。如果可以,儘量使用padding代替margin。

    • 在不影響層級深度的情況下,使用LinearLayoutFrameLayout而不是RelativeLayout

    • RecycleView中item 一般用ConstraintLayout或直接使用控件來佈局,以業務需求爲準。

    • 簡單佈局一般用FrameLayout來佈局,同時結合include、merge來使用。佈局文件都要有根節點,但android中的佈局嵌套過多會造成性能問題,於是在使用include嵌套的時候我們可以使用merge作爲根節點,這樣可以減少佈局嵌套,提高顯示速率。

    小結:使用佈局優先級:FrameLayout>ConstraintLayout>LinearLayout>RelativeLayout,結合效率和需求實現。

  2. 儘量減少使用wrap_content,推薦使用mathch_parent或固定尺寸配合gravity="center"

    因爲 在測量過程中,match_parent和固定寬高度對應EXACTLY,而wrap_content對應AT_MOST,這兩者對比AT_MOST耗時較多。

  3. 在需要的地方添加渲染背景,外層不渲染,在內層需要的地方渲染。

  4. 文本控件,需要考慮文本過長時的省略策略

  5. 切圖至少提供兩套,xhdpixxhdpi

  6. 消除佈局警告,同時刪除控件中的無用屬性

  7. 對於只有在某些條件下才展示出來的組件,建議使用viewStub包裹起來,include 某佈局如果其根佈局和引入他的父佈局一致,建議使用merge包裹起來,如果你擔心preview效果問題,這裏完全沒有必要,可以tools:showIn=""屬性,這樣就可以正常展示preview了。

  8. 歡迎補充。

參考資料:

1.Android性能優化之佈局優化

2.Android 高效佈局的幾點建議

3.(譯)寫出高效清晰Layout佈局文件的一些技巧

4.Android 過度繪製優化(推薦閱讀)

5.Android開發之merge結合include優化佈局

6.Android性能優化之渲染篇

7.如何優化你的佈局層級結構之RelativeLayout和LinearLayout及FrameLayout性能分析

8.LinearLayout增加divider分割線

9.Android APP 性能優化的一些思考

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