绘制优化:onDraw() 中的性能陷阱

说到 Android 的卡顿问题,onDraw() 方法绝对是个重灾区。我见过太多应用,明明业务逻辑不复杂,但界面就是掉帧。追查下来,十有八九是 onDraw() 里埋了雷。

onDraw() 这个方法,说白了就是 View 的「画布时间」。系统每帧都会调用它来刷新界面。你想想看,如果这个方法里干了太多不该干的事,那画面能流畅才怪。

常见的 onDraw() 陷阱

我个人习惯,在写 onDraw() 之前,先问自己三个问题:

  • 这个操作真的需要每帧都执行吗?
  • 能不能提前算好,而不是每次现算?
  • 有没有更轻量的替代方案?

好,咱们一个个来看。

1. 对象创建

这是最典型的坑。在 onDraw() 里 new 对象,等于每帧都在触发 GC。GC 一频繁,卡顿就来了。

// ❌ 错误示范:每帧都创建新对象
@Override
protected void onDraw(Canvas canvas) {
    Paint paint = new Paint();  // 每帧都 new,GC 压力巨大
    paint.setColor(Color.RED);
    canvas.drawCircle(100, 100, 50, paint);
}

// ✅ 正确做法:成员变量复用
private Paint mPaint = new Paint();

@Override
protected void onDraw(Canvas canvas) {
    mPaint.setColor(Color.RED);
    canvas.drawCircle(100, 100, 50, mPaint);
}

我在项目中遇到过,有个同事在 onDraw() 里创建了 Path 对象,结果界面滑动时明显卡顿。换成复用后,帧率直接从 30fps 飙到 55fps。

2. 复杂计算

onDraw() 里不要做耗时计算。比如三角函数、字符串拼接、循环遍历等。这些都应该提前算好,或者用缓存。

核心原则:onDraw() 只做绘制,不做计算。

// ❌ 错误示范:在 onDraw 里做计算
@Override
protected void onDraw(Canvas canvas) {
    for (int i = 0; i < 100; i++) {
        float x = Math.sin(i * Math.PI / 180) * radius;
        float y = Math.cos(i * Math.PI / 180) * radius;
        canvas.drawPoint(x, y, paint);
    }
}

// ✅ 正确做法:提前计算好坐标
private float[] mPoints = new float[200];

@Override
protected void onSizeChanged(int w, int h, int oldw, int oldh) {
    super.onSizeChanged(w, h, oldw, oldh);
    for (int i = 0; i < 100; i++) {
        mPoints[i * 2] = (float) Math.sin(i * Math.PI / 180) * radius;
        mPoints[i * 2 + 1] = (float) Math.cos(i * Math.PI / 180) * radius;
    }
}

@Override
protected void onDraw(Canvas canvas) {
    canvas.drawPoints(mPoints, paint);
}

3. 不必要的重绘

调用 invalidate() 要谨慎。不是所有变化都需要重绘整个 View。能用局部刷新就别全局刷新。

小技巧:使用 invalidate(Rect) 指定脏区域,只重绘变化的部分。

Canvas 裁剪与区域绘制

Canvas 裁剪是个好东西,但用不好就是性能杀手。我刚开始做自定义 View 时,也踩过这个坑。

裁剪的原理

Canvas.clipRect() 和 Canvas.clipPath() 会限制后续绘制只在指定区域内生效。听起来很美好,对吧?但问题在于,clipPath() 在硬件加速下表现很差。

裁剪方式 软件渲染 硬件加速
clipRect() ✅ 高效 ✅ 高效
clipPath() ✅ 可用 ❌ 性能差
clipRegion() ✅ 高效 ❌ 不支持

为什么会这样?因为 GPU 处理矩形裁剪非常快,但处理任意路径就需要回退到 CPU 计算,这中间的开销非常大。

避坑指南:我曾经在列表 Item 里用了 clipPath() 做圆角裁剪,结果列表滑动时帧率直接掉到 20fps。后来改用 clipRect() 配合 BitmapShader,问题就解决了。

区域绘制的正确姿势

如果你需要绘制复杂形状,我建议用以下方案:

  1. 能用矩形就别用路径——clipRect() 永远是最快的
  2. 用 BitmapShader 替代 clipPath()——提前生成圆角 Bitmap,绘制时直接贴图
  3. 考虑使用 ViewOutlineProvider——Android 5.0 以上可以用 outline 实现裁剪
// 推荐方案:使用 BitmapShader 实现圆角
private BitmapShader mShader;
private RectF mRect = new RectF();

@Override
protected void onDraw(Canvas canvas) {
    // 提前生成带圆角的 Bitmap
    if (mShader == null) {
        Bitmap bitmap = Bitmap.createBitmap(getWidth(), getHeight(), Bitmap.Config.ARGB_8888);
        Canvas tempCanvas = new Canvas(bitmap);
        // 在 tempCanvas 上绘制圆角内容
        // ...
        mShader = new BitmapShader(bitmap, Shader.TileMode.CLAMP, Shader.TileMode.CLAMP);
    }
    
    mPaint.setShader(mShader);
    canvas.drawRoundRect(mRect, radius, radius, mPaint);
}

硬件加速的原理与开关

硬件加速,说白了就是把绘制工作从 CPU 交给 GPU。GPU 擅长并行处理,所以能大幅提升绘制效率。

硬件加速的原理

没有硬件加速时,CPU 逐像素绘制,一帧一帧地算。有了硬件加速,GPU 一次性处理大量顶点和纹理,效率高得多。

但这里有个关键点:不是所有 Canvas 操作都支持硬件加速。有些操作在硬件加速下会被忽略,或者回退到软件渲染。

操作 硬件加速支持 说明
drawBitmap() ✅ 支持 纹理上传到 GPU
drawText() ✅ 支持 字体渲染优化
clipPath() ❌ 不支持 回退到软件渲染
drawPicture() ❌ 不支持 需要 CPU 解码

如何开关硬件加速

硬件加速默认是开启的。但有些场景下,你可能需要关闭它。比如:

  • 使用了大量不支持硬件加速的 Canvas 操作
  • 需要精确控制绘制顺序
  • 某些自定义 View 在硬件加速下出现渲染异常
// 全局关闭(AndroidManifest.xml)
<application android:hardwareAccelerated="false" ... />

// 按 Activity 关闭
<activity android:hardwareAccelerated="false" ... />

// 按 View 关闭(代码中)
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB) {
    setLayerType(View.LAYER_TYPE_SOFTWARE, null);
}

注意:关闭硬件加速是最后的手段。我建议先排查代码,看看能不能用兼容的方式实现。实在不行再关。毕竟硬件加速带来的性能提升是实打实的。

硬件加速的调试技巧

我个人习惯用开发者选项里的「GPU 呈现模式分析」来观察硬件加速的效果。如果看到绿色条(代表 GPU 处理)占比很高,说明硬件加速在正常工作。如果出现大量红色条(代表 CPU 处理),那就要检查是不是有回退操作了。

嗯,这里还要提一句:硬件加速不是万能药。它主要优化的是绘制阶段,如果你的卡顿是因为布局嵌套太深、measure 太慢,那关掉硬件加速也没用。得对症下药。

总结一下:

  • onDraw() 里别 new 对象,别做计算
  • 裁剪用 clipRect(),少用 clipPath()
  • 硬件加速默认开启,非必要不关闭
  • 遇到卡顿,先查 onDraw(),再查裁剪,最后考虑硬件加速