六、Android效能優化之UI卡頓分析之渲染效能優化

weixin_34320159發表於2018-04-26

渲染功能是應用程式最普遍的功能,開發任何應用程式都是這樣,一方面,設計師要求為使用者展現可用性最高的超
然體驗,另一方面,那些華麗的圖片和動畫,並不是在所有的裝置上都能劉暢地執行。我們來了解一下什麼是渲染效能。
首先,我們要知道Android系統每隔16ms就重新繪製一次Activity,也就是說,我們的應用必須在16ms內完成螢幕重新整理的全部邏輯操作,這樣才能達到每秒60幀,然而這個每秒幀數的引數由手機硬體所決定,現在大多數手機螢幕重新整理率是60赫茲(赫茲是國際單位制中頻率的單位,它是每秒中的週期性變動重複次數的計量),也就是說我們有16ms(1000ms/60次=16.66ms)的時間去完成每幀的繪製邏輯操作,如果錯過了,比如說我們花費34ms才完成計算,那麼就會出現我們稱之為丟幀的情況。

簡單理解16ms應該完成所有事情
4768590-6fddf8feb66d08d8
image

渲染管線

Android系統的渲染管線分為兩個關鍵元件:CPU和GPU,它們共同工作,在螢幕上繪製圖片,每個元件都有自身定義>的特定流程。我們必須遵守這些特定的操作規則才能達到效果。

4768590-d2698db8168debbe
image

在CPU方面,最常見的效能問題是不必要的佈局和失效,這些內容必須在檢視層次結構中進行測量、清除並重新建立,引發這種問題通常有兩個原因:一是重建顯示列表的次數太多,二是花費太多時間作廢檢視層次並進行不必要的重繪,這兩個原因在更新顯示列表或者其他快取GPU資源時導致CPU工作過度。
在GPU方面,最常見的問題是我們所說的過度繪製(overdraw),通常是在畫素著色過程中,通過其他工具進行後期著色時浪費了GPU處理時間。
接下來我們將講解更多關於失效佈局和重繪的內容,以及如何使用SDK中的工具找出拖累應用效能的原因

4768590-fbdb3190fa480146
image

想要開發一款效能優越的應用,我們必須瞭解底層是如何執行的。有一個主要問題就是,Activity是如何繪製到螢幕上的?那些複雜的XML佈局檔案和標記語言,是如何轉化成使用者能看懂的影象的?
實際上,這是由格柵化操作來完成的,柵格化就是將例如字串、按鈕、路徑或者形狀的一些高階物件,拆分到不同的畫素上在螢幕上進行顯示,柵格化是一個非常費時的操作。我們所有人的手機裡面都有一塊特殊硬體,它就是影象處理器(GPU顯示卡的處理器),目的就是加快柵格化的操作,GPU在上個世紀90年代被引入用來幫助加快柵格化操作。

4768590-5a438d557f2b74e6
image

GPU使用一些指定的基礎指令集,主要是多邊形和紋理,也就是圖片,CPU在螢幕上繪製影象前會向GPU輸入這些指令,這一過程通常使用的API就是Android的OpenGL ES,這就是說,在螢幕上繪製UI物件時無論是按鈕、路徑或者核取方塊,都需要在CPU中首先轉換為多邊形或者紋理,然後再傳遞給GPU進行柵格化。

4768590-525c74f7ec5387bf
image

polygons多邊形和textures紋理

我們要知道,一個UI物件轉換為一系列多邊形和紋理的過程肯定相當耗時,從CPU上傳處理資料到GPU同樣也很耗時。所以很明顯,我們需要儘量減少物件轉換的次數,以及上傳資料的次數,幸虧,OpenGL ES API允許資料上傳到GPU後可以對資料進行儲存,當我們下次繪製一個按鈕時,只需要在GPU儲存器裡引用它,然後告訴OpenGL如何繪製就可以了,一條經驗之談:渲染效能的優化就是儘可能地上傳資料到GPU,然後儘可能長地在不修改的情況下儲存資料,因為每次上傳資源到GPU時,我們都會浪費寶貴的處理時間,Android系統的Honeycomb版本釋出之後,整個UI渲染系統就在GPU中執行,之後各個版本都在渲染系統效能方面有更多改進。
Android系統在降低、重新利用GPU資源方面做了很多工作,這方面完全不用擔心,舉例說,任何我們的主題所提供的資源,例如Bitmaps、Drawables等都是一起打包到統一的紋理當中,然後使用網格工具上傳到GPU,例如Nine Patches等,這樣每次我需要繪製這些資源時,我們就不用做任何轉換,他們已經儲存在GPU中了,大大加快了這些檢視型別的顯示。然而隨著UI物件的不斷升級,渲染流程也變得越來越複雜,例如說繪製影象,就是把圖片上傳到CPU儲存器,然後傳遞到GPU中進行渲染。路徑使用時完全另外一碼事,我們需要在CPU中建立一系列的多邊形,甚至在GPU中建立掩蔽紋理來定義路徑。繪製字元就更加複雜一些,首先我們需要在CPU中把字元繪製製成影象,然後把影象上傳到GPU進行渲染再返回到CPU,在螢幕上為字串的每個字元繪製一個正方形。

4768590-9b0105a4a634a95b
image
總結上述原因,在我們的繪製渲染機制裡面比較耗時的:
1.CPU計算時間

CPU的優化,從減輕加工View物件成Polygons和Texture來下手
View Hierarchy中包涵了太多沒有的View,這些view根本就不會顯示在螢幕上面,一旦觸發測量和佈局操作,就會拖累應用的效能表現。

2.CPU將計算好的Polygons和Texture傳遞到GPU的時候也需要時間

OpenGL ES API允許資料上傳到GPU後可以對資料進行儲存,快取到display list。因此,我們平移等操作一個view是幾乎不怎麼耗時的。

3.GPU進行柵格化
CPU優化建議

針對CPU的優化,從減輕加工View物件成Polygons和Texture來下手:

View Hierarchy中包涵了太多的沒有用的view,這些view根本就不會顯示在螢幕上面,一旦觸發測量和佈局操作,就會拖累應用的效能表現。那麼我們就需要利用工具進行分析。

如何找出裡面沒用的view呢?或者減少不必要的view巢狀。

我們利用工具:Hierarchy Viewer進行檢測,優化思想是:檢視自己的佈局,層次是否很深以及渲染比較耗時,然後想辦法能否減少層級以及優化每一個View的渲染時間。

我們開啟APP,然後開啟Android Device Monitor,然後切換到Hierarchy Viewer皮膚。除了看層次結構之外,還可以看到一些耗時的資訊:

4768590-c7ae99a58b966c2b
image

三個圓點分別代表:測量、佈局、繪製三個階段的效能表現。
1)綠色:渲染的管道階段,這個檢視的渲染速度快於至少一半的其他的檢視。
2)黃色:渲染速度比較慢的50%。
3)紅色:渲染速度非常慢。

優化思想:檢視自己的佈局,層次是否很深以及渲染比較耗時,然後想辦法能否減少層級以及優化每一個View的渲染時間。

4768590-174928bda1b21f4c
image
4768590-d24b9cf04fdd01d2
image

優化建議:

1.當我們的佈局是用的FrameLayout的時候,我們可以把它改成merge,可以避免自己的幀佈局和系統的ContentFrameLayout幀佈局重疊造成重複計算(measure和layout)。
2.使用ViewStub:當載入的時候才會佔用。不載入的時候就是隱藏的,僅僅佔用位置。

GPU優化建議就是一句話:儘量避免過度繪製(overdraw)

GPU的主要問題 -過度繪製(overdraw)
如果我們曾經粉刷過房子,我們應該知道,給牆壁粉刷工作量非常大,如果我們需要重新粉刷,第一次的粉刷就白乾了。同樣的道理,我們的應用程式會因為過度繪製,從而導致效能問題,如果我們想兼顧高效能和完美的設計,往往會碰到一種效能問題,即過度繪製。過度繪製是一個術語,指的是螢幕上的某個畫素點在同一幀的時間內被繪製了多次。假如我們有一堆重疊的UI卡片,最接近使用者的卡片在最上面,其餘卡片都藏在下面,也就是說我們花大力氣繪製的那些下面的卡片基本都是不可見的。

4768590-6643de8d014d9bce
image

問題就在於此,因為每次畫素經過渲染後,並不是使用者最後看到的部分,這就是在浪費GPU的時間。目前流行的一些佈局是一把雙刃劍,帶給我們漂亮視覺感受的同時,也造成過度繪製的問題,為了最大限度地提高應用程式的效能,我們必須儘量減少過度繪製。幸運的是,Android手機提供了檢視過度繪製情況的工具,在開發者選項中開啟“Show GPU overdraw”選項,手機螢幕顯示會出現一些異常不用過於驚慌,Android在螢幕上使用不同顏色,標記過度繪製的區域,如果某個畫素點只渲染了一次,我們看到的是它原來的顏色,隨著過度繪製的增多,標記顏色也會逐漸加深,例如1倍過度繪製會被標記為藍色,2倍、3倍、4倍過度繪製遵循同樣的模式。所以當我們除錯應用程式的使用者介面時,目標就是儘可能的減少過度繪製,將紅色區塊轉變成藍色區塊,為了完成目標有兩種清楚過度繪製的方法,首先要從檢視中清楚那些,不必要的背景和圖片,他們不會在最終渲染影象中顯示,記住,這些都會影響效能。其次,對檢視中重疊的螢幕區域進行定義,從而降低CPU和GPU的消耗,接下來我們深入瞭解過度繪製

1、背景經常容易造成過度繪製。

手機開發者選項裡面找到工具:Debug GPU overdraw,其中,不同顏色代表了繪製了幾次:

4768590-e82981329a407adc
image

舉例
由於我們佈局設定了背景,同時用到的MaterialDesign的主題會預設給一個背景。解決的辦法:將主題新增的背景去掉:

//將主題的背景去掉
getWindow().setBackgroundDrawable(null);

又例如我們的根佈局經常會設定重複的背景,那麼這時候就應該去掉一些不必要的背景。

還有的就是,我們在寫列表控制元件的時候,如果Item在沒有圖片的時候需要一個背景色的時候,那麼我們這時候就需要靈活地利用透明色來防止過度繪製:

        if (chat.getAuthor().getAvatarId() != 0) {
            Picasso.with(getContext()).load(chat.getAuthor().getAvatarId()).into(
                    chat_author_avatar);
        }
        chat_author_avatar.setBackgroundColor(chat.getAuthor().getColor());

4768590-4f1d0413dbf2a1a9
image
ListView item中設定圖片的同時,要設定背景Color.TRANSPARENT 防止因複用導致的過度繪製
if (chat.getAuthor().getAvatarId() == 0) {
    //沒有頭像的時候,需要把Drawable設定為透明,防止過度繪製(每次都要設定,因為Item會複用)
    Picasso.with(getContext()).load(android.R.color.transparent).into(chat_author_avatar);
    //沒有頭像的時候,需要設定預設的背景色
    chat_author_avatar.setBackgroundColor(chat.getAuthor().getColor());
} else {
    //有頭像的時候,直接設定頭像,並且把背景色設定為透明,同樣也是防止過度繪製
    Picasso.with(getContext()).load(chat.getAuthor().getAvatarId()).into(
            chat_author_avatar);
    chat_author_avatar.setBackgroundColor(Color.TRANSPARENT);
}

4768590-f130400edecc3755
image

發現設定的圖片過度繪製顏色由紅色變為綠色,減少了過渡繪製

2.自定義控制元件處理過度繪製。

如果我們的自定義控制元件存在一些被遮擋的不需要顯示的區域,可以通過畫布的裁剪來處理

public class DroidCardsView extends View {
    //圖片與圖片之間的間距
    private int mCardSpacing = 150;
    //圖片與左側距離的記錄
    private int mCardLeft = 10;

    private List<DroidCard> mDroidCards = new ArrayList<DroidCard>();

    private Paint paint = new Paint();

    public DroidCardsView(Context context) {
        super(context);
        initCards();
    }

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

    /**
     * 初始化卡片集合
     */
    protected void initCards(){
        Resources res = getResources();
        mDroidCards.add(new DroidCard(res,R.drawable.alex,mCardLeft));

        mCardLeft+=mCardSpacing;
        mDroidCards.add(new DroidCard(res,R.drawable.claire,mCardLeft));

        mCardLeft+=mCardSpacing;
        mDroidCards.add(new DroidCard(res,R.drawable.kathryn,mCardLeft));
    }

    @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);
    }
}

4768590-208b7258961fef3e
image
自定義控制元件中,通過畫布的裁剪,處理掉不需要顯示的區域

canvas.clipRect(c.x,0.0f,mDroidCards.get(i+1).x,c.height); //裁剪函式


public class DroidCardsView extends View {
    //圖片與圖片之間的間距
    private int mCardSpacing = 150;
    //圖片與左側距離的記錄
    private int mCardLeft = 10;

    private List<DroidCard> mDroidCards = new ArrayList<DroidCard>();

    private Paint paint = new Paint();

    public DroidCardsView(Context context) {
        super(context);
        initCards();
    }

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

    /**
     * 初始化卡片集合
     */
    protected void initCards(){
        Resources res = getResources();
        mDroidCards.add(new DroidCard(res,R.drawable.alex,mCardLeft));

        mCardLeft+=mCardSpacing;
        mDroidCards.add(new DroidCard(res,R.drawable.claire,mCardLeft));

        mCardLeft+=mCardSpacing;
        mDroidCards.add(new DroidCard(res,R.drawable.kathryn,mCardLeft));
    }

    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);

        for(int i=0;i<mDroidCards.size() -1;i++){
            DroidCard droidCard = mDroidCards.get(i);
            drawDroidCard(canvas,droidCard,i);
        }
        int last = mDroidCards.size() -1;
        if(last>=0){
            drawDroidCard(canvas,mDroidCards.get(last));
        }

        invalidate();
    }

    /**
     * 繪製DroidCard
     * @param canvas
     * @param c
     */
    private void drawDroidCard(Canvas canvas, DroidCard c,int i) {
      //  canvas.drawBitmap(c.bitmap,c.x,0f,paint);
        canvas.save();
        canvas.clipRect(c.x,0.0f,mDroidCards.get(i+1).x,c.height);
        canvas.drawBitmap(c.bitmap,c.x,0f,paint);
        canvas.restore();//裁剪和繪製完畢後恢復畫布
    }

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

    }
}

4768590-0c2179be86f50949
image

相關文章