Android 優化之路(一)佈局優化

aiwannian發表於2019-03-04

前言

  • Android開發中,效能優化策略十分重要。因為我認為預防永遠比治癒有意義重要得多。我們不應該等到一個問題已經發生了,並且到了一定程度才想起來需要重構程式碼或者進行效能優化,通過早早的學習效能優化的思維和工具能避免很多問題,糾正一些不良的編碼習慣,對Coder的編碼能力提高具有很大的意義。
  • 本文主要講解佈局優化,希望對你們有幫助

目錄

Android 優化之路(一)佈局優化


1. 對效能的影響

主要影響Android應用中頁面顯示的速度。1個頁面通過遞迴 完成測量 & 繪製過程 = measure、layout 過程,而這個過程過長則會給使用者帶來卡頓的視覺效果。


2.優化思路

佈局優化的思路其實很簡單,就是儘量減少佈局檔案的層級。佈局的層級少了,這就意味著Android繪製時工作量少了,那麼程式的效能自然就高了。


3. 具體優化方案

3.1 刪除佈局中無用的控制元件和層級

3.2 選擇耗費效能較少的佈局

如果佈局中即可使用LinearLayout也可以使用RelativeLayout,那就採用LinearLayout。因為RelativeLayout在繪製時需要對子View分別進行了豎直和水平方向的兩次測量,而Linearlayout在繪製時是根據我們設定的方向分別呼叫不同的測量方法。注意一點如果LinearLayout中子View使用了layout_weight屬性時同樣需要對子View進行兩次測量以確定最終大小(對此不瞭解的小夥伴們可以檢視原始碼中onMeasureonLayout方法本文就不多貼原始碼)。LinearLayoutFrameLayout都是一種效能耗費低的佈局。但是很多時候單純通過一個LinearLayoutFrameLayout無法實現產品的效果,需要通過巢狀的方式來完成。這種情況下建議使用RelativeLayout,因為巢狀就相當於增加了佈局的層級,同樣會降低程式的效能。

評論多次提到Constraintlayout,由於筆者用的較少忘了說,疏忽了,疏忽了[手動哭笑]。面對複雜度高的佈局(比RelativeLayoutLinearLayout多次巢狀)Constraintlayout確實更簡單,繪製時間更短。但面對複雜度較低的佈局,RelativeLayoutConstraintLayoutonMesaure階段快數倍。下圖為Hierarchy Viewer的測試結果(裡面一個TextView,一個ImageView,關於Hierarchy Viewer的使用會在下文佈局調優工具中):

Android 優化之路(一)佈局優化 Android 優化之路(一)佈局優化 Android 優化之路(一)佈局優化
  • 效能耗費低的佈局 = 功能簡單 = FrameLayout、LinearLayout
  • 效能耗費高的佈局 = 功能複雜 = RelativeLayout(ConstraintLayout)
  • 巢狀所耗費的效能 > 單個佈局本身耗費的效能

3.3 提高佈局的複用性(使用 <include> 佈局標籤)

使用 <include>標籤提取佈局間的公共部分,通過提高佈局的複用性從而減少測量 & 繪製時間

<!--抽取出的公共佈局:include_title.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"
    android:background="@color/colorAccent">

    <ImageView
        android:layout_width="wrap_content"
        android:layout_height="match_parent"
        android:layout_alignParentLeft="true"
        android:paddingLeft="15dp"
        android:src="@mipmap/ic_titilebar_back"/>

    <TextView
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_centerInParent="true"
        android:text="@string/title"
        android:textColor="@color/white"
        android:textSize="18sp"/>

    <TextView
        android:layout_width="wrap_content"
        android:layout_height="match_parent"
        android:layout_alignParentRight="true"
        android:gravity="center"
        android:padding="15dp"
        android:text="@string/more"
        android:textColor="@color/white"
        android:textSize="16sp"/>

</RelativeLayout>


<!--佈局:activity_main.xl引用公共佈局include_title.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="@layout/include_title"/>

    <TextView
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:gravity="center"
        android:text="Hello World!" />

</LinearLayout>

複製程式碼

示例圖3.3

<include> 標籤只支援以android:layout_開頭的屬性(android:id除外),需要注意一點如果使用了android:layout_*這種屬性,那麼要求android:layout_width 和android:layout_height必須存在,否則其他android:layout_*形式的屬性無法生效,下面是一個指定了android:layout_*屬性的示例

<include
    android:id="@+id/include_title"
    android:layout_width="match_parent"
    android:layout_height="48dp"
    layout="@layout/include_title"/>
複製程式碼

3.4 減少佈局的層級(使用 <merge> 佈局標籤)

<merge> 佈局標籤一般和 <include> 標籤一起使用從而減少佈局的層級。例如當前佈局是一個豎直方向的LinearLayout,這個時候如果被包含的佈局也採用了豎直方向的LinearLayout,那麼顯然被包含的佈局檔案中的LinearLayout是多餘的,這時通過 <merge> 佈局標籤就可以去掉多餘的那一層LinearLayout。如下所示:

<!--抽取出的公共佈局:include_title.xml-->
<?xml version="1.0" encoding="utf-8"?>
<merge xmlns:android="http://schemas.android.com/apk/res/android">
    
    <Button
        android:id="@+id/button"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="Button" />

    <Button
        android:id="@+id/button2"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="Button" />

</merge>

<!--佈局:activity_main.xl引用公共佈局include_title.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="@layout/include_title"/>

</LinearLayout>

複製程式碼

示例圖3.4

3.5 減少初次測量 & 繪製時間

3.5.1使用 <ViewStub> 標籤

ViewStub繼承了View,它非常輕量級且寬和高都為0,因此它本身不參與任何的繪製過程,避免資源的浪費,減少渲染時間,在需要的時候才載入View。因此ViewStub的意義在於按需求載入所需的佈局,在實際開發中,很多佈局在正常情況下不會顯示,比如載入資料暫無資料,網路異常等介面,這個時候就沒必要在整個介面初始化的時候將其載入進來,通過ViewStub就可以做到在使用時在載入,提高了程式初始化時的效能。如下一個ViewStub的示例:

<!--暫無資料頁:empty_data.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:gravity="center"
    android:orientation="vertical">

    <ImageView
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:background="@mipmap/ic_empty_order"/>

    <TextView
        android:layout_below="@+id/iv_empty"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="暫無資料"/>

</LinearLayout>

<!--佈局activity_main.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:gravity="center"
    android:orientation="vertical">

    //view_stub是ViewStub的id,empty是empty_data.xml這個佈局根元素的id
    <ViewStub
        android:id="@+id/view_stub"            
        android:inflatedId="@+id/empty"         
        android:layout="@layout/empty_data"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        />

</LinearLayout>

<!--MainActivity-->
@Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        ButterKnife.bind(this);
        //載入ViewStub中的佈局的兩種方式setVisibility或inflate
        mViewStub.setVisibility(View.VISIBLE);
        mViewStub.inflate();
        
    }

複製程式碼

示例圖3.5.1

使用ViewStub需注意:當ViewStub通過setVisibility或inflate方法載入後,ViewStub就會被它內部的佈局替換掉,這個時候ViewStub就不再是佈局結構中的的一部分。目前ViewStub中的layout還不支援使用<merge> 標籤。

3.5.2儘可能少用佈局屬性 wrap_content

佈局屬性 wrap_content 會增加布局測量時計算成本,應儘可能少用

3.6 減少控制元件的使用(善用控制元件屬性)

在繪製佈局中,某些情況下我們可以省去部分控制元件的使用。下文介紹幾種常見的情況:

3.6.1 TextView文字加圖片

示例圖3.6.1
上圖佈局,通常想到的是一個相對佈局裡包含一個TextView和兩個ImageView。事實上我們只需要一個TextView就可以實現

<TextView
        android:layout_width="match_parent"
        android:layout_height="50dp"
        android:layout_marginTop="20dp"
        android:paddingLeft="16dp"
        android:paddingRight="16dp"
        android:gravity="center_vertical"
        android:drawableLeft="@mipmap/icon_my_unlock"   // 設定左邊顯示的icon  
        android:drawablePadding="10dp"                  // 設定icon和文字的間距  
        android:drawableRight="@mipmap/icon_right"      // 設定右邊顯示的icon
        android:text="@string/account_unlock"
        />
複製程式碼

3.6.2 LinearLayout分割線

示例圖3.6.2
上圖佈局,通常是用高為1dp的View來顯示,實際上LinearLayout本身就能實現

<?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:divider="@drawable/divider_line"
    android:dividerPadding="16dp"
    android:showDividers="middle"
    android:orientation="vertical">

    <TextView
        android:layout_width="match_parent"
        android:layout_height="50dp"
        android:paddingLeft="16dp"
        android:paddingRight="16dp"
        android:gravity="center_vertical"
        android:drawableLeft="@mipmap/icon_my_unlock"
        android:drawablePadding="10dp"
        android:drawableRight="@mipmap/icon_right"
        android:background="@color/color_FFFFFF"
        android:text="@string/account_unlock"
        />
    <TextView
        android:layout_width="match_parent"
        android:layout_height="50dp"
        android:paddingLeft="16dp"
        android:paddingRight="16dp"
        android:gravity="center_vertical"
        android:drawableLeft="@mipmap/icon_my_unlock"
        android:drawablePadding="10dp"
        android:drawableRight="@mipmap/icon_right"
        android:background="@color/color_FFFFFF"
        android:text="@string/account_unlock"
        />

    <TextView
        android:layout_width="match_parent"
        android:layout_height="50dp"
        android:paddingLeft="16dp"
        android:paddingRight="16dp"
        android:gravity="center_vertical"
        android:drawableLeft="@mipmap/icon_my_unlock"
        android:drawablePadding="10dp"
        android:drawableRight="@mipmap/icon_right"
        android:background="@color/color_FFFFFF"
        android:text="@string/account_unlock"
        />

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

    <solid android:color="@color/colorPrimary"/>
    <size android:height="1dp"/>

</shape>

</LinearLayout>
複製程式碼

核心程式碼就是對LinearLayout設定divider

  • divider 設定分割線樣式,需注意不能簡單隻給個顏色值,比如#f00或者@color/xxx這樣,drawable一定要是個有長、寬概念的drawable,當然你也可以直接一張圖片當divider。
  • dividerPadding 設定分割線兩邊的間距
  • showDividers 設定分割線顯示位置。其中middle控制元件之間顯示;beginning第一個控制元件上面顯示分割線;end最後一個控制元件下面顯示分割線,none不顯示分割線

3.6.3 TextView的行間距和佔位符的使用

示例圖3.6.3
上圖佈局,通常想到的是一個線性佈局裡包含3個TextView。事實上我們只需要一個TextView就可以實現

<TextView
        android:id="@+id/text"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:lineSpacingExtra="10dp"
        android:text="@string/test_text"
        android:textSize="20sp"
        android:textColor="@color/colorPrimary"/>
複製程式碼

通過lineSpacingExtra設定行間隔,test_text內容為:

<string name="test_text">標題:%1$s\n時間:%2$s\n內容:%3$s</string>
複製程式碼

在MainActivity中的使用

mText.setText(String.format(getResources().getString(R.string.test_text), "測試", "2018-8-9","測試測試"));
複製程式碼

佔位符的使用方法:%n表示第n位要被替換的,$s表示字串型別佔位符,$d表示整型佔位符,$f表示浮點型佔位符


4. 佈局調優工具

在實際開發中哪怕注意了上述的優化方案,難免還是會出現佈局效能的問題。這時我們可使用佈局調優工具來分析問題。本文將介紹常用的幾種工具。

4.1 Lint

Lint 是Android Studio 提供的 程式碼掃描分析工具,它可以幫助我們發現程式碼結構/質量問題,同時提供一些解決方案,而且這個過程不需要我們手寫測試用例。 Lint 的使用路徑: 工具欄 -> Analyze -> Inspect Code

示例圖4.1.1

預設是檢查整個專案,我們可以點選 Custom scope 自定義檢查範圍

  • Project Files:所有專案檔案
  • Project Production Files:專案的程式碼檔案
  • Project Test Files:專案的測試檔案
  • OpenFiles:當前開啟的檔案
  • Module ‘app’:主要的 app 模組
  • Current File:當前檔案

示例圖4.1.2

當然你也可以選擇特定的類進行檢查點選下圖紅色箭頭標識的地方

示例圖4.1.3

點選“+”號新增一個檢查範圍:

  • Local:只能當前專案使用
  • Shared:其他 Android Studio 專案也可以使用

示例圖4.1.4
選擇Shared,預設按專案顯示,檢查的檔案數為 0 。下圖紅色框中4個按鈕表示要操作的型別

  • Include:包括當前資料夾內的檔案,但不包括他的子資料夾
  • Include Recursively:包括當前資料夾以及它的子資料夾內所有的資料夾,遞迴新增
  • Exclude:移除當前資料夾,不包括子資料夾
  • Exclude Recursively:移除當前資料夾及所有子資料夾

示例圖4.1.4

我們左擊想掃描的檔案,點選右邊對應的按鈕。可以看到檔案邊色了,紅框顯示需掃描24個檔案,點選OK→OK

示例圖4.1.5

稍等一會兒,會彈出 Inspection 對話方塊,顯示檢查結果。

示例圖4.1.6

Lint 的警告嚴重程度有以下幾種:

  • Unused Entry:沒有使用的屬性,灰色,很不起眼
  • Typo:拼寫錯誤,綠色波浪下劃線,也不太起眼
  • Server Problem:伺服器錯誤?好像不是
  • Info:註釋文件,綠色,比較顯眼
  • Weak Warning:比較弱的警告,提示比較弱
  • Warning:警告,略微顯眼一點
  • Error:錯誤,最顯眼的一個
    示例圖4.1.7

本文對Lint的介紹就到此如果對Lint想了解更多的小夥伴可以點選Lint

4.2 Hierarchy Viewer

Hierarchy Viewer 是Android Studio 提供的UI效能檢測工具。可獲得UI佈局設計結構 & 各種屬性資訊,幫助我們優化佈局設計 。

使用Hierarchy Viewer ,您的裝置必須執行Android 4.1或更高版本。如果您是使用真機的話需注意以下兩點:

  • 在您的裝置上啟用開發者選項
  • 在開發計算機上設定環境變數 ANDROID_HVPROTO=ddm
    此變數告訴Hierarchy Viewer使用ddm 協議連線到裝置,該協議與DDMS協議相同。需要注意的是,主機上只能有一個連線到裝置的程式,因此您必須終止任何其他DDMS會話才能執行Hierarchy Viewer。

Hierarchy Viewer 的使用路徑: 工具欄 ->Tools->Android->Android Device Monitor(預設是顯示DDMS視窗,更改可點選Open Perspective->Hierarchy Viewer,也可以直接點選 Hierarchy Viewer Button),如下圖所示:

示例圖4.2.1

更改後顯示Hierarchy Viewer的視窗如下圖所示:

示例圖4.2.2

如果您看的的檢視排列不一樣,可選擇 Window->Reset Perspective 返回預設佈局。下面介紹下基本視窗:

  • Window(左上角)顯示裝置資訊,app以包名顯示,雙擊選中。
  • View Properties(左上角)顯示檢視的屬性
  • Tree View(中心):顯示檢視層次結構的樹檢視。您可以使用滑鼠拖動樹和縮放樹,並在底部使用縮放控制元件。每個節點都指示它的View類名和ID名稱。
  • Tree Overview(右上角):使您可以鳥瞰應用程式的完整檢視層次結構。移動灰色矩形以更改樹檢視中可見的視口。
  • Layout View(右下角):顯示佈局的線框檢視。當前所選檢視的輪廓為紅色,其父檢視為淺紅色。單擊此處的檢視也會在樹檢視中選擇它,反之亦然。
    示例圖4.2.3
    示例圖4.2.4

上圖圖示的使用左到右介紹:

  • 重新載入檢視層次結構
  • 該檢視彈窗
  • 使佈局無效
  • 請求佈局
  • 重新載入檢視層次結構
  • 分析檢視,顯示檢視的耗時

示例圖4.2.5
所選的節點上方會有個小視窗顯示子View數和MeasureLayoutDraw繪製所需時間。
所選節點的每個子檢視都有三個點,可以是綠色,黃色或紅色。

  • 左點表示渲染管道的繪製過程。
  • 中間點代表佈局階段。
  • 右點表示執行階段。

這些點大致對應於處理管道的度量,佈局和繪製階段。點的顏色表示該節點相對於本地系列中所有其他配置節點的相對效能。

  • 綠色 表示檢視渲染速度比其他檢視中的至少一半快。
  • 黃色 表示檢視呈現的速度比其他檢視的下半部分快。
  • 紅色 表示檢視是最慢的一半檢視之一。

所測量的是每個節點相對於兄弟檢視的效能,因此配置檔案中始終存在紅色節點,除非所有檢視執行相同,並且它並不一定意味著紅色節點執行得很差(僅限於它是最慢的檢視)在本地檢視組中)。

本文對Hierarchy Viewer的介紹就到此了,想了解更多的小夥伴可以點選Hierarchy Viewer

4.3 開發者選項(除錯GPU過度繪製)

開發者選項是Android 系統為開發者提供的一個APP驗證、除錯、優化等各種功能的入口(需Android 4.1以上)。本文主要對GPU過度繪製選單介紹,對其他選單功能有興趣的小夥伴可以參考探索Android手機開發者選項

GPU過度繪製是指在螢幕上某個畫素在同一幀的時間內被繪製多次,產生的主要原因有兩個:

  • 在XML佈局中,控制元件有重疊卻都設定背景
  • View的OnDraw中同一區域繪製多次

4.3.1 開啟開發者選項

不同手機開發商都對手機介面做了定製化處理,開啟手機開發者選項的方式各不相同,但基本的功能選單都是類似的。由於筆者現在用的是小米5s Plus,因此開啟方式是設定->我的裝置->全部引數->連續點選MIUI版本,提示開發者模式開啟即可(不同MIUI版本開啟路徑也不一樣)。然後設定->更多設定,高階設定或者其他設定等等選單中即可看到開發者選項的選單了。

4.3.2 開啟GPU過度繪製

在開發者選項中點選除錯GPU過度繪製,如下圖所示

示例圖4.3.2.1
示例圖4.3.2.2

這時螢幕會出現各種顏色,每種顏色的定義為:

  • 原色: 沒有過度繪製 – 每個畫素在螢幕上繪製了一次。
  • 藍色: 一次過度繪製 – 每個畫素點在螢幕上繪製了兩次。
  • 綠色: 兩次過度繪製 – 每個畫素點在螢幕上繪製了三次。
  • 粉色: 三次過度繪製 – 每個畫素點在螢幕上繪製了四次。
  • 紅色: 四次或四次以上過度繪製 – 每個畫素點在螢幕上繪製了五次或者五次以上。

以上就是本文對佈局調優工具的介紹,當然還有其他佈局調優工具如Systrace本文就不一一介紹了。


結尾

本文主要講解Android 效能優化中的 佈局優化。如對小夥伴們有幫助,麻煩給個喜歡,不足之處請留言私聊點出謝謝!
對Flutter有興趣的可參考Flutter學習之路(一)Flutter簡介及Window下開發環境搭建

Android 優化之路(一)佈局優化

相關文章