【轉】Android性能優化-過度繪制解決方案
過度繪制:
屏幕上某一像素點在一幀中被重復繪制多次,就是過度繪制。
下圖中多個卡片跌在一起,但是只有第一個卡片是完全可見的。背後的卡片只有部分可見。但是android系統在繪制時會將下層的卡片進行繪制,接著再將上層的卡片進行繪制。但其實,下層卡片不可見的部分是不需要進行繪制的,只有可見部分才需要進行繪制。
依據過度繪制的層度可以分成:
- 無過度繪制(一個像素只被繪制了一次)
- 過度繪制x1(一個像素被繪制了兩次)
- 過度繪制x2(一個像素被繪制了三次)
- 過度繪制x3(一個像素被繪制了四次)
- 過度繪制x4+(一個像素被繪制了五次以上)
查看自己應用的過度繪制情況:
方法一:通過開發者選項開啟GPU過度繪制調試
Android手機的開發者選項中有『調試 GPU 過度繪制』的選項:
點開後後選擇『顯示過度繪制區域』:
方法二:通過adb命令開啟GPU過度繪制調試
當然,如果每次都進入系統設置嫌麻煩,可以使用adb命令進行開啟和關閉:
開啟『調試 GPU 過度繪制』:
adb shell setprop debug.hwui.overdraw show
- 1
關閉『調試 GPU 過度繪制』:
adb shell setprop debug.hwui.overdraw false
- 1
執行命令之後可能需要重新啟動你當前開發的應用。
顏色與過度繪制:
- 原色:沒有過度繪制
- 藍色:1 次過度繪制
- 綠色:2 次過度繪制
- 粉色:3 次過度繪制
- 紅色:4 次及以上過度繪制
在平時的開發中,如果出現粉色及以上的過度繪制情況。說明過度繪制以及很嚴重了。需要進行優化。
優化過度繪制:
1. 去除Activity自帶的默認背景顏色:
查看Android源碼裏的Theme主題,如下:
<style name="Theme">
...
<!-- Window attributes -->
<item name="windowBackground">@drawable/screen_background_selector_dark</item>
...
</style>
- 1
- 2
- 3
- 4
- 5
- 6
也就是說繼承Theme這個style的風格,默認情況下,新建一個Activity都是有背景的。正常情況下,很多界面其實是不需要背景的。
下面是華為自帶天氣APP的首頁,我們可以看到文字部分以及圖標部分都是綠色,說面已經是第三層過度繪制了,其中背後天氣圖是一層,文字又是一層,正常來說應該只有兩層,也就是文字和圖標應該是藍色。那麽這多出來的一層應該就是Activity自帶的背景色了。也就是theme裏面設置的。
我們只要在自己的AppTheme裏面去除該背景色即可:
<style name="AppTheme" parent="android:Theme.Light.NoTitleBar">
<item name="android:windowBackground">@null</item>
</style>
- 1
- 2
- 3
或者在Activity的onCreate方法中:
getWindow().setBackgroundDrawable(null);
- 1
2.使用Canvas的clipRect和clipPath方法限制View的繪制區域
一個Activity對應有一個Canvas,也就是畫布,畫布的概念就是一個畫板,這個畫布提供了很多的API,我們可以通過調用畫布的API來繪圖以及對畫布做一些操作,clipRect方法用來裁切畫布上的一個矩形區域,該矩形區域用Rect對象來描述。調用了clipRect之後,畫布的可繪制區域減小到和Rect指定的矩形區域一樣大小。所有的繪制將限制在該矩形範圍之內。這裏的裁切概念和PS裏的裁切類似。
典型的例子,抽屜布局,找了網易雲音樂開刀:
註意觀察左側抽屜打開的時候,抽屜布局和背後布局重疊在一起了,此時整個屏幕一多半都變成了紅色,過度繪制嚴重。
在抽屜布局彈出時,抽屜布局是不透明的,也就是說抽屜布局背後擋住的內容布局是不需要繪制的,而網易雲進行了繪制,導致抽屜布局所在區域的像素點繪制了多次。
google官方在android.support.v4.widget包下有DrawerLayout.java類。使用來實現抽屜布局的。該類在重寫了drawChild方法:
@Override
protected boolean drawChild(Canvas canvas, View child, long drawingTime) {
final int height = getHeight();
// 判斷是否是內容視圖
final boolean drawingContent = isContentView(child);
int clipLeft = 0, clipRight = getWidth();
// 記錄當前畫布信息
final int restoreCount = canvas.save();
if (drawingContent) {
// 只有在繪制內容視圖時才進行裁切
final int childCount = getChildCount();
for (int i = 0; i < childCount; i++) {
final View v = getChildAt(i);
if (v == child || v.getVisibility() != VISIBLE ||
!hasOpaqueBackground(v) || !isDrawerView(v) ||
v.getHeight() < height) {
// 如果child是內容視圖/視圖不可見/視圖背景透明/不是抽屜視圖/child高度小於父布局高度
// 則不做畫布裁切
continue;
}
if (checkDrawerViewAbsoluteGravity(v, Gravity.LEFT)) {
// 盒子在左側時裁切的left和right
final int vright = v.getRight();
if (vright > clipLeft) clipLeft = vright;
} else {
// 盒子在右側時裁切的的left和right
final int vleft = v.getLeft();
if (vleft < clipRight) clipRight = vleft;
}
}
// 裁切畫布
canvas.clipRect(clipLeft, 0, clipRight, getHeight());
}
// 繪制子視圖
final boolean result = super.drawChild(canvas, child, drawingTime);
// 回復到裁切之前的畫布
canvas.restoreToCount(restoreCount);
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
- 31
- 32
- 33
- 34
- 35
- 36
- 37
- 38
- 39
- 40
drawChild方法在ViewGroup類的dispatchDraw方法內被調用,用來繪制子視圖,DrawerLayout類通過重寫該方法,因為在所有孩子視圖繪制之前都會調用drawChild方法,但是這裏只需要對內容區域視圖做裁切,當繪制內容區域視圖時,取得抽屜視圖的位置信息,如果抽屜視圖可見、背景為不透明、抽屜高度和父布局高度一致時,取得抽屜視圖左、上、右、下邊緣在canvas中的位置信息。接著進行裁切,將內容視圖未被擋住的部分區域裁切出來,並把裁切完的canvas交由子View進行繪制,這樣,內容區域只有在裁切後的區域才會繪制,其他區域不進行繪制。待子View繪制完之後,恢復Canvas到裁切之前的狀態,因為一個Window下的所有View都使用的是同一個Canvas,所以需要恢復狀態給其他子View使用。
下面看一個系統裏的“下載”APP,使用的是DrawerLayout實現:
應用中雖然內容區域是紅色,但是抽屜視圖拉出來之後,抽屜視圖的過度繪制情況卻比內容區域未被擋住的部分少。
3. ImageView的background和imageDrawable重疊
Android中,所有的view均可以設置background。ImageView除了能夠設置background之外,還能設置ImageDrawable。
在開發中,很多時候需要顯示圖片,在圖片加載出來之前通常是需要顯示一張默認圖片的,很多時候會使用ImageView的background屬性來設置默認背景圖,而imageDrawable來設置需要加載的圖片。這樣會導致一個問題,當圖片加載到頁面後,默認背景圖被擋住了,但是卻仍然然需要繪制,導致過度繪制情況的發生。
解決方案是把背景圖和真正加載的圖片都通過imageDrawable方法進行設置。
總結
- Android中一個window對應一個Canvas,window下的所有視圖(View/ViewGroup)使用的都是同一個canvas,視圖樹的父節點在調用子視圖的View.draw之前,會對Canvas進行裁切,裁切的區域就是View在屏幕中所占的矩形區域,這也就是為什麽超過View邊界的內容會被裁切掉的原因。
- 既然過度繪制值一個像素點被繪制多次,我們只要保證圖片或者背景顏色不要疊加在一起即可。正確的方式應該是盡量減少帶背景的View產生重疊區域。如果重疊,使用canvas的clipRect進行裁切。
- 盡量減少視圖的深度,來減少視圖樹的遍歷過程。
【轉】Android性能優化-過度繪制解決方案