六腐晾、Android性能優(yōu)化之UI卡頓分析之渲染性能優(yōu)化

渲染功能是應(yīng)用程序最普遍的功能,開發(fā)任何應(yīng)用程序都是這樣丐一,一方面赴魁,設(shè)計師要求為用戶展現(xiàn)可用性最高的超
然體驗,另一方面钝诚,那些華麗的圖片和動畫颖御,并不是在所有的設(shè)備上都能劉暢地運行。我們來了解一下什么是渲染性能凝颇。
首先潘拱,我們要知道Android系統(tǒng)每隔16ms就重新繪制一次Activity,也就是說拧略,我們的應(yīng)用必須在16ms內(nèi)完成屏幕刷新的全部邏輯操作芦岂,這樣才能達到每秒60幀,然而這個每秒幀數(shù)的參數(shù)由手機硬件所決定垫蛆,現(xiàn)在大多數(shù)手機屏幕刷新率是60赫茲(赫茲是國際單位制中頻率的單位禽最,它是每秒中的周期性變動重復次數(shù)的計量),也就是說我們有16ms(1000ms/60次=16.66ms)的時間去完成每幀的繪制邏輯操作袱饭,如果錯過了川无,比如說我們花費34ms才完成計算,那么就會出現(xiàn)我們稱之為丟幀的情況虑乖。

簡單理解16ms應(yīng)該完成所有事情
GC回收時間過長導致卡頓

渲染管線

Android系統(tǒng)的渲染管線分為兩個關(guān)鍵組件:CPU和GPU懦趋,它們共同工作,在屏幕上繪制圖片疹味,每個組件都有自身定義>的特定流程仅叫。我們必須遵守這些特定的操作規(guī)則才能達到效果。

Android系統(tǒng)的渲染管線

在CPU方面糙捺,最常見的性能問題是不必要的布局和失效诫咱,這些內(nèi)容必須在視圖層次結(jié)構(gòu)中進行測量、清除并重新創(chuàng)建洪灯,引發(fā)這種問題通常有兩個原因:一是重建顯示列表的次數(shù)太多坎缭,二是花費太多時間作廢視圖層次并進行不必要的重繪,這兩個原因在更新顯示列表或者其他緩存GPU資源時導致CPU工作過度。
在GPU方面幻锁,最常見的問題是我們所說的過度繪制(overdraw)凯亮,通常是在像素著色過程中,通過其他工具進行后期著色時浪費了GPU處理時間哄尔。
接下來我們將講解更多關(guān)于失效布局和重繪的內(nèi)容假消,以及如何使用SDK中的工具找出拖累應(yīng)用性能的原因

渲染優(yōu)化

想要開發(fā)一款性能優(yōu)越的應(yīng)用,我們必須了解底層是如何運行的岭接。有一個主要問題就是富拗,Activity是如何繪制到屏幕上的?那些復雜的XML布局文件和標記語言鸣戴,是如何轉(zhuǎn)化成用戶能看懂的圖像的啃沪?
實際上,這是由格柵化操作來完成的窄锅,柵格化就是將例如字符串创千、按鈕、路徑或者形狀的一些高級對象入偷,拆分到不同的像素上在屏幕上進行顯示追驴,柵格化是一個非常費時的操作。我們所有人的手機里面都有一塊特殊硬件疏之,它就是圖像處理器(GPU顯卡的處理器)殿雪,目的就是加快柵格化的操作,GPU在上個世紀90年代被引入用來幫助加快柵格化操作锋爪。

加快格柵化的操作

GPU使用一些指定的基礎(chǔ)指令集丙曙,主要是多邊形和紋理,也就是圖片其骄,CPU在屏幕上繪制圖像前會向GPU輸入這些指令亏镰,這一過程通常使用的API就是Android的OpenGL ES,這就是說年栓,在屏幕上繪制UI對象時無論是按鈕拆挥、路徑或者復選框,都需要在CPU中首先轉(zhuǎn)換為多邊形或者紋理某抓,然后再傳遞給GPU進行柵格化。

polygons多邊形和textures紋理

我們要知道惰瓜,一個UI對象轉(zhuǎn)換為一系列多邊形和紋理的過程肯定相當耗時否副,從CPU上傳處理數(shù)據(jù)到GPU同樣也很耗時。所以很明顯崎坊,我們需要盡量減少對象轉(zhuǎn)換的次數(shù)备禀,以及上傳數(shù)據(jù)的次數(shù),幸虧,OpenGL ES API允許數(shù)據(jù)上傳到GPU后可以對數(shù)據(jù)進行保存曲尸,當我們下次繪制一個按鈕時赋续,只需要在GPU存儲器里引用它,然后告訴OpenGL如何繪制就可以了另患,一條經(jīng)驗之談:渲染性能的優(yōu)化就是盡可能地上傳數(shù)據(jù)到GPU纽乱,然后盡可能長地在不修改的情況下保存數(shù)據(jù),因為每次上傳資源到GPU時昆箕,我們都會浪費寶貴的處理時間鸦列,Android系統(tǒng)的Honeycomb版本發(fā)布之后,整個UI渲染系統(tǒng)就在GPU中運行鹏倘,之后各個版本都在渲染系統(tǒng)性能方面有更多改進薯嗤。
Android系統(tǒng)在降低、重新利用GPU資源方面做了很多工作纤泵,這方面完全不用擔心骆姐,舉例說,任何我們的主題所提供的資源捏题,例如Bitmaps诲锹、Drawables等都是一起打包到統(tǒng)一的紋理當中,然后使用網(wǎng)格工具上傳到GPU涉馅,例如Nine Patches等归园,這樣每次我需要繪制這些資源時,我們就不用做任何轉(zhuǎn)換稚矿,他們已經(jīng)存儲在GPU中了庸诱,大大加快了這些視圖類型的顯示。然而隨著UI對象的不斷升級晤揣,渲染流程也變得越來越復雜桥爽,例如說繪制圖像,就是把圖片上傳到CPU存儲器昧识,然后傳遞到GPU中進行渲染钠四。路徑使用時完全另外一碼事,我們需要在CPU中創(chuàng)建一系列的多邊形跪楞,甚至在GPU中創(chuàng)建掩蔽紋理來定義路徑缀去。繪制字符就更加復雜一些,首先我們需要在CPU中把字符繪制制成圖像甸祭,然后把圖像上傳到GPU進行渲染再返回到CPU缕碎,在屏幕上為字符串的每個字符繪制一個正方形。

總結(jié)上述原因池户,在我們的繪制渲染機制里面比較耗時的:
1.CPU計算時間

CPU的優(yōu)化咏雌,從減輕加工View對象成Polygons和Texture來下手
View Hierarchy中包涵了太多沒有的View凡怎,這些view根本就不會顯示在屏幕上面,一旦觸發(fā)測量和布局操作赊抖,就會拖累應(yīng)用的性能表現(xiàn)统倒。

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

OpenGL ES API允許數(shù)據(jù)上傳到GPU后可以對數(shù)據(jù)進行保存,緩存到display list氛雪。因此房匆,我們平移等操作一個view是幾乎不怎么耗時的。

3.GPU進行柵格化
CPU優(yōu)化建議

針對CPU的優(yōu)化注暗,從減輕加工View對象成Polygons和Texture來下手:

View Hierarchy中包涵了太多的沒有用的view坛缕,這些view根本就不會顯示在屏幕上面,一旦觸發(fā)測量和布局操作捆昏,就會拖累應(yīng)用的性能表現(xiàn)赚楚。那么我們就需要利用工具進行分析。

如何找出里面沒用的view呢骗卜?或者減少不必要的view嵌套宠页。

我們利用工具:Hierarchy Viewer進行檢測,優(yōu)化思想是:查看自己的布局寇仓,層次是否很深以及渲染比較耗時举户,然后想辦法能否減少層級以及優(yōu)化每一個View的渲染時間。

我們打開APP遍烦,然后打開Android Device Monitor俭嘁,然后切換到Hierarchy Viewer面板。除了看層次結(jié)構(gòu)之外服猪,還可以看到一些耗時的信息:

Hierarchy Viewer

三個圓點分別代表:測量供填、布局、繪制三個階段的性能表現(xiàn)罢猪。
1)綠色:渲染的管道階段近她,這個視圖的渲染速度快于至少一半的其他的視圖。
2)黃色:渲染速度比較慢的50%膳帕。
3)紅色:渲染速度非常慢粘捎。

優(yōu)化思想:查看自己的布局,層次是否很深以及渲染比較耗時危彩,然后想辦法能否減少層級以及優(yōu)化每一個View的渲染時間攒磨。

優(yōu)化前
優(yōu)化后

優(yōu)化建議:

1.當我們的布局是用的FrameLayout的時候,我們可以把它改成merge恬砂,可以避免自己的幀布局和系統(tǒng)的ContentFrameLayout幀布局重疊造成重復計算(measure和layout)咧纠。
2.使用ViewStub:當加載的時候才會占用。不加載的時候就是隱藏的泻骤,僅僅占用位置漆羔。

GPU優(yōu)化建議就是一句話:盡量避免過度繪制(overdraw)

GPU的主要問題 -過度繪制(overdraw)
如果我們曾經(jīng)粉刷過房子,我們應(yīng)該知道狱掂,給墻壁粉刷工作量非常大演痒,如果我們需要重新粉刷,第一次的粉刷就白干了趋惨。同樣的道理鸟顺,我們的應(yīng)用程序會因為過度繪制,從而導致性能問題器虾,如果我們想兼顧高性能和完美的設(shè)計讯嫂,往往會碰到一種性能問題,即過度繪制兆沙。過度繪制是一個術(shù)語欧芽,指的是屏幕上的某個像素點在同一幀的時間內(nèi)被繪制了多次。假如我們有一堆重疊的UI卡片葛圃,最接近用戶的卡片在最上面千扔,其余卡片都藏在下面,也就是說我們花大力氣繪制的那些下面的卡片基本都是不可見的库正。

過度繪制

問題就在于此曲楚,因為每次像素經(jīng)過渲染后,并不是用戶最后看到的部分褥符,這就是在浪費GPU的時間龙誊。目前流行的一些布局是一把雙刃劍,帶給我們漂亮視覺感受的同時喷楣,也造成過度繪制的問題趟大,為了最大限度地提高應(yīng)用程序的性能,我們必須盡量減少過度繪制抡蛙。幸運的是护昧,Android手機提供了查看過度繪制情況的工具,在開發(fā)者選項中打開“Show GPU overdraw”選項粗截,手機屏幕顯示會出現(xiàn)一些異常不用過于驚慌惋耙,Android在屏幕上使用不同顏色,標記過度繪制的區(qū)域熊昌,如果某個像素點只渲染了一次绽榛,我們看到的是它原來的顏色,隨著過度繪制的增多婿屹,標記顏色也會逐漸加深灭美,例如1倍過度繪制會被標記為藍色,2倍昂利、3倍届腐、4倍過度繪制遵循同樣的模式铁坎。所以當我們調(diào)試應(yīng)用程序的用戶界面時,目標就是盡可能的減少過度繪制犁苏,將紅色區(qū)塊轉(zhuǎn)變成藍色區(qū)塊硬萍,為了完成目標有兩種清楚過度繪制的方法,首先要從視圖中清楚那些围详,不必要的背景和圖片朴乖,他們不會在最終渲染圖像中顯示,記住助赞,這些都會影響性能买羞。其次,對視圖中重疊的屏幕區(qū)域進行定義雹食,從而降低CPU和GPU的消耗畜普,接下來我們深入了解過度繪制

1、背景經(jīng)常容易造成過度繪制婉徘。

手機開發(fā)者選項里面找到工具:Debug GPU overdraw漠嵌,其中,不同顏色代表了繪制了幾次:

Show GPU overdraw工具

舉例
由于我們布局設(shè)置了背景盖呼,同時用到的MaterialDesign的主題會默認給一個背景儒鹿。解決的辦法:將主題添加的背景去掉:

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

又例如我們的根布局經(jīng)常會設(shè)置重復的背景,那么這時候就應(yīng)該去掉一些不必要的背景几晤。

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

        if (chat.getAuthor().getAvatarId() != 0) {
            Picasso.with(getContext()).load(chat.getAuthor().getAvatarId()).into(
                    chat_author_avatar);
        }
        chat_author_avatar.setBackgroundColor(chat.getAuthor().getColor());
優(yōu)化前
ListView item中設(shè)置圖片的同時圾浅,要設(shè)置背景Color.TRANSPARENT 防止因復用導致的過度繪制
if (chat.getAuthor().getAvatarId() == 0) {
    //沒有頭像的時候,需要把Drawable設(shè)置為透明憾朴,防止過度繪制(每次都要設(shè)置狸捕,因為Item會復用)
    Picasso.with(getContext()).load(android.R.color.transparent).into(chat_author_avatar);
    //沒有頭像的時候,需要設(shè)置默認的背景色
    chat_author_avatar.setBackgroundColor(chat.getAuthor().getColor());
} else {
    //有頭像的時候众雷,直接設(shè)置頭像灸拍,并且把背景色設(shè)置為透明,同樣也是防止過度繪制
    Picasso.with(getContext()).load(chat.getAuthor().getAvatarId()).into(
            chat_author_avatar);
    chat_author_avatar.setBackgroundColor(Color.TRANSPARENT);
}
優(yōu)化后

發(fā)現(xiàn)設(shè)置的圖片過度繪制顏色由紅色變?yōu)榫G色砾省,減少了過渡繪制

2.自定義控件處理過度繪制鸡岗。

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

public class DroidCardsView extends View {
    //圖片與圖片之間的間距
    private int mCardSpacing = 150;
    //圖片與左側(cè)距離的記錄
    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);
    }
}
裁剪前過度繪制情況
自定義控件中编兄,通過畫布的裁剪轩性,處理掉不需要顯示的區(qū)域

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


public class DroidCardsView extends View {
    //圖片與圖片之間的間距
    private int mCardSpacing = 150;
    //圖片與左側(cè)距離的記錄
    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);

    }
}



裁剪后過度繪制情況

特別感謝:
小楠總
動腦學院Ricky

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市狠鸳,隨后出現(xiàn)的幾起案子揣苏,更是在濱河造成了極大的恐慌悯嗓,老刑警劉巖,帶你破解...
    沈念sama閱讀 211,817評論 6 492
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件舒岸,死亡現(xiàn)場離奇詭異绅作,居然都是意外死亡芦圾,警方通過查閱死者的電腦和手機蛾派,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 90,329評論 3 385
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來个少,“玉大人洪乍,你說我怎么就攤上這事∫菇梗” “怎么了壳澳?”我有些...
    開封第一講書人閱讀 157,354評論 0 348
  • 文/不壞的土叔 我叫張陵,是天一觀的道長茫经。 經(jīng)常有香客問我巷波,道長,這世上最難降的妖魔是什么卸伞? 我笑而不...
    開封第一講書人閱讀 56,498評論 1 284
  • 正文 為了忘掉前任抹镊,我火速辦了婚禮,結(jié)果婚禮上荤傲,老公的妹妹穿的比我還像新娘垮耳。我一直安慰自己,他們只是感情好遂黍,可當我...
    茶點故事閱讀 65,600評論 6 386
  • 文/花漫 我一把揭開白布终佛。 她就那樣靜靜地躺著,像睡著了一般雾家。 火紅的嫁衣襯著肌膚如雪铃彰。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 49,829評論 1 290
  • 那天芯咧,我揣著相機與錄音牙捉,去河邊找鬼。 笑死唬党,一個胖子當著我的面吹牛鹃共,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播驶拱,決...
    沈念sama閱讀 38,979評論 3 408
  • 文/蒼蘭香墨 我猛地睜開眼霜浴,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了蓝纲?” 一聲冷哼從身側(cè)響起阴孟,我...
    開封第一講書人閱讀 37,722評論 0 266
  • 序言:老撾萬榮一對情侶失蹤晌纫,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后永丝,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體锹漱,經(jīng)...
    沈念sama閱讀 44,189評論 1 303
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 36,519評論 2 327
  • 正文 我和宋清朗相戀三年慕嚷,在試婚紗的時候發(fā)現(xiàn)自己被綠了哥牍。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 38,654評論 1 340
  • 序言:一個原本活蹦亂跳的男人離奇死亡喝检,死狀恐怖嗅辣,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情挠说,我是刑警寧澤澡谭,帶...
    沈念sama閱讀 34,329評論 4 330
  • 正文 年R本政府宣布,位于F島的核電站损俭,受9級特大地震影響蛙奖,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜杆兵,卻給世界環(huán)境...
    茶點故事閱讀 39,940評論 3 313
  • 文/蒙蒙 一雁仲、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧拧咳,春花似錦伯顶、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,762評論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至阅签,卻和暖如春掐暮,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背政钟。 一陣腳步聲響...
    開封第一講書人閱讀 31,993評論 1 266
  • 我被黑心中介騙來泰國打工路克, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人养交。 一個月前我還...
    沈念sama閱讀 46,382評論 2 360
  • 正文 我出身青樓精算,卻偏偏與公主長得像,于是被迫代替她去往敵國和親碎连。 傳聞我的和親對象是個殘疾皇子灰羽,可洞房花燭夜當晚...
    茶點故事閱讀 43,543評論 2 349

推薦閱讀更多精彩內(nèi)容