UI优化之不可忽略的知识点以及View的绘制优化举例

810 阅读6分钟

布局是如何显示到屏幕上的

在进行UI优化之前,得先明白我们写的布局是如何被绘制到屏幕上,这就涉及下面的知识点。

CPU和GPU的作用

  • CPU 作为“中央处理器”,除了要负责逻辑计算外,还需要做内存管理,显示操作,因此随着各种复杂的App的出现,实际运算的性能会大打折扣。
  • GPU 为了提高图像显示效率以及复杂的图形,主要功能为了帮助CPU分担图形显示。

XML布局显示到屏幕的流程

CPU和GPU是相互合作着把布局显示到屏幕,画了一个流程图便于理解。

image
多数的Android显示屏幕是以每秒60帧来刷新的(也就是60Hz),我们常说的16ms就是这么得来的(1000/60,60帧每秒,相当于60fps)。系统每隔16ms就会发出一次VSYNC信号触发对UI进行渲染,如果这16ms内没有完成对视图的绘制,那么就会出现丢帧的情况。为什么是16ms?因为需要60帧才能达到不卡顿的效果,可以看看其它效果需要多少帧。

  • 12fps : 画面帧率高于每秒约10—12帧时,眼睛会认为它是连贯的;

  • 24fps : 有声电影拍摄一般为每秒24帧;

  • 30fps : 早期动态电子游戏,一般会在每秒30帧左右;

  • 60fps : 手机交互过程中,需要触摸和反馈,需要60帧才能达到不卡顿的效果;

知道了布局绘制到屏幕的过程,也了解了对视图的绘制需要在16ms内完成,那么UI优化也就是缩短视图绘制的时间,主要从以下两部分着手。

  • CPU减少XML转换成对象的时间

  • GPU减少重复绘制

本篇举例的代码优化主要是针对减少过度绘制为目标。

什么是过度绘制

GPU每隔16ms画一次,如果CPU传递过来的图形有重复的位置,会造成用户只能看到顶层画面,而底层的画面被遮盖, 底层部分的绘制虽然用户无法看到,但同样也占据了计算资源,造成了不必要的浪费,这种情况就叫过度绘制。

过度绘制是UI优化中经常提及的一个概念,那么开发过程中怎么样尽量去避免过度绘制,避免过度绘制也是UI优化极其重要的一部分。解决过度绘制有很多种办法,比如去掉重复背景,多余的层级等这些在xml里能做到的优化,而本篇文章介绍的是在代码中如何进行View的绘制优化。在用代码举例前,先来看看怎么查看哪些部分过度绘制了。

如何查看过度绘制部分

第一步:通过开发者选项开启GPU过度绘制调试,如下图

image

第二步:按照颜色去判断过度绘制了几次

  • 原色:没有过度绘制
  • 蓝色:1 次过度绘制
  • 绿色:2 次过度绘制
  • 粉色:3 次过度绘制
  • 红色:4 次及以上过度绘制

UI优化涉及的部分重要知识点已经介绍完,下面就用代码去举例说明如何在代码里优化UI绘制。

代码举例

image
上图是我们都很熟悉的界面,本次就是模仿卡片的叠加效果来进行举例,将图片叠加的效果显示在屏幕上。代码如下

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过度绘制”,运行代码后就能看到如下效果。(由于在网上找到的类似上图的卡片都太大,不利于展示,在这里就随意找了一张图片,效果是一样的)

image

蓝色部分表示过度绘制了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();
    }
}

运行代码,效果如下

image
可以看到,重叠的部分就不会再被绘制,也就是我们的优化起到了效果。以上优化的过程,重要的代码加了注释,就不额外再去解释每句代码的作用了。

我的UI优化感想

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