相信很多人都有這種經(jīng)歷,在使用app的過程中,突然間發(fā)現(xiàn)程序雖然在運行,但是這里停頓一下,那里停頓一下的卡頓現(xiàn)象,就像看上網(wǎng)看視頻一樣,緩沖不過來,視頻很卡,不能連續(xù)的看下去。造成這樣原因有很多,其中一種就是UI被過度繪制了。
UI過度繪制簡單的來說是指在一個界面中有很多元素,但是我們只需要更新某一小塊的元素,app卻把所有的元素都刷新一遍,這就造成過度繪制。

過度繪制造成UI卡頓的原因是因為它浪費大量的CPU以及GPU資源。手機原本為了保持視覺的流暢度,其屏幕刷新頻率是60hz,即在1000/60=16.67ms內(nèi)更新一幀。如果沒有完成任務(wù),就會發(fā)生掉幀的現(xiàn)象,也就是我們所說的卡頓。
這其中的原理比較復(fù)雜,大家可以看看大神是怎么說的,這里給個胡凱大神的文章地址:
Android性能優(yōu)化之渲染篇
http://hukai.me/android-performance-render/
debug GPU overdraw
有問題就必然有解決辦法,在Android系統(tǒng)內(nèi)部也有一個神器可以查看app的UI的過度繪制情況,在開發(fā)者選項中有個debug GPU overdraw(調(diào)試GPU過度繪制),打開之后有off(關(guān)閉),show overdraw areas(顯示過度繪制區(qū)域),show areas for Deuteranomaly(為紅綠癥患者顯示過度繪制區(qū)域)

我們選擇show overdraw areas,發(fā)現(xiàn)整個手機界面的顏色變了,在打開過度繪制選項后,其中的藍(lán)色,淡綠,淡紅,深紅代表了4種不同程度的Overdraw情況,我們的目標(biāo)就是盡量減少紅色Overdraw,看到更多的藍(lán)色區(qū)域。

Profile GPU rendering
其次android系統(tǒng)還內(nèi)置了Profile GPU rendering工具,這個工具也是在開發(fā)者選項中打開,它能夠以柱狀圖的方式顯示當(dāng)前界面的渲染時間

- 藍(lán)色代表測量繪制的時間,或者說它代表需要多長時間去創(chuàng)建和更新你的DisplayList.在Android中,一個視圖在可以實際的進(jìn)行渲染之前,它必須被轉(zhuǎn)換成GPU所熟悉的格式,簡單來說就是幾條繪圖命令,復(fù)雜點的可能是你的自定義的View嵌入了自定義的Path. 一旦完成,結(jié)果會作為一個DisplayList對象被系統(tǒng)送入緩存,藍(lán)色就是記錄了需要花費多長時間在屏幕上更新視圖(說白了就是執(zhí)行每一個View的onDraw方法,創(chuàng)建或者更新每一個View的Display List對象).
- 橙色部分表示的是處理時間,或者說是CPU告訴GPU渲染一幀的地方,這是一個阻塞調(diào)用,因為CPU會一直等待GPU發(fā)出接到命令的回復(fù),如果柱狀圖很高,那就意味著你給GPU太多的工作,太多的負(fù)責(zé)視圖需要OpenGL命令去繪制和處理.
- 紅色代表執(zhí)行的時間,這部分是Android進(jìn)行2D渲染 Display List的時間,為了繪制到屏幕上,Android需要使用OpenGl ES的API接口來繪制Display List.這些API有效地將數(shù)據(jù)發(fā)送到GPU,最總在屏幕上顯示出來.
在這里也放一個大神關(guān)于Profile GPU rendering的介紹
http://androidperformance.com/2015/04/19/Android-Performance-Patterns-4.html
下面我們開始對UI多度繪制開始實戰(zhàn)吧!
實戰(zhàn)項目地址
初始界面的問題
剛打開這個項目,我們就發(fā)現(xiàn)了在第一個有過度繪制問題,效果如下
存在問題
- 在按鈕overdraw上面就有個紅色的過度繪制區(qū)域
- 在文本框This is test的布局中也是紅色過度繪制區(qū)域
解決方法
-
要解決這個問題,我們首先需要分析這是怎么引起的。分析到activity_main.xml的布局文件時,發(fā)現(xiàn)這里使用了多個嵌套的LinearLayout布局,而且每個LinearLayout都會使用一次android:background設(shè)置一次自己的背景顏色,他們造成了過度繪制。
仔細(xì)分析在其中一個嵌套ImageView的LinearLayout布局背景顏色與最外層的背景顏色是一樣的,屬于不需要的背景色,因此將這個LinearLayout中的android:background屬性刪除,這時發(fā)現(xiàn)文本框布局已經(jīng)不再是紅色了
第一次優(yōu)化.png - 咋看之下一切都很完美,但其實整個ui其實還含有一個隱含的繪制效果,那邊是在activity中,使用setContentView(R.layout.activity_main)設(shè)置布局的時候,android會自動填充一個默認(rèn)的背景,而在這個UI中,我們使用了填充整個app的背景,因此不需要默認(rèn)背景,取消也很簡單,只需要在activity中的onCreate方法中添加這么一句就行了
getWindow().setBackgroundDrawable(null);
現(xiàn)在看最終優(yōu)化效果

OVERDRAWVIEW頁面的問題
在overdrawviewactivity中只有一個自定義的圖案,而這個自定義的圖案引起了過度繪制的問題
解決方法
- 首先這個也是填充了整個ui界面的繪制圖片,因此我們也在activity中的onCreate方法中添加getWindow().setBackgroundDrawable(null);取消默認(rèn)繪制。
- 繼續(xù)研究,發(fā)現(xiàn)過度繪制問題是由于OverDrawView類中的ondraw方法中多次繪制了矩形導(dǎo)致的,代碼如下:
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
int width = getWidth();
int height = getHeight();
mPaint.setColor(Color.GRAY);
canvas.drawRect(0, 0, width, height, mPaint);
mPaint.setColor(Color.CYAN);
canvas.drawRect(0, height/4, width, height, mPaint);
mPaint.setColor(Color.DKGRAY);
canvas.drawRect(0, height/3, width, height, mPaint);
mPaint.setColor(Color.LTGRAY);
canvas.drawRect(0, height/2, width, height, mPaint);
}
通過分析得知,顏色為GRAY的矩形的高度其實不需要設(shè)置為整個屏幕的高度,它的高度只需要設(shè)置為它所顯示范圍的高度就可以了,因此可以設(shè)為height/4。
其他的矩形也是同樣的道理,因此更改這里的代碼為:
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
int width = getWidth();
int height = getHeight();
mPaint.setColor(Color.GRAY);
canvas.drawRect(0, 0, width, height/4, mPaint);
mPaint.setColor(Color.CYAN);
canvas.drawRect(0, height/4, width, height/3, mPaint);
mPaint.setColor(Color.DKGRAY);
canvas.drawRect(0, height/3, width, height/2, mPaint);
mPaint.setColor(Color.LTGRAY);
canvas.drawRect(0, height/2, width, height, mPaint);
}
優(yōu)化的界面

BUSYONDRAW頻繁繪制
當(dāng)我們點擊BUSYONDRAW按鈕的時候,我們發(fā)現(xiàn)明顯的卡頓現(xiàn)象。在開發(fā)者選項中打開Profile GPU rendering選項,然后在次點擊BUSYONDRAW按鈕,發(fā)現(xiàn)這個頁面繪制渲染時間已經(jīng)突破天際了!

在初始的時候,藍(lán)色繪制時間占滿整個屏幕高度,這是造成卡頓的重要原因。
解決方法
- 首先卡頓現(xiàn)象是由ondraw方法中的for循環(huán)中的打印字符串引起的
for (int i = 0; i < 1000; i++) {
System.out.println("canvas = [" + canvas + "]" + i);
}
我們將其提取出來放在另一個線程中運行。
先是創(chuàng)建一個線程池:
private ExecutorService pool = Executors.newCachedThreadPool();
將要運行的耗時操作封裝成Runable對象并通過一個方法獲?。?/p>
@NonNull
private Runnable getCommand(final Canvas canvas) {
return new Runnable() {
@Override
public void run() {
for (int i = 0; i &lt; 1000; i++) {
System.out.println("canvas = [" + canvas + "]" + i);
}
}
};
}
最后在onDraw里運行放在子線程里運行:
pool.execute(getCommand(canvas));
現(xiàn)在進(jìn)入也不會卡頓了。
其次在ondraw中也不宜創(chuàng)建Paint()對象,因為app會頻繁調(diào)用ondraw對象,會造成內(nèi)存泄漏,因此需要將其提取為全局變量。
最后繪制了多個圓形圖案也造成了一定程度的卡頓,但由于功力不夠,暫時無法優(yōu)化。
