布局优化的方式大概有如下几点:
过度绘制(Overdraw)是指在屏幕上的某个像素在同一帧的时间内被绘制了多次。在多层次重叠的 UI 结构(如带背景的 TextView)中,如果不可见的 UI 也在做绘制的操作,就会导致某些像素区域被绘制了多次,从而浪费多余的 CPU 以及 GPU 资源。
我们一般在 XML 布局和自定义控件中绘制,因此可以看出导致过度绘制的主要原因是:
要知道是否有过度绘制的情况,可以通过手机设置中的开发者选项-打开GPU过度绘制(Show GPU Overdraw)。做了一个重叠的布局,每个布局都设置了一个白色背景色,打开以后可以看到如下页面。
从上到下的颜色一次代表了:
(1)布局上的优化
在 XML 布局上,如果出现了过度绘制的情况,可以使用 Hierarchy View 来查看具体的层级情况,可以通过 XML 布局优化来减少层级。需要注意的是,在使用 XML 文件布局时,会设置很多背景,如果不是必需的,尽量移除。布局优化总结为以下几点:
使用 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 的案例。
// 卡片封装类
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 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中引用
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;
}
}