Android性能優化 之 UI 渲染

和你一起終身學習,這裏是程序員 Android

經典好文推薦,通過閱讀本文,您將收穫以下知識點:

一、關於ANR
二、怎麼衡量渲染性能的好壞?
三、關於渲染管線
四、Hierarchy Viewer工具介紹
五、問題分析以及解決方案

在用戶使用APP時,一方面想要華麗炫酷的動畫交互,一方面需要交互的的流暢運行,如何平衡設計和性能就需要我們不斷的學習和思考了。
UI渲染功能是最普通的功能,那麼怎麼衡量渲染性能的好壞?可能出現性能瓶頸的地方有哪些?造成卡頓的原因?如何解決卡頓?這些都是本章需要思考和解決的的問題。

一、關於ANR

1.1 什麼是ANR?

ANR全名Application Not Responding, 也就是"應用無響應".當操作在一段時間內系統無法處理時, 系統層面會彈出ANR對話框.

1.2 產生ANR的原因?

APP的響應是Activity Manage和Window Manage來監控的,系統產生ANR的原因:

  • 5s內無法響應用戶輸入事件
  • BoradCastReceiver在10s內沒有處理結束
    上面兩點的根本原因就是主線程有耗時操作。

1.3 如何避免?

  1. 耗時操作放到子線程操作
  2. I/O操作放到子線程
  3. 避免內存泄漏(內存不夠也會造成ANR,當時大多數情況是OOM)

1.4 ANR如何分析?

導出/data/anr/下的traces.txt,發現日誌來定位問題

adb pull data/anr/traces.txt ./

二、怎麼衡量渲染性能的好壞?

2.1 16ms

要知道Android系統每隔16ms就發出VSYNC信號重新繪製一次Activity,所以要在16ms內能夠完成繪製,這樣才能達到每秒60幀,然而這個每秒幀數的參數由手機硬件所決定,現在大多數手機屏幕刷新率是60赫茲(赫茲是國際單位制中頻率的單位,它是每秒中的週期性變動重複次數的計量),也就是說我們有16ms(1000ms/60次=16.66ms)的時間去完成每幀的繪製邏輯操作,就不會出現卡頓的現象,如果沒有完成,則會丟幀導致卡頓。

三、關於渲染管線

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

CPU負責包括Measure,Layout,Record,Execute的計算操作,GPU負責Rasterization(柵格化)操作。
在CPU方面,最常見的性能問題是不必要的佈局和失效,這些內容必須在視圖層次結構中進行測量、清除並重新創建,引發這種問題通常有兩個原因:一是重建顯示列表的次數太多,二是花費太多時間作廢視圖層次並進行不必要的重繪,這兩個原因在更新顯示列表或者其他緩存GPU資源時導致CPU工作過度。
在GPU方面,最常見的問題是我們所說的過度繪製(overdraw),通常是在像素着色過程中,通過其他工具進行後期着色時浪費了GPU處理時間。

3.1 GPU

瞭解Android中如何使用GPU進行畫面的渲染可以幫助我們更好的理解性能問題。我們的佈局文件是如何被繪製到屏幕上的?

Resterization柵格化是繪製那些Button,Shape,Path,String,Bitmap等組件最基礎的操作。它把那些組件拆分到不同的像素上進行顯示。這是一個很費時的操作,GPU的引入就是爲了加快柵格化的操作。

GPU使用一些指定的基礎指令集,主要是多邊形和紋理,也就是圖片,CPU在屏幕上繪製圖像前會向GPU輸入這些指令,這一過程通常使用的API就是Android的OpenGL ES,這就是說,在屏幕上繪製UI對象時無論是按鈕、路徑或者複選框,都需要在CPU中首先轉換爲多邊形或者紋理,然後再傳遞給GPU進行格柵化。
UI對象轉換爲一系列多邊形和紋理的過程肯定相當耗時,從CPU上傳處理數據到GPU同樣也很耗時。所以很明顯,我們需要儘量減少對象轉換的次數,以及上傳數據的次數,幸虧,OpenGL ES API允許數據上傳到GPU後可以對數據進行保存,當我們下次繪製一個按鈕時,只需要在GPU存儲器裏引用它,然後告訴OpenGL如何繪製就可以了,一條經驗之談:

3.2 渲染性能的優化就是儘可能地上傳數據到GPU,然後儘可能長地在不修改的情況下保存數據,因爲每次上傳資源到GPU時,我們都會浪費寶貴的處理時間.

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

四、Hierarchy Viewer工具介紹

Hierarchy Viewer可以很直接的呈現佈局的層次關係,視圖組件的各種屬性。 我們可以通過紅,黃,綠三種不同的顏色來區分佈局的Measure,Layout,Executive的相對性能表現如何。

使用步驟?

(1).打開Android Device Monitor

(2).選擇Hierarchy Viewer選項卡

4.1 設備連接問題

如果你是用的模擬器或者開發版手機的話則可以直接進行連接調試了,如果不是的話,官方提供了兩種方式,進行連接真機調試:

第一種,通過第三方庫,安裝和配置ViewServer,也是目前我在使用的方式,工具地址:點擊,步驟如下:
(1).添加依賴
project 下的 build.gradle

        allprojects {  
            repositories {  
                jcenter()  
                maven { url "https://jitpack.io" }  
            }  
        }  

module 下的 build.gradle

        dependencies {  
            ...  
            compile 'com.github.romainguy:ViewServer:017c01cd512cac3ec054d9eee05fc48c5a9d2de'  
        }  

(2).申請權限

    <uses-permission android:name="android.permission.INTERNET"/

(3).添加代碼

        public void onCreate(Bundle savedInstanceState) {  
            super.onCreate(savedInstanceState);  
            ViewServer.get(this).addWindow(this);  
        }  
        public void onDestroy() {  
            super.onDestroy();  
            ViewServer.get(this).removeWindow(this);  
        }  

        public void onResume() {  
            super.onResume();  
            ViewServer.get(this).setFocusedWindow(this);  
        } 

第二種,通過設置環境變量,export ANDROID_HVPROTO=ddm(可能對小米手機無用)

4.2 性能表示

這裏我們主要關注下面的三個圓圈,從左到右依次,代表View的Measure, Layout和Draw的性能,不同顏色代表不同的性能等級:

(1). 綠: 表示該View的此項性能比該View Tree中超過50%的View都要快;例如,一個綠點的測量時間意味着這個視圖的測量時間快於樹中的視圖對象的50%。

(2). 黃: 表示該View的此項性能比該View Tree中超過50%的View都要慢;例如,一個黃點佈局意味着這種觀點有較慢的佈局時間超過50%的樹視圖對象。

(3). 紅: 表示該View的此項性能是View Tree中最慢的;例如,一個紅點的繪製時間意味着花費時間最多的這一觀點在樹上畫所有的視圖對象。

五、問題分析以及解決方案

5.1 CPU

上面已經分析過了,CPU常見的性能問題是不必要的佈局和失效,引發這種問題通常有兩個原因:一是重建顯示列表的次數太多,二是花費太多時間作廢視圖層次並進行不必要的重繪。

5.1.1 佈局失效優化

Android需要把XML佈局文件轉換成GPU能夠識別並繪製的對象。這個操作是在DisplayList的幫助下完成的。DisplayList持有所有將要交給GPU繪製到屏幕上的數據信息,還有執行繪製操作的OpenGL命令列表。在某個View第一次需要被渲染時,Display List會因此被創建,當這個View要顯示到屏幕上時,我們會執行GPU的繪製指令來進行渲染。
那麼第二次渲染這個view會發生什麼呢?

1.如果View的Property屬性發生了改變(例如移動位置),我們就僅僅需要Execute Display List就夠了.

2.如果你修改了View中的某些可見組件的內容,那麼之前的DisplayList就無法繼續使用了,我們需要重新創建一個DisplayList並重新執行渲染指令更新到屏幕上。任何時候View中的繪製內容發生變化時,都會需要重新創建DisplayList,渲染DisplayList,更新到屏幕上等一系列操作。這個流程的表現性能取決於你的View的複雜程度,View的狀態變化以及渲染管道的執行性能。

3.如果某個View的大小需要增大到目前的兩倍,在增大View大小之前,需要通過父View重新計算並擺放其他子View的位置。修改View的大小會觸發整個HierarcyView的重新計算大小的操作。

5.1.2 嵌套結構優

提升佈局性能的關鍵點是儘量保持佈局層級的扁平化,避免出現重複的嵌套佈局。
我們先來看下列子,然後再來總結:
當前頁面有兩個條目,上面條目是使用LinearLayout中嵌套LinearLayout實現的,下麪條目使用一個RelativeLayout實現

我們使用Hierarchy Viewer工具來看下:

現在我們把上面一個條目改成和下麪條目一樣的實現,看下優化後的效果:

5.1.2.1 結果分析

紅色節點是代表應用性能慢的一個潛在問題,下面是幾個例子,如何來分析和解釋紅點的出現原因?

(1).如果在葉節點或者ViewGroup中,只有極少的子節點,這可能反映出一個問題,應用可能在設備上運行並不慢,但是你需要指導爲什麼這個節點是紅色的,可以藉助Systrace或者Traceview工具,獲取更多額外的信息

(2).如果一個視圖組裏面有許多的子節點,並且測量階段呈現爲紅色,則需要觀察下子節點的繪製情況

(3).如果視圖層級結構中的根視圖,Messure階段爲紅色,Layout階段爲紅色,Draw階段爲黃色,這個是比較常見的,因爲這個節點是所有其它視圖的父類

(4).如果視圖結構中的一個葉子節點,有20個視圖是紅色的Draw階段,這是有問題的,需要檢查代碼裏面的onDraw方法,不應該在那裏調用

5.1.2.2 優化建議

(1).沒有用的父佈局時指沒有背景繪製或者沒有大小限制的父佈局,這樣的佈局不會對UI效果產生任何影響。我們可以把沒有用的父佈局,通過<merge/標籤合併來減少UI的層次

(2).使用線性佈局LinearLayout排版導致UI層次變深,如果有這類問題,我們就使用相對佈局RelativeLayout代替LinearLayout,減少UI的層次

(3).不常用的UI被設置成GONE,比如異常的錯誤頁面,如果有這類問題,我們需要用<ViewStub/標籤,代替GONE提高UI性能

5.1.3 常用的優化示例

(1). include 標籤

include標籤常用於將佈局中的公共部分提取出來供其他layout共用,以實現佈局模塊化,這在佈局編寫方便提供了大大的便利。

(2). viewstub 標籤

viewstub標籤同include標籤一樣可以用來引入一個外部佈局,不同的是,viewstub引入的佈局默認不會擴張,即既不會佔用顯示也不會佔用位置,從而在解析layout時節省cpu和內存。
viewstub常用來引入那些默認不會顯示,只在特殊情況下顯示的佈局,如進度佈局、網絡失敗顯示的刷新佈局、信息出錯出現的提示佈局等。

    //第一種
    ViewStub stub = (ViewStub)findViewById(...)
    View stubView=  stub.inflate();
    //根據實際情況,顯示
    stubView.setVisibility()

    //第二種
    View viewStub = findViewById(R.id.network_error_layout);
    viewStub.setVisibility(View.VISIBLE);   // ViewStub被展開後的佈局所替換

注意:ViewStub所加載的佈局是不可以使用<merge標籤的

(3). merge 標籤

在使用了include後可能導致佈局嵌套過多,多餘不必要的layout節點,從而導致解析變慢

merge標籤可用於兩種典型情況:

(1). 佈局頂結點是FrameLayout且不需要設置background或padding等屬性,可以用merge代替,因爲Activity內容試圖的parent view就是個FrameLayout,所以可以用merge消除只剩一個。
(2). 某佈局作爲子佈局被其他佈局include時,使用merge當作該佈局的頂節點,這樣在被引入時頂結點會自動被忽略,而將其子節點全部合併到主佈局中。

5.2 GPU

在GPU方面,最常見的問題是我們所說的過度繪製(overdraw),通常是在像素着色過程中,通過其他工具進行後期着色時浪費了GPU處理時間。

過度繪製描述的是屏幕上的某個像素在同一幀的時間內被繪製了多次。在多層次重疊的UI結構裏面,如果不可見的UI也在做繪製的操作,會導致某些像素區域被繪製了多次。這樣就會浪費大量的CPU以及GPU資源。
當設計上追求更華麗的視覺效果的時候,我們就容易陷入採用複雜的多層次重疊視圖來實現這種視覺效果的怪圈。這很容易導致大量的性能問題,爲了獲得最佳的性能,我們必須儘量減少Overdraw的情況發生。

幸運的是,我們可以通過手機設置裏面的開發者選項,打開Show GPU Overdraw的選項,觀察UI上的Overdraw情況。

GPU Profiling

從Android M系統開始,系統更新了GPU Profiling的工具來幫助我們定位UI的渲染性能問題。早期的CPU Profiling工具只能粗略的顯示出Process,Execute,Update三大步驟的時間耗費情況。

但是僅僅顯示三大步驟的時間耗費情況,還是不太能夠清晰幫助我們定位具體的程序代碼問題,所以在Android M版本開始,GPU Profiling工具把渲染操作拆解成如下8個詳細的步驟進行顯示。

舊版本中提到的Proces,Execute,Update還是繼續得到了保留,他們的對應關係如下:

  • Sync & Upload:通常表示的是準備當前界面上有待繪製的圖片所耗費的時間,爲了減少該段區域的執行時間,我們可以減少屏幕上的圖片數量或者是縮小圖片本身的大小。
  • Measure & Layout:這裏表示的是佈局的onMeasure與onLayout所花費的時間,一旦時間過長,就需要仔細檢查自己的佈局是不是存在嚴重的性能問題。
  • Animation:表示的是計算執行動畫所需要花費的時間,包含的動畫有ObjectAnimator,ViewPropertyAnimator,Transition等等。一旦這裏的執行時間過長,就需要檢查是不是使用了非官方的動畫工具或者是檢查動畫執行的過程中是不是觸發了讀寫操作等等。
  • Input Handling:表示的是系統處理輸入事件所耗費的時間,粗略等於對於的事件處理方法所執行的時間。一旦執行時間過長,意味着在處理用戶的輸入事件的地方執行了複雜的操作。
  • Misc/Vsync Delay:如果稍加註意,我們可以在開發應用的Log日誌裏面看到這樣一行提示:I/Choreographer(691): Skipped XXX frames! The application may be doing too much work on its main thread。這意味着我們在主線程執行了太多的任務,導致UI渲染跟不上vSync的信號而出現掉幀的情況。

上面八種不同的顏色區分了不同的操作所耗費的時間,爲了便於我們迅速找出那些有問題的步驟,GPU Profiling工具會顯示16ms的閾值線,這樣就很容易找出那些不合理的性能問題,再仔細看對應具體哪個步驟相對來說耗費時間比例更大,結合上面介紹的細化步驟,從而快速定位問題,修復問題。

5.2.1 優化建議

(1).移除Window默認的Background

    getWindow().setBackgroundDrawable(null);

(2).移除XML佈局文件中非必需的Background

(3).按需顯示佔位背景圖片

在給ImageView設置圖片時,判斷是否獲取到對應的Bitmap,在獲取到圖像之後,把ImageView的Background設置爲Transparent,只有當圖像沒有獲取到的時候才設置對應的Background佔位圖片,這樣可以避免因爲設置背景圖而導致的過度渲染。

(4).剪輯不顯示的UI組件

對不可見的UI組件進行繪製更新會導致Overdraw。例如Nav Drawer從前置可見的Activity滑出之後,如果還繼續繪製那些在Nav Drawer裏面不可見的UI組件,這就導致了Overdraw。爲了解決這個問題,Android系統會通過避免繪製那些完全不可見的組件來儘量減少Overdraw。那些Nav Drawer裏面不可見的View就不會被執行浪費資源。

但是不幸的是,對於那些過於複雜的自定義的View(通常重寫了onDraw方法),Android系統無法檢測在onDraw裏面具體會執行什麼操作,系統無法監控並自動優化,也就無法避免Overdraw了。但是我們可以通過canvas.clipRect()來幫助系統識別那些可見的區域。這個方法可以指定一塊矩形區域,只有在這個區域內纔會被繪製,其他的區域會被忽視。這個API可以很好的幫助那些有多組重疊組件的自定義View來控制顯示的區域。同時clipRect方法還可以幫助節約CPU與GPU資源,在clipRect區域之外的繪製指令都不會被執行,那些部分內容在矩形區域內的組件,仍然會得到繪製。
除了clipRect方法之外,我們還可以使用canvas.quickreject()來判斷是否沒和某個矩形相交,從而跳過那些非矩形區域內的繪製操作。
下面我們來看個實例:

代碼:

    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        if (mCardList != null && mCardList.size()  0) {
            for (int i = 0; i < mCardList.size(); i++) {
                mCardLeft = i * mCardSpacing;
                drawCard(canvas, mCardList.get(i), mCardLeft, 0);
            }
        }
    }

    private void drawCard(Canvas canvas, CardItem card, int left, int top) {
        Bitmap mBitmap = getBitmap(card.resId);
        canvas.drawBitmap(mBitmap, left, top, mPaint);
    }

    private Bitmap getBitmap(int resId) {
        return BitmapFactory.decodeResource(this.getResources(), resId);
    }

我們看到撲克牌有不可見的區域但是還是被繪製了,導致過度繪製。下面我們進行剪輯。

剪輯過後效果:

代碼:

    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        if (mCardList != null && mCardList.size()  0) {
            for (int i = 0; i < mCardList.size()-1; i++) {
                mCardLeft = i * mCardSpacing;
                canvas.save();
                canvas.clipRect(mCardLeft,
                        0f,
                        mCardLeft + mCardSpacing,
                        mCardList.get(i).getHeight());
                drawCard(canvas, mCardList.get(i), mCardLeft, 0);
                canvas.restore();
            }
            drawCard(canvas, mCardList.get(mCardList.size()-1), mCardLeft + mCardSpacing, 0);
        }
    }

注意:有些過度繪製對於運行性能,可能是必要的也是可以接受的,比如說Android的ActionBar,但是,如果我們希望應用體驗更進一步,我們可以考慮儘可能地減少過度繪製。

5.3 其他問題引起的卡頓分析

5.3.1 內存抖動

內存抖動是因爲在短時間內大量的對象被創建又馬上被釋放。瞬間產生大量的對象會嚴重佔用Young Generation的內存區域,當達到閥值,剩餘空間不夠的時候,會觸發GC從而導致剛產生的對象又很快被回收。即使每次分配的對象佔用了很少的內存,但是他們疊加在一起會增加Heap的壓力,從而觸發更多其他類型的GC。這個操作有可能會影響到幀率,並使得用戶感知到性能問題(卡頓)。

解決上面的問題有簡潔直觀方法,如果你在Memory Monitor裏面查看到短時間發生了多次內存的漲跌,這意味着很有可能發生了內存抖動。

同時我們還可以通過Allocation Tracker來查看在短時間內,同一個棧中不斷進出的相同對象。這是內存抖動的典型信號之一。

當你大致定位問題之後,接下去的問題修復也就顯得相對直接簡單了。例如,你需要避免在for循環裏面分配對象佔用內存,需要嘗試把對象的創建移到循環體之外,自定義View中的onDraw方法也需要引起注意,每次屏幕發生繪製以及動畫執行過程中,onDraw方法都會被調用到,避免在onDraw方法裏面執行復雜的操作,避免創建對象。對於那些無法避免需要創建對象的情況,我們可以考慮對象池模型,通過對象池來解決頻繁創建與銷燬的問題,但是這裏需要注意結束使用之後,需要手動釋放對象池中的對象。

參考鏈接:https://www.jianshu.com/p/82f4fa395d9f

至此,本篇已結束。轉載網絡的文章,小編覺得很優秀,歡迎點擊閱讀原文,支持原創作者,如有侵權,懇請聯繫小編刪除,歡迎您的建議與指正。同時期待您的關注,感謝您的閱讀,謝謝!

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章