1. 程式人生 > >(二)佈局渲染流程與原理 —— 優化卡頓

(二)佈局渲染流程與原理 —— 優化卡頓

一、CPU 和 GPU

1.區別

CPU 需要很強的通用性來處理邏輯計算、記憶體管理、顯示操作等,都使得CPU的內部結構異常複雜。因此,在沒有 GPU 的時代,CPU 的實際運算效能跟不上今天覆雜三維遊戲的要求,這時候 GPU 的設計出來了。GPU 面對的是型別高度統一的、相互無依賴的大規模資料和不需要被打斷的純淨的計算環境。

這裡寫圖片描述

黃色的 Control 為控制器,用於協調控制整個 CPU 的執行,包括取出指令、控制其他模組的執行等;
綠色的 ALU ( Arithmetic Logic Unit )是算術邏輯單元,用於進行數學、邏輯運算;
橙色的 Cache 和 DRAM 分別為快取和 RAM ,用於儲存資訊。

從結構圖可以看出, CPU 的控制器較為複雜,而 ALU 數量較少。因此 CPU 擅長各種複雜的邏輯運算,但不擅長數學尤其是浮點運算。

這裡寫圖片描述

2.在繪製中作用

1.這是一個在佈局檔案中的按鈕。

<Button
    android:layout_width="wrap_content"
    android:layout_height="wrap_content" />

2.通過 LayoutInflater 載入到記憶體中,變成一個 Button 物件,這個物件含有 left、top、right、buttom、width、height 等資訊。

3.CPU 經過計算,把 Button 物件處理成類似 svg 的多維向量影象。

4.CPU 將向量影象交給 GPU,GPU 進行柵格化,然後繪製,負責畫素填充。

柵格化:就是講向量圖形格式表示的影象轉換成點陣圖以用於顯示器。
這裡寫圖片描述

二、卡頓原理

1.60Hz 重新整理頻率由來

12 fps :由於人類眼睛的特殊生理結構,如果所看畫面之幀率高於每秒約 10-12 幀的時候,就會認為是連貫的

24 fps :有聲電影的拍攝及播放幀率均為每秒 24 幀,對一般人而言已算可接受

30 fps :早期的高動態電子遊戲,幀率少於每秒 30 幀的話就會顯得不連貫,這是因為沒有動態模糊使流暢度降低

60 fps 在與手機互動過程中,如觸控和反饋 60 幀以下人是能感覺出來的。 60 幀以上不能察覺變化

當幀率低於 60 fps 時感覺的畫面的卡頓和遲滯現象

2.繪製計算

從上面可以知道,一個 UI 物件,需要經過 CPU 轉化成多維向量影象,再經過 GPU 進行柵格化。

柵格化完成後進行重新整理顯示。但是介面不是已計算好就進行實時重新整理的,需要等待底層 UI 渲染。

Android 系統每隔 16ms 發出 VSYNC 訊號 (1000ms/60=16.66ms) ,觸發對 UI 進行渲染, 如果每次計算都成功這樣就能夠達到流暢的畫面所需要的 60fps ,為了能夠實現 60fps ,這意味著計算繪製內容的大多數操作都必須在 16ms 內完成。

3.卡頓原理

當某一幀畫面的渲染計算時間超過 16ms 的時候,垂直同步機制會讓顯示器硬體等待 CPU 和 GPU 計算完成後再進行渲染操作。

這樣會讓這一幀畫面多停留 16ms 甚至更多,看起來的效果就像是卡頓。

舉個例子:下方綠色這一幀計算需要 16ms, 紅色的這一幀計算需要用到 20ms,當時間到 16ms 時,UI 進行渲染,把綠色這一幀繪製出來,當時間到達 32ms 的時候,發現紅色這一幀還沒計算好,所以顯示的還是綠色這一幀,直到 48ms 的時候,發現紅色這一幀已經計算好了,才會顯示紅色這一幀。

這裡寫圖片描述

注:雖然紅色這一幀在 36ms 就計算好了,但是不會馬上進行顯示,需要等待下一次 UI 渲染。

4.卡頓優化

通過上面分析,我們可以知道,在 16ms 內,渲染計算沒有完成的話,將造成卡頓。這個過程主要做兩件事情:

第一件:將 UI 物件轉換為一系列多邊形和紋理
第二件: CPU 傳遞處理資料到 GPU 。

所以很明顯,我們要縮短這兩部分的時間,也就是說需要儘量減少物件轉換的次數,以及上傳資料的次數。

CPU 減少 xml 轉換成物件的時間。
GPU 減少重複繪製的時間。

三、巢狀佈局過深

這個主要就是儘量減少巢狀佈局,對於較較複雜的佈局,使用 RelativeLayout 替代多層次的 LinearLayout。

1. Hierarchy Viewer

我們可以藉助一些工具來檢視我們的佈局層級。現在 Hierarchy Viewer 在 SDK 裡找不到了,google 已經把它隱藏起來。並且 Hierarchy Viewer 只能在開發版手機或模擬器執行的限制。

這裡寫圖片描述

2.Layout Inspector

現在 Android Studio 已經不支援 Hierarchy Viewer,也可以使用 Layout Inspector 進行替代檢視佈局。

正常情況下,一個 ViewGroup 下如果只有一個 ViewGroup(scrollview 巢狀例外),那就要考慮這層佈局是否需要,不需要的話就應該去掉,以減少 CPU 對 xml 解析轉話為物件的時間。

四、過度繪製

1.過度繪製

GPU 的繪製過程,就跟刷牆一樣,一層層的進行。刷牆的時候,假如先刷一層紅色,接著再刷一層白色。雖然我們看到的是白色,但是牆是刷了兩遍的。

GPU 的繪製過程中也會出現這樣的情況,先繪製了一個圖層,然後再繪製一個圖層覆蓋在上面,即無用的圖層被繪製在底層,造成不必要的浪費。

過渡繪製主要分為兩種情況:

1.自定義控制元件中,onDraw 方法做了過多的重複繪製。

2.佈局層次太深,重疊性太強,多次進行繪製。

2.過度繪製檢視工具

在手機端的開發者選項裡,有 OverDraw 監測工具,除錯 GPU 過度繪製工具,其中顏色代表渲染的圖層情況,分別代表1層,2層,3層,4層覆蓋。
這裡寫圖片描述

藍色:繪製一次,無過度繪製
淡綠:繪製兩次
淡紅:繪製三次
深紅:繪製四次及以上

這是我的模擬器上的設定,有色盲的可以選擇第三個選項。
這裡寫圖片描述

這是開啟後的介面效果。
這裡寫圖片描述

3.計算渲染的耗時

當 View 中的繪製內容發生變化時,會重新執行建立 DisplayList,渲染 DisplayList,更新到螢幕上等一 系列操作。這個流程的表現效能取決於你的 View 的複雜程度,View 的狀態變化以及渲染管道的執行效能。

舉個例子,當View 的大小發生改變,DisplayList 就會重新建立,然後再渲染,而當 View 發生位移,則 DisplayList 不會重新建立,而是執行重新渲染的操作。

開啟設定中 GPU 呈現模式分析:
這裡寫圖片描述

開啟後效果:
這裡寫圖片描述

介面上會滾動顯示垂直的柱狀圖來表示每幀畫面所需要渲染的時間,柱狀圖越高表示花費的渲染時間越長。中間有一根綠色的橫線,代表16ms,我們需要確保每一幀花費的總時間都低於這條橫線,這樣才能夠避免出現卡頓的問題。

每一條柱狀線都包含三部分,

藍色:測量繪製Display List的時間。
紅色:OpenGL 渲染 Display List 所需要的時間。 
黃色:CPU等待GPU處理的時間。

注: OpenGL ES 是手持嵌入式裝置的 3DAPI,跨平臺的、功能完善的 2D 和 3D 圖形應用程式介面 API,有一套固定渲染管線流程。

DisplayList 在 Android 把 XML 佈局檔案轉換成 GPU 能夠識別並繪製的物件。這個操作是在 DisplayList 的幫助下完成的。DisplayList 持有所有將要交給 GPU 繪製到螢幕上的資料資訊。

4.安卓系統的優化

CPU 轉移到 GPU 是一件很麻煩的事情,所幸的是 OpenGL ES 可以把那些需要渲染的紋理 Hold 在 GPU Memory 裡面,在下次需要渲染的時候直接進行操作。所以如果你更新了 GPU 所 hold 住的紋理內容,那麼之前儲存的狀態就丟失了。

在 Android 裡面那些由主題所提供的資源,例如 Bitmaps , Drawables 都是一起打包到統一的 Texture 紋理當中,然後再傳遞到 GPU 裡面,這意味著每次你需要使用這些資源的時候,都是直接從紋理裡面進行獲取渲染的。當然隨著 UI 元件的越來越豐富,有了更多演變的形態。例如顯示圖片的時候,需要先經過 CPU 的計算載入到記憶體中,然後傳遞給 GPU 進行渲染。文字的顯示比較複雜,需要先經過 CPU 換算成紋理,然後交給 GPU 進行渲染,返回到 CPU 繪製單個字元的時候,再重新引用經過 GPU 渲染的內容。動畫則存在一個更加複雜的操作流程。

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

5.自定義控制元件過度繪製

比較典型的就是自拍疊加效果。

這裡寫圖片描述

    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        for (DroidCard c : mDroidCards){
            drawDroidCard(canvas, c);
        }

        invalidate();
    }

    /**
     * 繪製DroidCard
     * @param canvas
     * @param c
     */
    private void drawDroidCard(Canvas canvas, DroidCard c) {
        canvas.drawBitmap(c.bitmap,c.x,0f,paint);
    }

這個自定義控制元件沒有進行計算,直接連續繪製多張圖片,後面繪製的圖片會蓋住前面繪製圖片的部分割槽域,導致過度繪製。對於這樣的,可以進行邏輯計算,對第一張圖片要顯示的內容進行裁剪,然後再進行繪製。

優化後代碼:

 @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        for (int i = 0; i < mDroidCards.size() - 1; i++){
            drawDroidCard(canvas, mDroidCards,i);
        }

        drawLastDroidCard(canvas,mDroidCards.get(mDroidCards.size()-1));
        invalidate();
    }

    /**
     * 繪製最後一個DroidCard
     * @param canvas
     * @param c
     */
    private void drawLastDroidCard(Canvas canvas,DroidCard c) {
        canvas.drawBitmap(c.bitmap,c.x,0f,paint);
    }

    /**
     * 繪製DroidCard
     * @param canvas
     * @param mDroidCards
     * @param i
     */
    private void drawDroidCard(Canvas canvas,List<DroidCard> mDroidCards,int i) {
        DroidCard c = mDroidCards.get(i);
        canvas.save();
        canvas.clipRect((float)c.x,0f,(float)(mDroidCards.get(i+1).x),(float)c.height);
        canvas.drawBitmap(c.bitmap,c.x,0f,paint);
        canvas.restore();
    }

優化後效果:
這裡寫圖片描述

5.佈局過度繪製

佈局過度繪製造成的原因,主要是給深層次的佈局控制元件設定了多個背景顏色,導致其進行多次繪製。

1.去掉主題樣式的預設背景

    <style name="Theme.Base" parent="Theme.AppCompat.Light.DarkActionBar">
        <item name="android:windowBackground">@null</item>
    </style>

如果 Activity 的背景樣式不是必要的,把他置空,這樣會少繪製一層。

2.去掉容器背景

佈局檔案:

<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:background="@android:color/white"
    android:gravity="center"
    android:orientation="vertical">

    <TextView
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="內容"
        android:background="@android:color/white"/>

</LinearLayout>

效果:
這裡寫圖片描述

這是一個很簡單的佈局,可以看到,除了 TextView 外的區域全部被進行了一次繪製,TextView 除了文字區域,被繪製了兩次,呈現綠色。如果說我們只是為了單純設定背景為白色的話,TextView 是不需要再進行背景顏色的設定,因為他的背景已經是白色的了。

修改後佈局檔案:

<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:background="@android:color/white"
    android:gravity="center"
    android:orientation="vertical">

    <TextView
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="內容"/>

</LinearLayout>

修改後效果:
這裡寫圖片描述

這兩個顯示出來的效果是一樣的,但是 TextView 所在的區域,少進行了一次繪製。

小結:我們應該儘量保證介面大部分呈現為藍色、綠色。

五、總結

佈局優化:

1.佈局是否有可以刪除的多層巢狀

2.佈局是否有多層背景,過度繪製

3.自定義控制元件是都有進行裁剪