1. 程式人生 > >Android效能優化記錄之介面渲染優化

Android效能優化記錄之介面渲染優化

前言:網上已經有很多對於效能優化的文章,但是為啥自己還要造輪子呢?俗話說的好讀萬卷書不如行萬里路,好記性不如爛筆頭。只有自己動手去做了印象才會更深嘛!寫這篇是為了記錄效能優化這一系列的學習以及使用,順便鍛鍊下語言組織能力。

在之前Google釋出了一系列的Android效能優化的線上課程,這是在Udacity的教學視訊

  • 使用者感知介面卡頓的原因
  • 卡頓的原因
  • 過度繪製

使用者感知介面卡頓的原因

  • 對於Android系統來說每16ms都會重新繪製一次Activity,應用必須得在16ms內完成螢幕重新整理的全部邏輯,這樣才能達到每秒60幀(盜圖一張),當應用繪製的時間超過16ms 使用者就會感覺到介面卡頓,也是俗稱的掉幀。當繪製超過16ms時使用者會感覺到卡頓,如果在進行互動的時候比如輸入文字什麼的,這是時候掉幀是一件非常糟糕的事情
    圖一
    這裡寫圖片描述

卡頓的原因

Android 介面的渲染 是由cpu 和Gpu來完成的。cpu最常見的效能問題就是不必要的佈局和失效佈局這些會在檢視結構中進行測量,清除與重新建立,CPU常見的問題就是渲染的時候浪費了時間。造成這種問題的原因有兩種

- 列表顯示重複建立的次數太多  使cpu工作過度
- GPU進行後期著色的時候浪費了GPU的處理時間 -過度繪製

將一個Activity繪製到螢幕是由柵格化完成的,怎麼說?就是由CPU將影象轉化成多邊形或者紋理在傳給CPU進行柵格化操作把影象繪製出來,整個過程就是物件轉換以及資料的上傳。以減少物件的轉換以及資料上傳的時間來實現優化

過度繪製

過度繪製是指螢幕上的某一畫素在同一時間進行了多次繪製,在進行沒必要的繪製時浪費GPU和CPU的資源。過度繪製主要浪費GPU的資源和時間,使其在進行柵格化操作的時候浪費大量時間,開啟手機的開發者模式—除錯GPU過度繪製—顯示過度繪製區域,開啟之後會顯示過度繪製的顏色。
圖二


過度繪製的優化以佈局開始, 以下程式碼為例

<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:id="@+id/activity_main_container"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:paddingLeft="@dimen
/activity_horizontal_margin"
android:paddingRight="@dimen/activity_horizontal_margin" android:paddingTop="@dimen/activity_vertical_margin" android:paddingBottom="@dimen/activity_vertical_margin" android:orientation="vertical" android:background="@android:color/white"//這裡一次背景顏色 tools:context=".MainActivity"> <LinearLayout android:orientation="vertical" android:background="@android:color/white"//這裡在一次背景顏色, android:layout_width="match_parent" android:layout_height="match_parent"> <TextView android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="@string/header_text"/> </LinearLayout> </LinearLayout>

對兩個LinearLayout都給了白色的背景色,我們來看看效果
這裡寫圖片描述
我們發現圖片顯示了兩倍的過度繪製,現在我們將第二個LinearLayout背景色去掉

<LinearLayout
        android:orientation="vertical"
        android:layout_width="match_parent"
        android:layout_height="match_parent">
        <TextView
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="@string/header_text"/>
    </LinearLayout>

這裡寫圖片描述
再來看看效果, 變成了一倍 !嗯?1倍?對的1倍,因為受Android手機主題的影響視窗的背景有預設顏色,在根layout沒有設定背景顏色的時候在不同的手機上會發現背景是黑色或者白色的這是候怎麼辦?一行程式碼搞定

getWindow().setBackgroundDrawable(null);

這裡寫圖片描述
現在是不是乾淨多了?當然有些過度繪製是不影響效能的比如系統的ActionBar。
在佈局中減少多餘的背景色我們達到優化的效果,當然啦對於複雜的自定義view我們如何來優化?舉個列子當類似於卡片層疊的時候被遮住的一部分是沒用必要的繪製的,這是候就需要用到剪下 clipRect()方法的使用了。

這裡寫圖片描述

程式碼以Google 官方視訊裡的 圖片層疊為例,仿寫的。

public class CardView extends View {
    private Paint paint;

    public CardView(Context context) {
        super(context);
        paint=new Paint();
        paint.setColor(Color.BLUE);
        paint.setStyle(Paint.Style.STROKE);
        paint.setAntiAlias(true);
    }

    public CardView(Context context, AttributeSet attrs) {
        super(context, attrs);
    }

    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        Bitmap bitmap1 = BitmapFactory.decodeResource(getResources(),
                R.drawable.joanna).copy(Bitmap.Config.ARGB_8888, true);
        canvas.save();
        Utils.drawImage(canvas,bitmap1,000,0,200,200,0,0);
        canvas.restore();
        canvas.save();
        Utils.drawImage(canvas,bitmap1,100,0,200,200,0,0);
//// 儲存新合成的圖片
        canvas.restore();
    }

}

這裡寫圖片描述
第二張圖片蓋住第一張圖片的部分 顯示了二倍過度繪製,被蓋住的地方是沒用必要繪製的,這時候我們就用剪下的方式來剪下沒一張圖片的大小 例:第一張圖片只剪下一半大小第二張圖片剪下整張圖片大小以此類推。我們來看看修改之後的程式碼

    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        Bitmap bitmap1 = BitmapFactory.decodeResource(getResources(),
                R.drawable.joanna).copy(Bitmap.Config.ARGB_8888, true);
        canvas.save();
        canvas.clipRect(0,0,100,200);//
        Utils.drawImage(canvas,bitmap1,000,0,200,200,0,0);
        canvas.restore();
        canvas.save();
        canvas.clipRect(100,0,400,200);//
        Utils.drawImage(canvas,bitmap1,100,0,200,200,0,0);
//// 儲存新合成的圖片
        canvas.restore();
    }

這裡寫圖片描述
現在是不是蓋住的地方沒有過度繪製啦?對於過度繪製的優化總結下來3句話

  • 移除多餘的背景色
  • 設定窗體背景為空 getWindow().setBackgroundDrawable(null);
  • 利用clipRect()方法剪下掉不必要的繪製

失效、多餘、深度佈局

這裡說下渲染管道中的CPU部分,在螢幕中繪製某一個圖案的時候,Android將高階的xml檔案轉換成GPU能夠識別的物件然後顯示到螢幕上,這個操作是在DisplayList的幫助下完成的,DisplayList持有所有要交給GPU繪製到螢幕上的資料資訊包含GPU要繪製的全部物件的資訊列表以及執行繪製的OpenGL命令列表,在某個view第一次需要被渲染的時候DisplayList會因此被建立,當這個view需要顯示在螢幕上時將繪製指令提交給GPU還是執行DisplayList,當時這個view的位置發生改變時僅僅執行DisplayList就行了。
這裡寫圖片描述

當修改了view的某些可見元件的內容時候之前的DisplayList就不能再用了,這個時候就要建立一個新的DisplayList重新更新到螢幕上,當然啦任何view的內容發生變化 就會重新更新,這個過程的表現效能取決於view的複雜程度。
這裡寫圖片描述
當view的大小位置發生改變的時候會經過ViewHierarchy詢問各個View的新尺寸,view以及view下的子控制元件會重新測量大小。當然現在Android系統已經非常處理記錄並執行渲染管道,當然啦在處理View太多或者佈局更復雜或者自定義View系統執行功能的時間就會越長,當ViewHierarchy失控的時候執行這些功能的時間是和ViewHierarchy中所需要處理的節點數成正比的,View更多或者View更復雜處理的時間就越長,造成的這種浪費的首要原因就是ViewHierarchy包含了太多的無用View,這些view不會顯示在螢幕上,一旦發生測量操作就會拖累效能。

這裡寫圖片描述

針對以上問題有一款Hierarchy Viewer工具來查詢這些無用View

Hierarchy Viewer

Hierarchy Viewer讓佈局顯示更加結構化一目瞭然,讓我們更容易理解這個結構內的獨特檢視的相對渲染效能,Hierarchy Viewer的使用在Udacity的教學視訊中第11個視訊開始講解。我們就來看看一個例子
這裡寫圖片描述

由圖可知這是一個listview 下面的是item的佈局程式碼

<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"

    android:orientation="horizontal"
    android:paddingBottom="@dimen/chat_padding_bottom">

    <ImageView
        android:id="@+id/chat_author_avatar"
        android:layout_width="@dimen/avatar_dimen"
        android:layout_height="@dimen/avatar_dimen"
        android:layout_margin="@dimen/avatar_layout_margin" />

    <LinearLayout
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:orientation="vertical">

        <RelativeLayout
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:orientation="horizontal"
            android:textColor="#78A">

            <TextView xmlns:android="http://schemas.android.com/apk/res/android"
                android:id="@+id/chat_author_name"
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:layout_alignParentLeft="true"
                android:gravity="bottom"
                android:padding="@dimen/narrow_space" />

            <TextView xmlns:android="http://schemas.android.com/apk/res/android"
                android:id="@+id/chat_datetime"
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:layout_alignParentRight="true"
                android:padding="@dimen/narrow_space"
                android:textStyle="italic" />
        </RelativeLayout>

        <TextView xmlns:android="http://schemas.android.com/apk/res/android"
            android:id="@+id/chat_text"
            android:layout_width="match_parent"
            android:layout_height="match_parent"
            android:padding="@dimen/narrow_space" />
    </LinearLayout>

</LinearLayout>

這是一個橫線佈局的線性佈局這個巢狀相對較深,這時候我們用Hierarchy Viewer來看看這裡的ui檢視結構
這裡寫圖片描述這裡寫圖片描述
由上圖可以看到item的佈局節點為三個,在第一個LinearLayout和下一個RelativeLayout他們的測量均為紅色,對於顏色的分析如下

  • 如果在葉節點或者ViewGroup中,只有極少的子節點,這可能反映出一個問題,應用可能在裝置上執行並不慢,但是你需要指導為什麼這個節點是紅色的,可以藉助Systrace或者Traceview工具,獲取更多額外的資訊;

  • 如果一個檢視組裡面有許多的子節點,並且測量階段呈現為紅色,則需要觀察下子節點的繪製情況;

  • 如果檢視層級結構中的根檢視,Messure階段為紅色,Layout階段為紅色,Draw階段為黃色,這個是比較常見的,因為這個節點是所有其它檢視的父類;

  • 如果檢視結構中的一個葉子節點,有20個檢視是紅色的Draw階段,這是有問題的,需要檢查程式碼裡面的onDraw方法,不應該在那裡呼叫。

現在我們採用相對佈局來進行優化,優化程式碼如下

<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="horizontal"
    android:paddingBottom="@dimen/chat_padding_bottom">

    <ImageView
        android:id="@+id/chat_author_avatar"

        android:layout_width="@dimen/avatar_dimen"
        android:layout_height="@dimen/avatar_dimen"
        android:layout_alignParentLeft="true"
        android:layout_margin="@dimen/avatar_layout_margin"
        android:src="@drawable/ic_launcher" />

    <TextView xmlns:android="http://schemas.android.com/apk/res/android"
        android:id="@+id/chat_author_name"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"

        android:layout_toRightOf="@+id/chat_author_avatar"
        android:gravity="bottom"
        android:padding="@dimen/narrow_space"
        android:text="2222" />

    <TextView xmlns:android="http://schemas.android.com/apk/res/android"

        android:id="@+id/chat_datetime"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_alignParentRight="true"
        android:layout_alignParentTop="true"
        android:padding="@dimen/narrow_space"
        android:text="2222"
        android:textStyle="italic" />


    <TextView xmlns:android="http://schemas.android.com/apk/res/android"
        android:id="@+id/chat_text"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_below="@+id/chat_author_name"
        android:layout_toRightOf="@+id/chat_author_avatar"
        android:padding="@dimen/narrow_space"
        android:text="2222" />


</RelativeLayout>

從XML佈局程式碼中相比沒有優化之前的程式碼是不是清爽了很多,這是我們在來看看Hierarchy Viewer的情況
這裡寫圖片描述
現在看起來是不是更加清爽,好了總結一下:在我們開發時佈局的時候儘可能的採用相對佈局進行佈局,這樣能夠最大的減少佈局巢狀,在開始的時候就應該注意到多重巢狀佈局帶來的效能問題,這些在後期的效能優化當中也是能夠減少工作量的嘛。

後記:這邊博文僅僅作為學習的一個筆記,文中有錯誤或者其他問題歡迎大佬吐槽