Android效能優化--過度繪製
渲染機制
前提知識
android 的渲染主要分為兩個元件:1.CPU 2.GPU,由這兩者共同完成在螢幕上繪製 。
CPU:中央處理器,它集成了運算,緩衝,控制等單元,包括繪圖功能.CPU將物件處理為多維圖形,紋理(Bitmaps、Drawables等都是一起打包到統一的紋理)。
GPU:一個類似於CPU的專門用來處理Graphics的處理器,用來幫助加快格柵化操作,當然,也有相應的快取資料(例如快取已經光柵化過的bitmap等)機制。
OpenGL ES:手持嵌入式裝置的3DAPI,跨平臺的、功能完善的2D和3D圖形應用程式介面API,有一套固定渲染管線流程。
DisplayList:把XML佈局檔案轉換成GPU能夠識別並繪製的物件。這個操作是在DisplayList的幫助下完成的。DisplayList持有所有將要交給GPU繪製到螢幕上的資料資訊。
格柵化:將圖片等向量資源,轉化為一格格畫素點的畫素圖,顯示到螢幕上。
垂直同步VSYNC:讓顯示卡的運算和顯示器重新整理率一致以穩定輸出的畫面質量。它告知GPU在載入新幀之前,要等待螢幕繪製完成前一幀。下面的三張圖分別是GPU和硬體同步所發生的情況,Refresh
Rate:螢幕一秒內重新整理螢幕的次數,由硬體決定,例如60Hz.而Frame Rate:GPU一秒繪製操作的幀數,單位fps。如下圖:
渲染流程
UI物件—->CPU處理為多維圖形,紋理 —–通過OpeGL ES介面呼叫GPU—-> GPU對圖進行光柵化(Frame Rate ) —->硬體時鐘(Refresh Rate)—-垂直同步—->投射到螢幕
渲染情況
- 正常情況
Android系統每隔16ms發出VSYNC訊號(1000ms/60=16.66ms),觸發對UI進行渲染, 如果每次渲染都成功,這樣就能夠達到流暢的畫面所需要的60fps,為了能夠實現60fps,這意味著計算渲染的大多數操作都必須在16ms內完成。正常情況如下:
注意:使順利在60幀,每幀必須小於16毫秒完成。
- 渲染超時
當這一幀畫面渲染時間超過16ms的時候,垂直同步機制會讓顯示器硬體 等待GPU完成柵格化渲染操作, 這樣會讓這一幀畫面,多停留了16ms,甚至更多.這樣就這造成了 使用者看起來 畫面停頓.。
當GPU渲染速度過慢,就會導致,某些幀顯示的畫面內容就會與上一幀的畫面相同。
通過上面的介紹,我們知道渲染中可能出現的問題,那麼如何避免或者說盡量減少這種問題的發生呢?接下來就是本文要說的過度繪製,減少不必要的繪製,儘量會減少渲染的時間。
過度繪製
過度繪製(Overdraw)描述的是螢幕上的某個畫素在同一幀的時間內被繪製了多次。在多層次的UI結構裡面, 如果不可見的UI也在做繪製的操作,這就會導致某些畫素區域被繪製了多次。這就浪費大量的CPU以及GPU資源。
檢測工具
按照以下步驟開啟Show GPU Overrdraw的選項:設定 -> 開發者選項 -> 除錯GPU過度繪製 -> 顯示GPU過度繪製。
- 藍色: 代表1層覆蓋。畫素繪製了兩次。大片的藍色還是可以接受的,若整個視窗是藍色的,可以擺脫一層。
- 綠色: 代表2層覆蓋。畫素繪製了三次。中等大小的綠色區域是可以接受的但你應該嘗試優化、減少它們。
- 淡紅: 代表3層覆蓋倍。畫素繪製了四次,小範圍可以接受。
- 深紅: 代表4層覆蓋。畫素繪製了五次或者更多。這是錯誤的,要修復它們。
渲染耗時呈現工具
開啟“開發者選項”->“GPU呈現模式分析”->“在螢幕上顯示為條形圖”
這個會分別顯示關於StatusBar,NavBar,啟用的程式Activity區域的GPU Rendering資訊。Activity區域有一根綠色的橫線,代表16ms,我們需要確保每一幀花費的總時間都低於這條橫線,這樣才能夠避免出現卡頓的問題。
介面上會滾動顯示垂直的柱狀圖來表示每幀畫面所需要渲染的時間,柱狀圖越高表示花費的渲染時間越長。每一條柱狀線都包含三部分:
- 紅色代表OpenGL渲染Display
List所需要的時間,假如當前介面的檢視越多,那麼紅色便會“跳”得越高。但其實這可能並不意味著你卡住了。 - 黃色通常較短,它代表著CPU通知GPU“你已經完成檢視渲染了”,這裡CPU會等待GPU的回話。假如黃色部分很高的話,說明當前GPU過於忙碌,有很多命令需要去處理。
- 藍色代表了檢視繪製所花費的時間,表示檢視在介面發生變化(更新)的用時情況。藍色對於判斷流暢度的參考意義是較大。當它越長時,說明當前檢視較複雜或者無效需要重繪,即我們通常說的“卡了”。
優化策略
1.減少佈局層級
關閉相關手機上的開發者檢測工具開關,開啟Android Device Monitor, 找到 Hierarychy view 檢視自己的佈局找到,深的層級,是否可以做優化. 最外層父容器 是否需要。
使用線性佈局LinearLayout排版導致UI層次變深,如果有這類問題,我們就使用相對佈局RelativeLayout代替LinearLayout,減少UI的層次;如果不能減少佈局層次推薦使用LinearLayout。
沒有用的父佈局時指沒有背景繪製或者沒有大小限制的父佈局,這樣的佈局不會對UI效果產生任何影響。我們可以把沒有用的父佈局,通過merge標籤合併來減少UI的層次;
2.去除不必要的背景
有時候為了方便會先給Layout設定一個整體的背景,再給子View設定背景,這裡也會造成重疊,如果子View寬度mach_parent,可以看到完全覆蓋了Layout的一部分,這裡就可以通過分別設定背景來減少重繪。再比如如果採用的是selector的背景,將normal狀態的color設定為“@android:color/transparent”,也同樣可以解決問題。適時使用Color.TRANSPARENT,因為透明色Color.TRANSPARENT是不會被渲染的,他是透明的。
所以開發過程中我們為某個View或者ViewGroup設定背景的時候,先思考下是否真的有必要,或者思考下這個背景能不能分段設定在子View上,而不是圖方便直接設定在根View上。
注意:在不同安卓版本的測試機中,去掉相同層次的背景色後,有的顯示正常,有的不正常。
3.優化自定義View的計算
學會裁剪掉View的覆蓋部分,增加cpu的計算量,來優化GPU的渲染,這個API可以很好的幫助那些有多組重疊 元件的自定義View來控制顯示的區域。同時clipRect方法還可以幫助節約GPU資源,在clipRect區域之外的繪製指令都不會被執行,那些部分內容在矩形區域內的元件,仍然會得到繪製。並且在onDraw方法中減少View的重複繪製。
下面一個運用clipRect的例子
public class CardView extends View {
private Bitmap[] mCards = new Bitmap[3];
private int[] mImgId = new int[]{R.mipmap.a, R.mipmap.b, R.mipmap.c};
public CardView(Context context) {
super(context);
for (int i = 0; i < mCards.length; i++) {
Bitmap bm = BitmapFactory.decodeResource(getResources(), mImgId[i]);
mCards[i] = Bitmap.createScaledBitmap(bm, 510, 510, false);
}
setBackgroundColor(Color.WHITE);
}
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
canvas.save();
canvas.translate(50, 200);
for (Bitmap bitmap : mCards) {
canvas.translate(150, 0);
canvas.drawBitmap(bitmap, 0, 0, null);
}
canvas.restore();
}
}
對於這段程式碼,沒有使用clipRect方法前,過度繪製檢測效果圖:
修改onDraw方法:
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
canvas.save();
canvas.translate(50, 200);
for (int i = 0; i < mCards.length; i++) {
canvas.translate(150, 0);
canvas.save();
if (i < mCards.length - 1) {
canvas.clipRect(0, 0, 150, mCards[i].getHeight());
}
canvas.drawBitmap(mCards[i], 0, 0, null);
canvas.restore();
}
canvas.restore();
}
去除windowbackground :
getWindow().setBackgroundDrawable(null);
//getWindow().setBackgroundDrawableResource(android.R.color.transparent);
再看下過度繪製檢測效果圖:
平時的開發過程中,需要權衡效果和效能,根據需求進行選擇. ,養成良好的思考習慣。