布局是如何显示到屏幕上的
在进行UI优化之前,得先明白我们写的布局是如何被绘制到屏幕上,这就涉及下面的知识点。
CPU和GPU的作用
- CPU 作为“中央处理器”,除了要负责逻辑计算外,还需要做内存管理,显示操作,因此随着各种复杂的App的出现,实际运算的性能会大打折扣。
- GPU 为了提高图像显示效率以及复杂的图形,主要功能为了帮助CPU分担图形显示。
XML布局显示到屏幕的流程
CPU和GPU是相互合作着把布局显示到屏幕,画了一个流程图便于理解。

-
12fps : 画面帧率高于每秒约10—12帧时,眼睛会认为它是连贯的;
-
24fps : 有声电影拍摄一般为每秒24帧;
-
30fps : 早期动态电子游戏,一般会在每秒30帧左右;
-
60fps : 手机交互过程中,需要触摸和反馈,需要60帧才能达到不卡顿的效果;
知道了布局绘制到屏幕的过程,也了解了对视图的绘制需要在16ms内完成,那么UI优化也就是缩短视图绘制的时间,主要从以下两部分着手。
-
CPU减少XML转换成对象的时间
-
GPU减少重复绘制
本篇举例的代码优化主要是针对减少过度绘制为目标。
什么是过度绘制
GPU每隔16ms画一次,如果CPU传递过来的图形有重复的位置,会造成用户只能看到顶层画面,而底层的画面被遮盖, 底层部分的绘制虽然用户无法看到,但同样也占据了计算资源,造成了不必要的浪费,这种情况就叫过度绘制。
过度绘制是UI优化中经常提及的一个概念,那么开发过程中怎么样尽量去避免过度绘制,避免过度绘制也是UI优化极其重要的一部分。解决过度绘制有很多种办法,比如去掉重复背景,多余的层级等这些在xml里能做到的优化,而本篇文章介绍的是在代码中如何进行View的绘制优化。在用代码举例前,先来看看怎么查看哪些部分过度绘制了。
如何查看过度绘制部分
第一步:通过开发者选项开启GPU过度绘制调试,如下图

第二步:按照颜色去判断过度绘制了几次
- 原色:没有过度绘制
- 蓝色:1 次过度绘制
- 绿色:2 次过度绘制
- 粉色:3 次过度绘制
- 红色:4 次及以上过度绘制
UI优化涉及的部分重要知识点已经介绍完,下面就用代码去举例说明如何在代码里优化UI绘制。
代码举例

DroidCard.class
import android.content.res.Resources;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
public class DroidCard {
public int x;
public int width;
public int height;
public Bitmap bitmap;
public DroidCard(Resources res,int resId,int x){
this.bitmap = BitmapFactory.decodeResource(res,resId);
this.x = x;
this.width = this.bitmap.getWidth();
this.height = this.bitmap.getHeight();
}
}
DroidCardView.class,将卡片按顺序绘制。
import android.content.Context;
import android.content.res.Resources;
import android.graphics.Canvas;
import android.graphics.Paint;
import android.util.AttributeSet;
import android.view.View;
import androidx.annotation.Nullable;
import java.util.ArrayList;
import java.util.List;
public class DroidCardView extends View {
private int mCardSpacing = 150;
//图片与左侧距离的记录
private int mCardLeft = 10;
private List<DroidCard> mDroidCards = new ArrayList<>();//卡片集合
private Paint paint = new Paint();
public DroidCardView(Context context) {
super(context);
initCards();
}
public DroidCardView(Context context, @Nullable AttributeSet attrs) {
super(context, attrs);
initCards();
}
//初始化卡片集合
private void initCards() {
Resources res = getResources();
mDroidCards.add(new DroidCard(res, R.drawable.card_imag, mCardLeft));
//mCardLeft + mCardSpacing就是每张卡片露出来的宽度
mCardLeft += mCardSpacing;
mDroidCards.add(new DroidCard(res, R.drawable.card_imag, mCardLeft));
mCardLeft += mCardSpacing;
mDroidCards.add(new DroidCard(res, R.drawable.card_imag, mCardLeft));
mCardLeft += mCardSpacing;
mDroidCards.add(new DroidCard(res, R.drawable.card_imag, mCardLeft));
}
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
for (DroidCard c : mDroidCards) {//遍历卡片进行绘制
drawDroidCard(canvas, c);
}
invalidate();
}
private void drawDroidCard(Canvas canvas, DroidCard c) {
canvas.drawBitmap(c.bitmap, c.x, 0f, paint);
}
}
写好以后在布局中引用DroidCardView,打开手机的“调试GPU过度绘制”,运行代码后就能看到如下效果。(由于在网上找到的类似上图的卡片都太大,不利于展示,在这里就随意找了一张图片,效果是一样的)

蓝色部分表示过度绘制了1次,绿色部分表示过度绘制了2次。过度绘制2次的,就是两张卡片的叠加地方。看代码得知是按顺序依次绘制各个卡片,除了最后一张卡片是完全显示出来,其余的都是上面的卡片会盖住下面卡片一部分,卡片间重叠的部分也会进行绘制,发生Overdraw。那么我们只要把图片被遮盖的地方不绘制,用canvas.clipRect()指定要绘制的区域,只绘制不被遮盖的地方,那么就不存在过度绘制的问题了。优化代码如下
import android.content.Context;
import android.content.res.Resources;
import android.graphics.Canvas;
import android.graphics.Paint;
import android.util.AttributeSet;
import android.view.View;
import androidx.annotation.Nullable;
import java.util.ArrayList;
import java.util.List;
public class DroidCardView extends View {
private int mCardSpacing = 150;
//图片与左侧距离的记录
private int mCardLeft = 10;
private List<DroidCard> mDroidCards = new ArrayList<>();//卡片集合
private Paint paint = new Paint();
public DroidCardView(Context context) {
super(context);
initCards();
}
public DroidCardView(Context context, @Nullable AttributeSet attrs) {
super(context, attrs);
initCards();
}
//初始化卡片集合
private void initCards() {
Resources res = getResources();
mDroidCards.add(new DroidCard(res, R.drawable.card_imag, mCardLeft));
mCardLeft += mCardSpacing;
mDroidCards.add(new DroidCard(res, R.drawable.card_imag, mCardLeft));
mCardLeft += mCardSpacing;
mDroidCards.add(new DroidCard(res, R.drawable.card_imag, mCardLeft));
}
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
// for (DroidCard c : mDroidCards) {//遍历卡片进行绘制
// drawDroidCard(canvas, c);
// }
for (int i = 0;i < mDroidCards.size()-1;i++){//将最后一张和其它的图片分开绘制
drawDroidCard(canvas,mDroidCards,i);
}
drawLastDroisCard (canvas,mDroidCards.get(mDroidCards.size()-1));
invalidate();
}
//绘制最后一个DroidCard
private void drawLastDroisCard(Canvas canvas, DroidCard c) {
canvas.drawBitmap(c.bitmap, c.x, 0f, paint);
}
private void drawDroidCard(Canvas canvas, List<DroidCard> mDroidCards,int i) {
DroidCard c = mDroidCards.get(i);
canvas.save();//canvas.save()保存当前状态
canvas.clipRect((float)c.x,0f,(float)(mDroidCards.get(i+1)).x,(float)c.height);
canvas.drawBitmap(c.bitmap, c.x, 0f, paint);
//canvas.restore()恢复canvas的状态,如果不恢复,canvas.clipRect()的修改会一直生效,影响之后的绘制
canvas.restore();
}
}
运行代码,效果如下

我的UI优化感想
UI优化的方式很多,代码里的优化让我头疼,像自定义View时如何做好优化,还需要大量的学习与实践。以上也只是我个人的小小经验,有写错的地方欢迎指正,感激。