Android 性能優化之佈局優化

1. 爲何進行佈局優化?

設計者對 UI 的效果要求愈來愈高,致使的直接結果是界面複雜度愈來愈高。界面越複雜,加載速度越慢,界面加載越慢,用戶體驗越差,用戶體驗越差,App 被卸載的機率就越大。所以,佈局佈局優化勢在必行。html

2. Android 設備 UI 刷新機制

2.1 Android 設備 UI 刷新機制

Android 設備 UI 刷新主要涉及三個模塊,分別是:python

  • CPU;
  • GPU;
  • DISPLAY(硬件);

CPU 用於計算數據,GPU 用於將圖像數據進行柵格化處理並把處理好的數據存入 BUFFER,DISPLAY 從 BUFFER 中取數據並將圖像數據展現出來:android

但這是理想狀態,實際狀況是:
DISPLAY 的刷新率和 GPU 的幀率並不能總保持一致,也就是說,GPU 存數據的時間和 DISPLAY 取數據的時間是不一致的。針對此,Android 引入了 Vsync(垂直同步)。性能優化

Vsync 主要是爲了讓 DISPLAY 的刷新率和 GPU 的幀率保持一致,只有收到 Vsync 信號的時候,CPU 纔開始計算數據、GPU 纔開始處理數據、DISPLAY 纔開始展現數據。bash

2.2 多長時間刷新一次 UI 比較合適?

人眼與大腦之間的協做沒法感知超過 60 fps(Frame per Second)的畫面更新,也就是當畫面的更新頻率超過 60HZ 以後,人眼已經看不出它和更新頻率爲 60HZ 的界面之間的區別了。app

1s = 1000ms
1000ms / 60 = 16.7 mside

所以,每 16.7 ms 刷新一次界面,用戶就會感受很流暢。也正是所以,View 的 measure、layout、draw 必須在 16ms 內完成,不然用戶將感受到卡頓。模塊化

2.3 相關問題

2.3.1 什麼是刷新率?

一秒內刷新屏幕的次數,如 60 HZ,多數狀況下,屏幕刷新率越高越好。不過,若是畫面的內容還未生成,刷新再快也沒有什麼卵用,不是嗎?工具

2.3.2 什麼是幀率?

GPU 在一秒內操做界面的幀數,如 30 fps,60 fps。佈局

3. 如何排查佈局性能問題?

我用過的排查佈局性能的工具備三種:

  1. LayoutInspector;
  2. 手機自帶「過分繪製檢測工具」;
  3. systrace & traceview;

3.1 LayoutInspector

Android Studio 自帶的用於檢測佈局文件嵌套層級的工具,能夠在開發階段方便的檢測佈局文件的嵌套層級。

打開的方法:

Tools → Layout Inspector → 選擇要測試的 App  
複製代碼

在左邊「View Tree」欄能夠查看當前界面的嵌套層級,「View Tree」右邊對應的就是當前界面的顯示的效果,當點擊「View Tree」中不一樣的控件時,「View Tree」右邊界面中的相應控件會被藍色框框起來。

3.2 手機自帶「過分繪製檢測工具」

打開手機自帶「過分繪製檢測工具」方法:

設置 → 開發者選項 → 顯示過分繪製區域
複製代碼

打開「顯示過分繪製區域」以後,界面就會根據當前像素被繪製了幾回顯示不一樣的顏色:

各類顏色表明的意思:

顏色 過分重繪次數
藍色 1
綠色 2
粉色 3
紅色 4+

舉個例子,建立一個下面這樣的「我的中心」,這是經手機自帶的「過分繪製檢測工具」檢測的結果:

3.3 systrace & traceview

systrace 搭配 traceview 定義佈局性能問題,經過 systrace 定位大方向,經過 traceview 解析具體細節。

具體步驟:

3.3.1 systrace 定位大方向

  1. 啓動 App,併到目標測試界面或者目標測試界面前一界面;
  2. 在「終端」執行「python systrace.py --time=10 -o xxx.html」;
  3. 進入目標界面,操做目標界面;
  4. 在 Chrome 中查看「file:///Users/xxx/Library/Android/sdk/platform-tools/systrace/xxx.html」中的 xxx.html 文件,解析 xxx.html 文件;

3.3.2 traceview 解析具體細節

  1. 在目標測試界面加入追蹤代碼:Debug.startMethodTracing(Constants.TRACE)(開始追蹤)、Debug.stopMethodTracing()(中止追蹤);
  2. 在 Android Studio 中查看「sdcard/Android/data/package name/files/xxx.trace」中的 xxx.trace 文件,解析 xxx.trace 文件;
  3. 在 「TopDown」 欄,直接查看每一個方法的調用時長,進而找到耗時最多的方法,並進行相應的優化;

4. 佈局優化策略

  1. 根據實際狀況,選用性能高的佈局文件;
  2. 減小布局嵌套層級(打造「寬而淺」,遠離「窄而深」);
  3. 提升佈局複用性;
  4. 減小測量、佈局、繪製時間;
  5. 減小控件的使用(善用控件屬性);

4.1 根據實際狀況,選用性能高的佈局文件

在 Android 中,並非全部的 ViewGroup 性能都同樣,若是非要排個序的話,那大概會是下面這個樣子:

ConstraintLayout > FrameLayout > LinearLayout > RelativeLayout  
複製代碼

上面的排序是針對「四種容器」均能實現的效果而說的,也就是說,假設有某一場景,以上四種佈局都可實現,ConstraintLayout 性能是最好的。

ConstraintLayout 能在不經任何嵌套的狀況下,建立複雜的佈局,所以,在從此的開發中應多使用它。另外,當 LinearLayout 和 RelativeLayout 均能在無需嵌套的狀況下實現某效果時,用 LinearLayout,由於 RelativeLayout 測量時須要測量兩次(當 LinearLayout 的子 View 有 layout_weight 屬性時,也須要測兩次)。

4.2 減小布局嵌套層級

4.2.1 合理選擇佈局類型

在實現某個佈局時,若是有可能不用任何嵌套層級,那就不要用任何嵌套層級實現該界面。

舉個例子,有一個登陸界面,分別用兩種方式實現:

  1. 嵌套的 LinearLayout;
  2. RelativeLayout;
//1. 第一種實現方式:嵌套的 LinearLayout  
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:gravity="center_horizontal"
    android:orientation="vertical"
    android:paddingLeft="@dimen/item_height"
    android:paddingTop="@dimen/padding_medium"
    android:paddingRight="@dimen/item_height"
    android:paddingBottom="@dimen/padding_medium">


    <de.hdodenhof.circleimageview.CircleImageView
        android:id="@+id/avatar"
        android:layout_width="@dimen/padding_ninety_six"
        android:layout_height="@dimen/padding_ninety_six"
        android:layout_gravity="center_horizontal"
        android:layout_marginTop="@dimen/padding_ninety_six"
        android:scaleType="centerCrop"
        android:src="@drawable/bird_woodpecker"
        app:civ_border_color="@color/grey_800"
        app:civ_border_width="@dimen/padding_micro_x" />

    <LinearLayout
        android:id="@+id/login_username_container"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:layout_marginTop="@dimen/padding_large"
        android:orientation="horizontal">

        <TextView
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="@string/login_username" />

        <EditText
            android:id="@+id/login_username"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:layout_marginLeft="@dimen/padding_medium"
            android:background="@null"
            android:hint="@string/login_username_hint"
            android:inputType="text"
            android:maxLines="1"
            android:singleLine="true"
            android:textColor="@color/grey_700"
            android:textCursorDrawable="@drawable/common_edit_text_cursor"
            android:textSize="@dimen/font_micro" />
    </LinearLayout>


    <View
        android:layout_width="match_parent"
        android:layout_height="@dimen/padding_micro_xx"
        android:layout_marginTop="@dimen/padding_small"
        android:layout_marginBottom="@dimen/padding_small"
        android:background="@color/grey_300" />

    <LinearLayout
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:layout_marginTop="@dimen/padding_medium"
        android:orientation="horizontal">

        <TextView
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="@string/login_password" />

        <EditText
            android:id="@+id/login_password"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:layout_marginLeft="@dimen/padding_medium"
            android:background="@null"
            android:hint="@string/login_password_hint"
            android:inputType="textPassword"
            android:maxLines="1"
            android:singleLine="true"
            android:textColor="@color/grey_700"
            android:textCursorDrawable="@drawable/common_edit_text_cursor"
            android:textSize="@dimen/font_micro" />
    </LinearLayout>

    <View
        android:layout_width="match_parent"
        android:layout_height="@dimen/padding_micro_xx"
        android:layout_marginTop="@dimen/padding_small"
        android:layout_marginBottom="@dimen/padding_small"
        android:background="@color/grey_300" />

    <TextView
        android:id="@+id/login"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:layout_marginLeft="@dimen/padding_medium"
        android:layout_marginTop="@dimen/padding_large"
        android:layout_marginRight="@dimen/padding_medium"
        android:background="@drawable/ripple_login"
        android:clickable="true"
        android:elevation="@dimen/divider_height"
        android:gravity="center"
        android:paddingTop="@dimen/padding_medium"
        android:paddingBottom="@dimen/padding_medium"
        android:text="@string/login"
        android:textColor="@color/grey_700"
        android:textSize="@dimen/font_small"
        android:textStyle="bold" />

</LinearLayout>
複製代碼

經過 LayoutInspector 檢測該界面的嵌套層級:

//2. 第二種實現方式:RelativeLayout  
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical"
    android:paddingLeft="@dimen/item_height"
    android:paddingTop="@dimen/padding_medium"
    android:paddingRight="@dimen/item_height"
    android:paddingBottom="@dimen/padding_medium">


    <de.hdodenhof.circleimageview.CircleImageView
        android:id="@+id/avatar"
        android:layout_width="@dimen/padding_ninety_six"
        android:layout_height="@dimen/padding_ninety_six"
        android:layout_centerHorizontal="true"
        android:layout_marginTop="@dimen/padding_ninety_six"
        android:scaleType="centerCrop"
        android:src="@drawable/bird_woodpecker"
        app:civ_border_color="@color/grey_800"
        app:civ_border_width="@dimen/padding_micro_x" />

    <TextView
        android:id="@+id/login_username_label"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_below="@id/avatar"
        android:layout_marginTop="@dimen/padding_large"
        android:text="@string/login_username" />

    <EditText
        android:id="@+id/login_username"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:layout_below="@id/avatar"
        android:layout_marginLeft="@dimen/padding_medium"
        android:layout_marginTop="@dimen/padding_large"
        android:layout_toRightOf="@id/login_username_label"
        android:background="@null"
        android:hint="@string/login_username_hint"
        android:inputType="text"
        android:maxLines="1"
        android:singleLine="true"
        android:textColor="@color/grey_700"
        android:textCursorDrawable="@drawable/common_edit_text_cursor"
        android:textSize="@dimen/font_micro" />


    <View
        android:id="@+id/login_username_divider"
        android:layout_width="match_parent"
        android:layout_height="@dimen/padding_micro_xx"
        android:layout_below="@id/login_username_label"
        android:layout_marginTop="@dimen/padding_small"
        android:layout_marginBottom="@dimen/padding_small"
        android:background="@color/grey_300" />

    <TextView
        android:id="@+id/login_password_label"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_below="@id/login_username_divider"
        android:layout_marginTop="@dimen/padding_medium"
        android:text="@string/login_password" />

    <EditText
        android:id="@+id/login_password"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:layout_below="@id/login_username_divider"
        android:layout_marginLeft="@dimen/padding_medium"
        android:layout_marginTop="@dimen/padding_medium"
        android:layout_toRightOf="@id/login_password_label"
        android:background="@null"
        android:hint="@string/login_password_hint"
        android:inputType="textPassword"
        android:maxLines="1"
        android:singleLine="true"
        android:textColor="@color/grey_700"
        android:textCursorDrawable="@drawable/common_edit_text_cursor"
        android:textSize="@dimen/font_micro" />

    <View
        android:id="@+id/login_password_divider"
        android:layout_width="match_parent"
        android:layout_height="@dimen/padding_micro_xx"
        android:layout_below="@id/login_password_label"
        android:layout_marginTop="@dimen/padding_small"
        android:layout_marginBottom="@dimen/padding_small"
        android:background="@color/grey_300" />

    <TextView
        android:id="@+id/login"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:layout_below="@id/login_password_divider"
        android:layout_marginLeft="@dimen/padding_medium"
        android:layout_marginTop="@dimen/padding_large"
        android:layout_marginRight="@dimen/padding_medium"
        android:background="@drawable/ripple_login"
        android:clickable="true"
        android:elevation="@dimen/divider_height"
        android:gravity="center"
        android:paddingTop="@dimen/padding_medium"
        android:paddingBottom="@dimen/padding_medium"
        android:text="@string/login"
        android:textColor="@color/grey_700"
        android:textSize="@dimen/font_small"
        android:textStyle="bold" />

</RelativeLayout>
複製代碼

經過 LayoutInspector 檢測該界面的嵌套層級:

4.2.2 使用 標籤

在使用 標籤引入其餘佈局文件時,不會產生多餘的嵌套層級。由於佈局層級少,因此繪製的工做量小,所以繪製速度更快、性能更高。

須要注意的是,最終 標籤仍是要靠 或 標籤來引用才能真正的「被使用」,另外, 標籤自己是沒有任何屬性能夠配置的, 標籤用的是父 View 的屬性。

舉個例子:

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

    <TextView
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:background="@color/white"
        android:paddingLeft="@dimen/padding_medium"
        android:paddingTop="@dimen/padding_medium"
        android:paddingBottom="@dimen/padding_medium"
        android:text="@string/clear_cache"
        android:textSize="@dimen/font_micro" />

    <TextView
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:background="@color/white"
        android:paddingLeft="@dimen/padding_medium"
        android:paddingTop="@dimen/padding_medium"
        android:paddingBottom="@dimen/padding_medium"
        android:text="@string/version_update"
        android:textSize="@dimen/font_micro" />

    <TextView
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:background="@color/white"
        android:paddingLeft="@dimen/padding_medium"
        android:paddingTop="@dimen/padding_medium"
        android:paddingBottom="@dimen/padding_medium"
        android:text="@string/about"
        android:textSize="@dimen/font_micro" />

    <TextView
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:background="@color/white"
        android:paddingLeft="@dimen/padding_medium"
        android:paddingTop="@dimen/padding_medium"
        android:paddingBottom="@dimen/padding_medium"
        android:text="@string/logout"
        android:textSize="@dimen/font_micro" />
</merge>

//2. 引用 <merge> 標籤  
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    android:id="@+id/merge_parent"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:divider="@drawable/divider_line"
    android:dividerPadding="@dimen/padding_medium"
    android:orientation="vertical"
    android:showDividers="middle">

    <LinearLayout
        android:id="@+id/user_info"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:layout_marginTop="@dimen/item_height"
        android:layout_marginBottom="@dimen/item_height"
        android:gravity="center_horizontal"
        android:orientation="vertical">

        <de.hdodenhof.circleimageview.CircleImageView
            android:id="@+id/user_avatar"
            android:layout_width="@dimen/padding_ninety_six"
            android:layout_height="@dimen/padding_ninety_six"
            android:scaleType="centerCrop"
            android:src="@drawable/bird_woodpecker"
            app:civ_border_color="@color/grey_800"
            app:civ_border_width="@dimen/padding_micro_x" />

        <TextView
            android:id="@+id/user_name"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_marginTop="@dimen/padding_medium"
            android:text="@string/test_list_item6"
            android:textColor="@color/grey_700"
            android:textSize="@dimen/font_large" />
    </LinearLayout>


    <include
        android:id="@+id/marge_include"
        layout="@layout/activity_main_merge_child"
        android:layout_width="match_parent"
        android:layout_height="wrap_content" />
</LinearLayout>
複製代碼

標籤中的內容:

引用 標籤:

經過 LayoutInspector 檢測該界面的嵌套層級:

由檢測結果可知:

標籤引入其餘佈局文件時,確實不會產生多餘的嵌套層級。

4.3 提升佈局複用性

在 Android 開發中,不只 Java 代碼能夠被複用,XML 佈局文件的內容也能夠被複用。在 Android 中,佈局文件的複用,要經過 標籤。

不少 App 中,用的都是自定義的 Title,像這種在多個地方都使用且內容相同的構件,徹底不必每次使用的時候都從新寫一個,而是能夠只寫一個,在多個地方使用。

舉個例子,建立一個通用的 Title 佈局,經過 標籤使用:

//1. Title Layout  
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:id="@+id/title_container"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:background="@color/grey_700"
    android:orientation="horizontal">

    <ImageView
        android:id="@+id/back"
        android:layout_width="@dimen/item_height"
        android:layout_height="@dimen/item_height"
        android:padding="@dimen/padding_medium_x"
        android:src="@drawable/icon_back" />

    <TextView
        android:id="@+id/title"
        android:layout_width="match_parent"
        android:layout_height="@dimen/item_height"
        android:layout_toLeftOf="@id/share"
        android:layout_toRightOf="@id/back"
        android:gravity="center"
        android:text="@string/test_title"
        android:textColor="@color/white"
        android:textSize="@dimen/font_medium" />

    <ImageView
        android:id="@+id/share"
        android:layout_width="@dimen/item_height"
        android:layout_height="@dimen/item_height"
        android:layout_alignParentRight="true"
        android:padding="@dimen/padding_medium_x"
        android:src="@drawable/icon_share" />
</RelativeLayout>

//2. 在 SecondActivity 中經過 <include> 標籤引用 Title Layout
<?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:background="@color/grey_700"
    android:orientation="horizontal">

    <include
        layout="@layout/layout_title"
        android:layout_width="match_parent"
        android:layout_height="wrap_content" />

</LinearLayout>
複製代碼

標籤中的內容:

引用 標籤:

經過 LayoutInspector 檢測該界面的嵌套層級:

由檢測結果可知:

標籤引入其餘佈局文件時,會產生多餘的嵌套層級。

須要注意的是,當在 標籤中從新定義 標籤所引用的佈局文件屬性時,將覆蓋掉原佈局文件中定義的屬性。例如,原佈局文件的 layout_width 和 layout_height 爲 wrap_content,而 標籤中定義的 layout_width 和 layout_height 爲 match_parent,那最終顯示的效果將是 match_parent。

使用 標籤的好處除了複用 XML 佈局文件以外,還有一個好處——將複雜的佈局模塊化,提升複用率,下降學習成本。

4.4 減小測量、佈局、繪製時間

在 Android 中,默認狀況下,全部的 View 都是須要測量(measure())、佈局(layout())和繪製(draw())的,不管該 View 是否可見。

但有這樣一種標籤佈局,只有當它可見的時候纔會佔用資源,它就是 ViewStub。所以,咱們能夠藉助它的這個特性對佈局進行一些優化。

舉個例子,在一個界面中請求數據時,當請求失敗時展現一個錯誤提示界面,當請求成功時,展現請求到的信息:

//1. 錯誤提示界面  
<?xml version="1.0" encoding="utf-8"?>
<ImageView xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:src="@drawable/icon_error" />
    
//2. 引用「錯誤提示界面」的界面
<?xml version="1.0" encoding="utf-8"?>
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:id="@+id/user_info_container"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context=".third.ThirdActivity">

    <ViewStub
        android:id="@+id/error_placeholder"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:layout="@layout/layout_error" />

    <include
        android:id="@+id/user_info"
        layout="@layout/activity_main_merge_parent"
        android:layout_width="match_parent"
        android:layout_height="match_parent" />
</FrameLayout>

//3. 在 Activity 中作相應的處理  
public class ThirdActivity extends AppCompatActivity implements View.OnClickListener {

    private boolean isInvalid = true;
    private FrameLayout mContainer;
    private LinearLayout mUserInfo;
    private ViewStub mErrorPlaceHolder;
    private View mErrorView;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_third);
        initView();
    }

    private void initView(){
        mContainer = findViewById(R.id.user_info_container);
        mContainer.setOnClickListener(this);
        mUserInfo = findViewById(R.id.user_info);
        mErrorPlaceHolder = findViewById(R.id.error_placeholder);
    }

    @Override
    public void onClick(View view) {
        isInvalid = !isInvalid;
        if(isInvalid){
            mUserInfo.setVisibility(View.VISIBLE);
            mContainer.setBackgroundColor(getResources().getColor(R.color.white));
            if(mErrorView != null){
                mErrorView.setVisibility(View.INVISIBLE);
            }
            Toast.makeText(this, getString(R.string.test_request_succeed), Toast.LENGTH_SHORT).show();
        }else{
            mUserInfo.setVisibility(View.INVISIBLE);
            mContainer.setBackgroundColor(getResources().getColor(R.color.cyan_200));
            if(mErrorView == null){
                mErrorView = mErrorPlaceHolder.inflate();
                mErrorView.setVisibility(View.VISIBLE);
            }else{
                mErrorView.setVisibility(View.VISIBLE);
            }
            Toast.makeText(this, getString(R.string.test_request_failed), Toast.LENGTH_SHORT).show();
        }
    }

}
複製代碼

須要注意的是,默認狀況下,ViewStub 是不可見的,也是不佔用資源的,只有 inflate() 以後,ViewStub 纔會佔用資源,與此同時,ViewStub 自己也會被它指向的佈局文件(ViewStub 中 layout 屬性對應的佈局文件)所替代。

ViewStub inflate() 以前,經過 LayoutInspector 檢測該界面的嵌套層級:

由檢測結果可知,此時 ViewStub 在 View Tree 中是虛線,也就表示它此時不佔用資源。

ViewStub inflate() 以後,經過 LayoutInspector 檢測該界面的嵌套層級:

由檢測結果可知,此時 ViewStub 已經被它所指向的佈局文件所替代。

4.5 減小控件的使用(善用控件屬性)

  1. LinearLayout 分割線;
  2. TextView 同時顯示文字和圖片;

4.5.1 LinearLayout 分割線

//1. 定義分割線  
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android"
    android:shape="rectangle">

    <size
        android:width="@dimen/padding_micro_xx"
        android:height="@dimen/padding_micro_xx" />

    <solid android:color="@color/grey_200" />

</shape>

//2. 在 LinearLayout 中應用分割線  
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:divider="@drawable/divider_line"
    android:dividerPadding="@dimen/padding_medium"
    android:orientation="vertical"
    android:showDividers="middle"
    tools:context=".four.FourActivity">

    <TextView
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:background="@color/white"
        android:paddingLeft="@dimen/padding_medium"
        android:paddingTop="@dimen/padding_medium"
        android:paddingBottom="@dimen/padding_medium"
        android:text="@string/clear_cache"
        android:textSize="@dimen/font_micro" />

    <TextView
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:background="@color/white"
        android:paddingLeft="@dimen/padding_medium"
        android:paddingTop="@dimen/padding_medium"
        android:paddingBottom="@dimen/padding_medium"
        android:text="@string/version_update"
        android:textSize="@dimen/font_micro" />

    <TextView
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:background="@color/white"
        android:paddingLeft="@dimen/padding_medium"
        android:paddingTop="@dimen/padding_medium"
        android:paddingBottom="@dimen/padding_medium"
        android:text="@string/about"
        android:textSize="@dimen/font_micro" />

    <TextView
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:background="@color/white"
        android:paddingLeft="@dimen/padding_medium"
        android:paddingTop="@dimen/padding_medium"
        android:paddingBottom="@dimen/padding_medium"
        android:text="@string/logout"
        android:textSize="@dimen/font_micro" />

</LinearLayout>
複製代碼

最終效果:

4.5.2 TextView 同時顯示文字和圖片

//1. TextView 同時顯示文字和圖片
<?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="@dimen/padding_medium"
    android:orientation="vertical"
    android:showDividers="middle">

    <TextView
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:drawableLeft="@drawable/icon_animal_01"
        android:drawableRight="@drawable/icon_forward"
        android:drawablePadding="@dimen/padding_medium"
        android:gravity="center_vertical"
        android:paddingLeft="@dimen/padding_medium"
        android:paddingTop="@dimen/padding_medium"
        android:paddingRight="@dimen/padding_medium"
        android:paddingBottom="@dimen/padding_medium"
        android:text="@string/spending_traffic"
        android:textSize="@dimen/font_micro" />

    <TextView
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:drawableLeft="@drawable/icon_animal_02"
        android:drawableRight="@drawable/icon_forward"
        android:drawablePadding="@dimen/padding_medium"
        android:gravity="center_vertical"
        android:paddingLeft="@dimen/padding_medium"
        android:paddingTop="@dimen/padding_medium"
        android:paddingRight="@dimen/padding_medium"
        android:paddingBottom="@dimen/padding_medium"
        android:text="@string/spending_clothes"
        android:textSize="@dimen/font_micro" />

    <TextView
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:drawableLeft="@drawable/icon_animal_03"
        android:drawableRight="@drawable/icon_forward"
        android:drawablePadding="@dimen/padding_medium"
        android:gravity="center_vertical"
        android:paddingLeft="@dimen/padding_medium"
        android:paddingTop="@dimen/padding_medium"
        android:paddingRight="@dimen/padding_medium"
        android:paddingBottom="@dimen/padding_medium"
        android:text="@string/spending_diet"
        android:textSize="@dimen/font_micro" />

    <TextView
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:drawableLeft="@drawable/icon_animal_04"
        android:drawableRight="@drawable/icon_forward"
        android:drawablePadding="@dimen/padding_medium"
        android:gravity="center_vertical"
        android:paddingLeft="@dimen/padding_medium"
        android:paddingTop="@dimen/padding_medium"
        android:paddingRight="@dimen/padding_medium"
        android:paddingBottom="@dimen/padding_medium"
        android:text="@string/spending_communication"
        android:textSize="@dimen/font_micro" />

    <TextView
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:drawableLeft="@drawable/icon_animal_05"
        android:drawableRight="@drawable/icon_forward"
        android:drawablePadding="@dimen/padding_medium"
        android:gravity="center_vertical"
        android:paddingLeft="@dimen/padding_medium"
        android:paddingTop="@dimen/padding_medium"
        android:paddingRight="@dimen/padding_medium"
        android:paddingBottom="@dimen/padding_medium"
        android:text="@string/spending_live"
        android:textSize="@dimen/font_micro" />

    <TextView
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:drawableLeft="@drawable/icon_animal_06"
        android:drawableRight="@drawable/icon_forward"
        android:drawablePadding="@dimen/padding_medium"
        android:gravity="center_vertical"
        android:paddingLeft="@dimen/padding_medium"
        android:paddingTop="@dimen/padding_medium"
        android:paddingRight="@dimen/padding_medium"
        android:paddingBottom="@dimen/padding_medium"
        android:text="@string/spending_game"
        android:textSize="@dimen/font_micro" />

    <TextView
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:drawableLeft="@drawable/icon_animal_07"
        android:drawableRight="@drawable/icon_forward"
        android:drawablePadding="@dimen/padding_medium"
        android:gravity="center_vertical"
        android:paddingLeft="@dimen/padding_medium"
        android:paddingTop="@dimen/padding_medium"
        android:paddingRight="@dimen/padding_medium"
        android:paddingBottom="@dimen/padding_medium"
        android:text="@string/spending_others"
        android:textSize="@dimen/font_micro" />

</LinearLayout>
複製代碼

最終效果:

4.6 其餘佈局優化方法

  1. 移除沒必要要的 background,避免 OverDraw;
  2. 在自定義 View 中巧用 Canvas.clipRect() 避免重繪;
  3. ListView
    • 複用 ConvertView;
    • 使用 ViewHolder,減小 findViewById() 次數;
    • 分批加載;
  4. WebView
    • 定義全局的 WebView,以減小 WebView 首次打開時間;
    • 先加載文本,再加載圖片,以優化網頁加載速度;
  5. ViewPager 懶加載

5. 總結

正如啓動優化同樣,佈局優化也勢在必行,由於,複雜的佈局會影響其加載速度,而佈局加載速度越慢,用戶體驗越差,用戶體驗越差,App 被卸載的機率就越大,而這不是開發者想看到的,也不是老闆想看到的。

佈局優化的總目標就一個——快速加載佈局,佈局優化的策略有:

  1. 根據實際狀況,選用性能高的佈局文件;
  2. 減小布局嵌套層級(打造「寬而淺」,遠離「窄而深」);
  3. 提升佈局複用性;
  4. 減小測量、佈局、繪製時間;
  5. 減小控件的使用(善用控件屬性);

我相信,在使用了上面這些策略以後,你開發的 App 的性能確定會有必定的提高,趕忙去試試吧~


參考文檔

  1. Android性能優化:佈局優化
  2. Android 性能優化(二)之佈局優化面面觀
  3. Android佈局優化技巧
  4. Android UI性能優化實戰 識別繪製中的性能問題
相關文章
相關標籤/搜索