布局优化的方式大概有如下几点:
- 减少层级:通过合理的使用RelativeLayout 、LineaLayout 和ConstraintLayout等,具体效率这里不再阐述。
- 合理使用Merge
- ViewStub的使用
- 使用布局复用 include
- 避免过度绘制
本文主要讲述第五条避免过度绘制。
过度绘制。
过度绘制(Overdraw)是指在屏幕上的某个像素在同一帧的时间内被绘制了多次。在多层次重叠的 UI 结构(如带背景的 TextView)中,如果不可见的 UI 也在做绘制的操作,就会导致某些像素区域被绘制了多次,从而浪费多余的 CPU 以及 GPU 资源。
我们一般在 XML 布局和自定义控件中绘制,因此可以看出导致过度绘制的主要原因是:
- XML 布局->控件有重叠且都有设置背景
- View 自绘->View.OnDraw 里面同一个区域被绘制多次
过度绘制检测工具
要知道是否有过度绘制的情况,可以通过手机设置中的开发者选项-打开GPU过度绘制(Show GPU Overdraw)。做了一个重叠的布局,每个布局都设置了一个白色背景色,打开以后可以看到如下页面。
从上到下的颜色一次代表了:
- 无色(白色是因为自己设置的背景是白色):没有过度绘制,每个像素绘制了一次。
- 蓝色(也许是紫色):每个像素多绘制了一次。大片的蓝色还是可以接受的。如果整个窗口都是蓝色,可以尝试减少一次绘制。
- 绿色:每个像素绘制了两次。
- 淡红色:每个像素多绘制了三次。一般来说,整个位置不超过屏幕的1/4是可以接受的。
- 深共色:每个像素多绘制了四次或者更多。严重影响性能,需要优化避免深红色区域。
我们的目标是尽量减少红色区域,看到更多的蓝色/白色区域。
如何避免过度绘制
(1)布局上的优化
在 XML 布局上,如果出现了过度绘制的情况,可以使用 Hierarchy View 来查看具体的层级情况,可以通过 XML 布局优化来减少层级。需要注意的是,在使用 XML 文件布局时,会设置很多背景,如果不是必需的,尽量移除。布局优化总结为以下几点:
- 移除 XML 中非必需的背景,或根据条件设置。
- 移除 Window 默认的背景。
- 按需显示占位背景图片。
使用 Android 自带的一些主题时,activity 往往会被设置一个默认的背景,这个背景由DecorView持有。当自定义布局有一个全屏的背景时,比如设置了这个界面的全屏黑色背景,DecorView 的背景此时对我们来说是无用的,但是它会产生一次 Overdraw。因此没有必要的话,也可以移除,代码如下:
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
getWindow().setBackgroundDrawable(null);
}
(2)自定义View的优化
事实上,由于我们的产品设计总是追求更华丽的视觉效果,仅仅通过布局优化很难做到最好,这时可以对复杂的控件使用自定义 View 来实现,虽然 自定义 View 减 少了 Layout 的层级,但在实际绘制时也是会过度绘制的。原因是有些过于复杂的自定义 View(通常重写了 onDraw 方法),Android 系统无法检测在 onDraw 中具体会执行什么操作,无法监控并自动优化,也就无法避免 Overdraw 了。但是在自定义 View 中可以通过 canvas.clipRect()来帮助系统识别那些可见的区域。这个方法可以指定一块矩形区域,只有在这个区域内才会被绘制,其他的区域会被忽视。canvas.clipRect()可以很好地帮助那些有多组重叠组件的自定义 View 来控制显示的区域。clipRect 方法还可以帮助节约 CPU 与 GPU 资源,在 clipRect区域之外的绘制指令都不会被执行,那些部分内容在矩形区域内的组件,仍然会得到绘制,并且可以使用 canvas.quickreject ()来判断是否没和某个矩形相交,从而跳过那些非矩形区域内的绘制操作。接下来介绍使用一个自定义 View 避免 OverDraw 的案例。
- 下面是四张图片叠加的效果。我选择的是四张白色的图片,打开查看过度绘制后,首先是正常未处理过度绘制的样子:可以看下图,有浅红色过度绘制的区域
- 再看经过处理的效果
效果是很明显的,接下来是自定义view处理过度绘制的代码:
// 卡片封装类
public class SingleCard {
// 卡片绘制的区域
public RectF area;
// 需要绘制的图片
private Bitmap bitmap;
private Paint paint = new Paint();
public SingleCard(RectF area) {
this.area = area;
}
public void setBitmap(Bitmap bitmap) {
this.bitmap = bitmap;
}
public void draw(Canvas canvas) {
canvas.drawBitmap(bitmap, null, area, paint);
}
}
接下来是自定义View
public class MultiCardsView extends View {
private ArrayList<SingleCard> cardsList = new ArrayList<>();
public boolean isEnableOverdrawOpt() {
return enableOverdrawOpt;
}
private boolean enableOverdrawOpt = true;
public MultiCardsView(Context context) {
this(context, null);
}
public MultiCardsView(Context context, AttributeSet attrs) {
this(context, attrs, 0);
}
public MultiCardsView(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
}
public void addCards(SingleCard card) {
cardsList.add(card);
}
public void setEnableOverdrawOpt(boolean enableOverdrawOpt) {
this.enableOverdrawOpt = enableOverdrawOpt;
invalidate();
}
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
if (canvas == null || cardsList == null || cardsList.size() == 0) {
return;
}
Rect clip = canvas.getClipBounds();
if (enableOverdrawOpt) {
drawCardWithoutOverdraw(canvas, cardsList.size() - 1);
} else {
drawCardNormal(canvas, cardsList.size() - 1);
}
}
private void drawCardNormal(Canvas canvas, int i) {
if (canvas == null || i < 0 || i >= cardsList.size()) {
return;
}
SingleCard card = cardsList.get(i);
if (card != null) {
drawCardNormal(canvas, i - 1);
card.draw(canvas);
}
}
private void drawCardWithoutOverdraw(Canvas canvas, int i) {
if (canvas == null || i < 0 || i >= cardsList.size()) {
return;
}
SingleCard card = cardsList.get(i);
// 判断是否和其他卡片相交 从而跳过那些非矩形区域内的绘制
if (card != null && !canvas.quickReject(card.area, Canvas.EdgeType.BW)) {
int saveCount = canvas.save();
// 只绘制可见区域
if (canvas.clipRect(card.area, Region.Op.DIFFERENCE)) {
drawCardWithoutOverdraw(canvas, i - 1);
}
canvas.restoreToCount(saveCount);
saveCount = canvas.save();
//只绘制可见区域
if (canvas.clipRect(card.area)) {
Rect clip = canvas.getClipBounds();
card.draw(canvas);
}
canvas.restoreToCount(saveCount);
} else {
drawCardWithoutOverdraw(canvas, i - 1);
}
}
}
然后是在Activity中引用
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="#FFFFFF"
android:orientation="vertical"
tools:context=".MainActivity">
<Button
android:id="@+id/btn"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="click" />
<com.demo.text.MultiCardsView
android:id="@+id/cardView"
android:layout_width="wrap_content"
android:layout_height="wrap_content" />
</LinearLayout>
public class MainActivity extends Activity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
getWindow().setBackgroundDrawable(null);
final MultiCardsView cardsView = findViewById(R.id.cardView);
cardsView.setEnableOverdrawOpt(true);
int width = getResources().getDisplayMetrics().widthPixels;
int height = getResources().getDisplayMetrics().heightPixels;
int cardWidth = width / 3;
final int cardHeight = height / 3;
int yOffset = 40;
int xOffset = 40;
Integer[] ids = new Integer[]{R.drawable.ic_launcher_background, R.drawable.ic_launcher_background, R.drawable.ic_launcher_background, R.drawable.ic_launcher_background, R.drawable.ic_launcher_background, R.drawable.ic_launcher_background};
for (int i = 0; i < ids.length; i++) {
SingleCard singleCard = new SingleCard(new RectF(xOffset, yOffset, xOffset + cardWidth, yOffset + cardHeight));
Bitmap bitmap = getBitmap(this, R.drawable.aaa);
singleCard.setBitmap(bitmap);
cardsView.addCards(singleCard);
xOffset += cardWidth / 3;
}
findViewById(R.id.btn).setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
cardsView.setEnableOverdrawOpt(!cardsView.isEnableOverdrawOpt());
}
});
}
private static Bitmap getBitmap(Context context, int vectorDrawableId) {
Bitmap bitmap = null;
if (Build.VERSION.SDK_INT > Build.VERSION_CODES.LOLLIPOP) {
Drawable vectorDrawable = context.getDrawable(vectorDrawableId);
bitmap = Bitmap.createBitmap(vectorDrawable.getIntrinsicWidth(),
vectorDrawable.getIntrinsicHeight(), Bitmap.Config.ARGB_8888);
Canvas canvas = new Canvas(bitmap);
vectorDrawable.setBounds(0, 0, canvas.getWidth(), canvas.getHeight());
vectorDrawable.draw(canvas);
} else {
bitmap = BitmapFactory.decodeResource(context.getResources(), vectorDrawableId);
}
return bitmap;
}
}
- 上面的代码有两个方法
(1)QuickReject:快速判断Canvas是否需要绘制。在绘制一个单元前,先判断是否在Canvas的剪切区域内,若不在则直接返回。
(2)Canvas.ClipRect:避免绘制越界。每个绘制单元都有自己的绘制区域,绘制前,Canvas.ClipRect(Region.Op.INTERSECT)帮助系统识别那些可见的区域。这个方法可以指定一块矩形区域,只有在这个区域内,才会被绘制,其他的区域被忽视。