做過Android開發(fā)的人都知道,有時候我們給app加上一些絢麗的動畫,運行app的時候卻發(fā)現(xiàn)app出現(xiàn)明顯的卡頓,這嚴重影響了用戶的體驗。接下來我要從渲染來講解為什么會出現(xiàn)這種問題以及如何解決這種問題?
首先,我們來談?wù)勪秩荆?/b>

渲染功能是應(yīng)用程序最普遍的功能,Android系統(tǒng)每隔16ms重新繪制一次Activity,也就是說,你的應(yīng)用程序必須在16ms內(nèi)完成屏幕刷新的全部邏輯操作,這樣才能達到每秒60幀。這個每秒幀數(shù)的參數(shù)實際上來源于手機硬件,定義了屏幕每秒刷新速度有多快,我們大多數(shù)手機屏幕刷新率大概在60赫茲,這就意味著你有60ms的時間去完成每幀的繪制邏輯操作。如果錯過了,比如說你花費了24ms才完成計算,那些就會出現(xiàn)失幀的情況,Android系統(tǒng)嘗試在屏幕上繪制新的一幀,但是這一幀還沒準備好,所以畫面就不會刷新,用戶盯著同一張圖看了32ms而不是16ms。失幀情況下丟失的任何動畫,用戶很容易察覺出卡頓感,哪怕僅僅出現(xiàn)一次失幀,用戶都會發(fā)現(xiàn)動畫不是很順暢,如果出現(xiàn)多次失幀,用戶就會開始抱怨卡頓。
上面我們對每幀花費的事件有了清晰的了解,接下來我們來看看是什么原因?qū)е铝丝D,以及如何去解決應(yīng)用中的這些問題?
Android系統(tǒng)的渲染,分為兩個關(guān)鍵組件CPU和GPU。兩者共同在屏幕上繪制圖片,每個組件都有自身定義的特定流程,你必須遵守這些特定的操作規(guī)則才能達到效果。
CPU方面,最常見的性能問題是不必要的布局和失效, 這些內(nèi)容必須在視圖層析結(jié)構(gòu)中進行測量、清除并重新創(chuàng)建。引發(fā)這種問題通常有兩個原因,一是重建顯示列表的次數(shù)太多,而是花費太多的事件作廢視圖層次,并進行不必要的重繪,這兩個原因在更新顯示列表,或者其他緩存GPU資源時,導(dǎo)致CPU工作過度;第二個問題主要出在GPU方面,
就是我們所說的透支,通常是在像素著色過程中,通過其他工具進行后期著色,浪費了GPU處理時間。
下圖中可以看出渲染的常見三個問題,不必要的布局layouts、失效invalidations、過度繪制(overdraw)

想要開發(fā)一款性能優(yōu)越的app,你必須了解底層是如何運行的,如果你不知道硬件是如何運行的,你就無法熟練使用它。Activity是如何繪制到屏幕上的?或者說,那些復(fù)雜的xml布局文件和標記語言是如何轉(zhuǎn)換成用戶能看懂的圖像?
實際上,這是由格柵化操作來完成的,格柵化將諸如字符串、按鈕、路勁或者形狀的一些高級對象拆分到不同的像素上在屏幕上進行顯示。格柵化是一個非常耗時的操作,也就是說你的手機里有一塊特殊硬件,目的就是加快格柵化的操作,圖像處理單元,也就是GPU,是在上個世紀90年代被引入的主流電腦,幫助加快格柵化操作。

現(xiàn)在GPU使用一些指定的基礎(chǔ)指令集,主要多邊形和紋理,也就是圖片。CPU在屏幕上繪制圖像前會向GPU輸入這些指令,這一過程通常使用的API就是Android的OpenGL ES。這就是說,在屏幕上繪制UI對象時,無論是按鈕、路徑或者復(fù)選框,都需要在CPU中首先轉(zhuǎn)換為多邊形或者紋理,然后在傳遞給GPU進行格柵化。你可以想象,一個UI對象轉(zhuǎn)換為一系列多邊形和紋理的過程,肯定是相當(dāng)耗時;從CPU上傳數(shù)據(jù)到GPU同時也很耗時,所以,你需要盡量減少對象轉(zhuǎn)換的次數(shù)以及上傳數(shù)據(jù)的次數(shù)。幸虧OpenGL ES API允許數(shù)據(jù)上傳到GPU后,可以對數(shù)據(jù)進行保存,只需要在GPU存儲器里引用它,然后告訴OpenGL如何繪制。
歸納起來,渲染性能的優(yōu)化就是盡可能快的上傳數(shù)據(jù)到GPU,然后盡可能長的在不修改的條件下保存數(shù)據(jù)。因為每次上傳資源到GPU時,你都會浪費寶貴的處理時間。
Android系統(tǒng)單Honeycomb版本發(fā)布之后,整個UI渲染系統(tǒng)就在GPU中運行,之后各個版本都在渲染系統(tǒng)性能方面有更大改進,Android系統(tǒng)在降低、重新利用GPU資源方面做了很多工作,這方面你完全不用擔(dān)心。比如說,任何你的主題所提供的資源,如Bitmaps、Drawables等都是一起打包到統(tǒng)一的紋理中,然后使用網(wǎng)格工具(如Nine Patches)上傳到GPU,這就意味著,你每次需要繪制這些資源時,你不用做任何轉(zhuǎn)換,因為它們已經(jīng)存儲在GPU中了,大大加快了這些視圖類型的顯示。然而隨著UI對象的不斷升級,渲染流程也變得越來越復(fù)雜,例如說,繪制圖像,就是把圖片上傳到CPU存儲器,然后傳遞到GPU中進行渲染,路徑使用是完全不同一碼事,你需要在CPU中創(chuàng)建一系列的多邊形,甚至在GPU中創(chuàng)建掩蔽紋理來定義路徑;還有,繪制字符也很復(fù)雜,首先我們需要在CPU中把字符繪制成圖像,然后把圖像上傳到GPU進行渲染再返回到CPU,在屏幕上為字符串的每個字符繪制一個正方形。
上面我們稍微了解了渲染的工作機制,接下來我們就是說一個困擾我們大部分程序的問題,GPU性能問題瓶頸——Overdraw(過度繪制)。
如果你曾今裝修過房子,應(yīng)該知道,可能墻壁的裝飾不符合我們的美觀,我們就需要在以前的基礎(chǔ)上在刷一遍漆,新刷的漆就覆蓋了以前的,墻壁可能刷了好幾次,讓費了資源,也讓費了時間,這個情形很像Android中的Overdraw問題。

Overdraw,指在一幀的時間內(nèi)像素被繪制了多少次,理論上一個像素每次只繪制一次是最優(yōu)的。但是由于我們的布局一般都是重疊的,不管是可見的,還是不可見的,都可能繪制一次,這就導(dǎo)致了一些像素會被多次繪制。這是一個非常大的問題,因為我們的渲染的像素對我們最終顯示在屏幕上沒有任何用處,造成了GP性能的浪費。
為了最大化優(yōu)化你應(yīng)用的性能,你就應(yīng)該盡量避免Overdraw。
那么我們?nèi)绾蝸聿榭磻?yīng)用是否Overdraw呢?這很簡單,只需要打開手機的設(shè)置——開發(fā)者選項——調(diào)試GPU過度繪制,如下圖

打開以后,你將會看到自己的應(yīng)用上出現(xiàn)幾種顏色變化,每種顏色代表的含義如下

沒有顏色: 意味著沒有overdraw。像素只畫了一次。
藍色: 意味著overdraw 1倍。像素繪制了兩次。大片的藍色還是可以接受的(若整個窗口是藍色的,可以擺脫一層)。
綠色: 意味著overdraw 2倍。像素繪制了三次。中等大小的綠色區(qū)域是可以接受的但你應(yīng)該嘗試優(yōu)化、減少它們。
淺紅: 意味著overdraw 3倍。像素繪制了四次,小范圍可以接受。
暗紅: 意味著overdraw 4倍。像素繪制了五次或者更多。這是錯誤的,要修復(fù)它們。
清除Overdraw有兩種主要的方式,首先,清除不必要的背景和圖片:其次,你可以定義view隱藏的屏幕位置區(qū)域,也就是說,隱藏起來的部分不渲染,這樣會減少CPU和GPU的開銷。
一、清除不必要的背景和圖片
1. 去掉window的默認背景
當(dāng)我們使用了Android自帶的一些主題時,window會被默認添加一個純色的背景,這個背景是被DecorView持有的。當(dāng)我們的自定義布局時又添加了一張背景圖或者設(shè)置背景色,那么DecorView的background此時對我們來說是無用的,但是它會產(chǎn)生一次Overdraw,帶來繪制性能損耗。
去掉window的背景可以在onCreate()中setContentView()之后調(diào)用
getWindow().setBackgroundDrawable(null);
或者在theme中添加
android:windowbackground="null";
2. xml中去掉不必要的背景
根view設(shè)置了背景,如果子view的背景和根view一樣,或者子view不需要設(shè)置背景,那些字view不能設(shè)置背景,完全可以使用根view的背景顏色,否者,就會造成背景多次繪制。
3. 圖片設(shè)置背景,已加載到圖片則去掉背景
如果我們需要給imageview設(shè)置背景,通常我們可能直接用Picasso加載圖片到imageview,然后就調(diào)用imageview的setBackgroundColor方法,但是這樣會圖片和背景顏色會繪制兩只,造成Overdraw。
其實,我們完全可以在加載到圖片的時候去掉背景,在沒加載到圖片的時候設(shè)置背景,這樣就解決了Overdraw的問題,解決的代碼如下:
if (chat.getAuthor().getAvatarId() == 0) {
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);
}
二、自定義View的隱藏部分不渲染(clipRect和quickReject)
Android會設(shè)法避免繪制那些在最終圖片中不顯示的UI組件,這種優(yōu)化類型稱作剪切,它對UI性能非常重要。如果你能確定某個對象完全被阻擋,你完全沒有必要繪制它,事實上,這是最重要的性能優(yōu)化方法之一,而且是由Android系統(tǒng)執(zhí)行的。但是必行的是,這一技術(shù),無法應(yīng)對復(fù)雜的自定義控件,系統(tǒng)無法檢測onDraw具體會執(zhí)行什么操作,這些情況下,底層系統(tǒng)無法識別如何去繪制對象,系統(tǒng)很難將覆蓋的view從渲染管道中清除,
例如,這疊牌只有上面的牌可見,其他牌都被擋住了,這就意味著繪制那些重疊的像素就是浪費時間。為了解決這個問題,我們可以使用具有一些特別方法的Canvas類,去讓Android系統(tǒng)識別被遮擋的不需要繪制的部分,最有用的方法是Canvas.clipRect,它可以幫助你識別給定view的圖片邊界,邊界之外區(qū)域的任何繪制操作都會被忽略。如果你知道view的可見部分或者被遮擋的部分范圍,你就可以使用Canvas.clipRect定義邊界,可以避免遮擋區(qū)域的任何繪制操作。ClipRect API幫助系統(tǒng)識別出無需繪制的區(qū)域,對自定義view進行剪切時,這個方法也很有用處,比如說,如果你知道繪制對象在剪輯矩形之外,這個方法就非常好用。幸運的是,你不必要親自搞清楚重疊邏輯,我們可以使用Canvas.quickReject方法,判斷給定區(qū)域是否完全在剪輯矩形之外,這種情況下可以忽略全部繪制工作。
關(guān)于過度繪制就講到這里,是時候了解一下渲染管道中的CPU部分。
為了在屏幕上繪制某個東西,Android通常將高級XML文件轉(zhuǎn)換成GPU能夠識別的對象,然后顯示在屏幕上,這個操作是在DisplayList的幫助下完成的。DisplayList持有所有要交給GPU繪制到屏幕上的數(shù)據(jù)信息,包含GPU要繪制的全部對象的信息列表,還有執(zhí)行繪制操作的OpenGL命令列表,在某個view第一次需要被渲染時,DisplayList會因此被創(chuàng)建,當(dāng)這個view要顯示到屏幕上時,我們將繪制指令提交給GPU來執(zhí)行DisplayList。我們下次渲染這個view時,比如說位置發(fā)生了變化,我們僅僅需要執(zhí)行DisplayList就夠了,但是,若果你修改了view的某些可見組件的內(nèi)容,那么之前的DisplayList就不能用了,這時我們要重新創(chuàng)建一個DisplayList,重新執(zhí)行渲染指令并更新到屏幕上。

任何時候view的繪制內(nèi)容發(fā)生變化,都需要重新創(chuàng)建DisplayList,并重新執(zhí)行指定更新到屏幕上,這個流程的表現(xiàn)性能取決于你的view的復(fù)雜程度和視覺變化的類型。比如說,某個文本框尺寸突然變成當(dāng)前的兩倍,在改變尺寸前,需要通過父view重新計算,并擺放其他子view的位置,在這種情況下,我們改變了其中一個view,后面就會有很多工作要做,這些類型的視覺變化需要渲染管道的額外工作。當(dāng)你的尺寸發(fā)生變化時,出發(fā)了測量操作,會經(jīng)過整個View Hierarchy詢問各個View的新尺寸,你一旦改變View的大小就會觸發(fā)上述工程;如果你是改變對象位置或者布局中某個View重新擺放了子view,都會觸發(fā)布局操作,會觸發(fā)整個Hierarchy重新計算對象在屏幕上的新位置。
現(xiàn)在Android系統(tǒng)已經(jīng)非常有效的處理記錄并執(zhí)行渲染管道,除非你要自定義View或者通知繪制很多View,其他情況下一般不會耗費太多時間,測量和布局性能也很好。但是,當(dāng)你的View Hierarchy失控時,也更容易出現(xiàn)問題,執(zhí)行這個功能的時間是和你的View Hierarchy中需要處理的節(jié)點數(shù)成正比,系統(tǒng)需要處理的View越多,處理時間就越長。造成這些浪費的原因是,View Hierarchy中包含太多的無用View,這些View根本不會顯示在屏幕上,一旦觸發(fā)測量操作和布局操作,只會拖累應(yīng)用程序的性能。
當(dāng)然,Android的SDK中有一款叫做Hierarchy Viewer工具,可以幫助你查找并修復(fù)這個流氓View,刪除不必要的View,減少布局層級(這里就不介紹Hierarchy Viewer的使用了)。
關(guān)于CPU部分的優(yōu)化,可以從以下幾個方面來講
1. 使用Hierarachy Viewer來查找、刪除無用的View和層級
2. 在布局層級相同的情況下,能使用LinearLayout就不要使用RelativeLayout。因為LinearLayout效率更高,RelativeLayout的功能比較復(fù)雜,CPU渲染時間更長。
3. include標簽配合merge標簽使用,可以重用布局、減少布局層數(shù)
4、使用ViewStub。它是個非常輕量級的View,寬高都為0,因此本身不參與任何的布局和繪制過程。他的意義在于按需要加載所需的布局,需要的時候加載,不需要的時候就不加載進來,提高程序初始化的性能。
總而言之,渲染問題主要包括三個方面:背景重疊、自定義view重疊(重疊部分不必要繪制)、view的層級過多,想要減少渲染時間,從這個三面去考慮就可以了。