Android性能優化篇之UI渲染性能優化

image

引言

1. Android性能優化篇之內存優化--內存泄漏

2.Android性能優化篇之內存優化--內存優化分析工具

3.Android性能優化篇之UI渲染性能優化

4.Android性能優化篇之計算性能優化

5.Android性能優化篇之電量優化(1)——電量消耗分析

6.Android性能優化篇之電量優化(2)

7.Android性能優化篇之網絡優化

8.Android性能優化篇之Bitmap優化

9.Android性能優化篇之圖片壓縮優化

10.Android性能優化篇之多線程并發優化

11.Android性能優化篇之數據傳輸效率優化

12.Android性能優化篇之程序啟動時間性能優化

13.Android性能優化篇之安裝包性能優化

14.Android性能優化篇之服務優化

介紹

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

1.關于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.怎么衡量渲染性能的好壞?

2.1 16ms

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


image1.png

image2.png

3.關于渲染管線

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


image3.png

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

3.1 GPU

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

image4.png

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


image5.png

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

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

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

4.Hierarchy Viewer工具介紹

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

使用步驟?
(1).打開Android Device Monitor
image9.png
(2).選擇Hierarchy Viewer選項卡
image10.png
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.問題分析以及解決方案

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就夠了.
image6.png
2.如果你修改了View中的某些可見組件的內容,那么之前的DisplayList就無法繼續使用了,我們需要重新創建一個DisplayList并重新執行渲染指令更新到屏幕上。任何時候View中的繪制內容發生變化時,都會需要重新創建DisplayList,渲染DisplayList,更新到屏幕上等一系列操作。這個流程的表現性能取決于你的View的復雜程度,View的狀態變化以及渲染管道的執行性能。
image7.png
3.如果某個View的大小需要增大到目前的兩倍,在增大View大小之前,需要通過父View重新計算并擺放其他子View的位置。修改View的大小會觸發整個HierarcyView的重新計算大小的操作。
image8.png
5.1.2 嵌套結構優

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


image13.png

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


image11.png

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


image12.png
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處理時間。

image14.png

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

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


image15.png
GPU Profiling

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


image1.jpg

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


image2.jpg

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


image3.jpg
  • 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就不會被執行浪費資源。


image16.png

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


image17.png

代碼:

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

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

剪輯過后效果:


image18.png

代碼:

    @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。這個操作有可能會影響到幀率,并使得用戶感知到性能問題(卡頓)。


image19.png

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


image20.png

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

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

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 230,321評論 6 543
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 99,559評論 3 429
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 178,442評論 0 383
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 63,835評論 1 317
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 72,581評論 6 412
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 55,922評論 1 328
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,931評論 3 447
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 43,096評論 0 290
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 49,639評論 1 336
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 41,374評論 3 358
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 43,591評論 1 374
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 39,104評論 5 364
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,789評論 3 349
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 35,196評論 0 28
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 36,524評論 1 295
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 52,322評論 3 400
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 48,554評論 2 379

推薦閱讀更多精彩內容