前言
最近在看Android中性能優化的,其中提到了LinearLayout會引起overdraw,但是並沒有具體的分析原因,我自己查找了一些資料從LinearLayout的繪製等方面來說明爲什麼使用LinearLayout會引起overdraw和哪些情況下使用LinearLayout會引起overdraw。希望大家看完之後對view的繪製和測量過程更加了解。
什麼是overdraw
Android中在屏幕上繪製一個像素會花一定的時間,如果在屏幕的同一個位置多次繪製就會花大量的時間,多次在屏幕上同一位置繪製的情況就成爲overdraw。overdraw會非常影響應用的性能。一個高效的佈局要做到兩點:
1、減少overdraw。
2、簡化佈局結構。
看下一個overdraw的例子
上面的六張牌的重合部分被多次繪製引發了overdraw,
檢測overdraw
可以通過手機的設置來直觀的查看應用的overdraw情況。
1、打開設置,打開開發者選項。
2、選擇調試GPU過度渲染。
3、打開顯示過度渲染區域。
上面左邊是正常的未打開,調試GPU過度渲染的情形,右邊是打開GPU過度渲染檢測之後的。
上面右圖中不同的顏色對應不同的overdraw的次數。對應關係如下:
如下三幅圖,最右邊的是基本上沒有overdraw或者只有一次overdraw的,而中間的紅色區域很多,大部分是有三次及以上的overdraw。overdraw的次數越多越影響性能,所以中間的這種是不提倡的。
減少overdraw的方法
1、減少不必要的background
因爲有background的時候會先繪製一遍background然後再在background的基礎上面繪製其他元素,所以會增加一次overdraw。
所有用戶看不到的background都應該刪除掉。
如下面的這個例子:
刪除background之前
<ImageView
android:layout_width="match_parent"
android:layout_height="match_parent"
android:src="@drawable/beach"
android:background="@android:color/white">
</ImageView>
刪除background之後
<ImageView
android:layout_width="match_parent"
android:layout_height="match_parent"
android:src="@drawable/beach" >
</ImageView>
因爲imageView的background在有src的時候根本就不會被用戶觀察到,所以應該刪除掉。
2、統一整個app的background顏色
可以通過設置整個app的統一背景色來防止不同的activity設置不同的背景色而導致多個background。可以在AndroidManifest.xml添加:
android:theme="@android:style/Theme.Light"
或者想要的背景色。
2、使用clip減少渲染區域
clip具有裁剪功能,在自定義view的時候,渲染的圖像可能和用戶觀察到的圖像不一樣,自定義view的ondraw方法裏面不僅僅只渲染用戶可見的圖像而且會渲染到被遮擋的圖像,使用clip方法裁剪出用戶可見的區域,這樣可以減少overdraw。
使用clipRect()方法
在自定義view中使用 Canvas.clipRect()方法可以有效的減少overdraw。這個方法可以爲自定義的view提供一個rectangle 區域,並且只有在這個區域中的內容纔會被繪製。上面的撲克牌就可以使用Canvas.clipRect()來減少繪製。如下:
下面通過一個自定義的view來進行一個對比:
未使用clipRect()的自定義view
/**
* Created time 20:54.
*
* @author huhanjun
* @since 2019/6/12
*/
public class MyView extends View {
private Paint mPaint;
private Bitmap mBitmap;
private int mPadding = 0;
public MyView(Context context) {
super(context);
}
public MyView(Context context, @Nullable AttributeSet attrs) {
super(context, attrs);
mPaint = new Paint();
mBitmap = BitmapFactory.decodeResource(getResources(), R.drawable.over_draw);
}
public MyView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
mPaint = new Paint();
mBitmap = BitmapFactory.decodeResource(getResources(), R.drawable.over_draw);
}
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
for (int i = 0; i < 5; i++) {
canvas.save();
canvas.drawBitmap(mBitmap,mPadding,0,mPaint);
canvas.restore();
mPadding += 200;
}
}
}
在開啓檢測overdraw後的顯示效果如下:
上圖從左到右可以由顏色看出分別進行了一次,兩次,三次和多次的overdraw,下面看看使用clipRect()後的顯示效果。
使用clipRect()的自定義view
/**
* Created time 20:54.
*
* @author huhanjun
* @since 2019/6/12
*/
public class MyView extends View {
private Paint mPaint;
private Bitmap mBitmap;
private int mPadding = 0;
public MyView(Context context) {
super(context);
}
public MyView(Context context, @Nullable AttributeSet attrs) {
super(context, attrs);
mPaint = new Paint();
mBitmap = BitmapFactory.decodeResource(getResources(), R.drawable.over_draw);
}
public MyView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
mPaint = new Paint();
mBitmap = BitmapFactory.decodeResource(getResources(), R.drawable.over_draw);
}
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
for (int i = 0; i < 5; i++) {
canvas.save();
Rect rect = new Rect(mPadding, 0, 200+mPadding, mBitmap.getWidth() + mPadding);
canvas.clipRect(rect);//此處增加clipRect方法
canvas.drawBitmap(mBitmap,mPadding,0,mPaint);
canvas.restore();
mPadding += 200;
}
}
}
運行後的顯示效果如下:
可以看出從左到右所有的圖片都只進行了一次overdraw。大大減少了渲染次數。
Canvas.clipRect()該方法用於裁剪畫布,也就是設置畫布的顯示區域 調用clipRect()方法後,只會顯示被裁剪的區域,之外的區域將不會顯示 .
3、儘量少用透明(alpha )效果
Alpha是圖形界面開發中常用的特效,通常我們會使用以下代碼來實現Alpha特效:
view.setAlpha(0.5f);
View.ALPHA.set(view, 0.5f);
ObjectAnimator.ofFloat(view, "alpha", 0.5f).start();
view.animate().alpha(0.5f).start();
view.setAnimation(new AlphaAnimation(1.0f, 0.5f));
其效果都等同於:
canvas.saveLayer(l, r, t, b, 127, Canvas.CLIP_TO_LAYER_SAVE_FLAG);
渲染帶有透明度像素被稱爲:alpha rendering,alpha rendering會導致overdraw,因爲系統會先渲染透明像素,然後渲染透明像素下面的view的像素,最後結合兩者,從而產生透明度的效果,因此會導致overdraw。如下圖:
上圖左邊是沒有加透明度的像素,右邊是在其上面加上一個透明度的蒙層,下面的圖是加上透明度後最終產生的效果。
類似於透明動畫,淡入,淡出等或者帶有陰影的效果都會導致alpha rendering。因此會導致overdraw。下面看一個由於設置alpha引起性能問題的實例。
Android Performance Case
在開發者模式中打開GPU profiling 工具後,發現上圖中右邊存在明顯的掉幀現象(底下紅柱超過藍線部分的),打開Tracer for OpenGL工具來檢測後發現:
是由viewpager上面滑動時標誌當前位置和其他位置的白色小點引起的,這些白色的小點,設置了透明度,每次都會調用Canvas.saveLayer()生成一個臨時圖層。正好滿足了以下條件:
getAlpha() returns a value < 1
onSetAlpha() returns false
getLayerType() returns LAYER_TYPE_NONE
hasOverlappingRendering() returns true
上面的用紅色區域標記出來的圓點,在每次滑動viewpager的時候都會動態調用setAlpha()方法來改變顏色和透明度,因此引起了overdraw和掉幀。爲了解決上面的問題,可以採用下面的任一種方法:
1、Use a customizable “inactive” color instead of setting an opacity on the View
2、 Return false from hasOverlappingRendering() and the framework will set the proper alpha on the Paint for you
(注意:在android的View裏有透明度的屬性,當設置透明度setAlpha的時候,android裏默認會把當前view繪製到offscreen buffer中,然後再顯示出來。 這個offscreen buffer 可以理解爲一個臨時緩衝區,把當前View放進來並做透明度的轉化,然後在顯示到屏幕上。這個過程是消耗資源的,所以應該儘量避免這個過程。而當繼承了hasOverlappingRendering()方法返回false後,android會自動進行合理的優化,避免使用offscreen buffer。
)
3、Return true from onSetAlpha() and set an alpha on the Paint used to draw the “gray” circles
如何高效的使用alpha屬性
上面已經說到了使用alpha屬性的時候會導致overdraw,那麼應該如何避免這些情況以減少overdraw呢?
下面分別對textview,imageview,和customview中使用到alpha情況進行說明:
textview
對於TextView我們通常需要文字透明效果,而不是View本身透明,所以,直接設置帶有alpha值的TextColor是比較高效的方式。
// 錯誤使用方式
textView.setAlpha(alpha);
//----------------------------------------------------------------
//正確方式
// 以下方式可以避免創建 offscreen buffer
int newTextColor = (int) (0xFF * alpha) << 24 | baseTextColor & 0xFFFFFF;
textView.setTextColor(newTextColor);
ImageView
同樣的對於只具有src image的ImageView,直接調用setImageAlpha()方法更爲合理。
//1、 錯誤方式, setAlpha方法由View繼承而來,性能不佳
imageView.setAlpha(0.5f);
//-------------------------------------------------------------
//正確方式
// 使用以下方式時,ImageView會在繪製圖片時單獨爲圖片指定Alpha
// 可以避免創建 offScreenBuffer
imageView.setImageAlpha((int) alpha * 255);
CustomView
類似的,自定義控件時,應該直接去設置paint的alpha。
//錯誤方式
customView.setAlpha(alpha);
//----------------------------------------------------
// 正確方式 But this
paint.setAlpha((int) alpha * 255);
canvas.draw*(..., paint);
LinearLayout導致的overdraw
未完待續
參考文獻
1、https://google-developer-training.github.io/android-developer-advanced-course-concepts/unit-2-make-your-apps-fast-and-small/lesson-4-performance/4-1-c-rendering-and-layout/4-1-c-rendering-and-layout.html
2、https://sriramramani.wordpress.com/2015/05/06/custom-viewgroups/
3、https://helw.net/2016/01/27/on-linearlayout-measures/
4、https://www.cnblogs.com/tianzhijiexian/p/4644693.html
5、https://www.androidperformance.com/2015/03/31/android-performance-case-study-follow-up/
6、http://www.curious-creature.com/2015/03/25/android-performance-case-study-follow-up/?utm_source=Android+Weekly&utm_campaign=0692ef161b-Android_Weekly_146&utm_medium=email&utm_term=0_4eb677ad19-0692ef161b-337850757
7、http://www.curious-creature.com/2012/12/01/android-performance-case-study/
8、http://yangm90.github.io/android-Alpha/