绘制优化: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,问题就解决了。
区域绘制的正确姿势
如果你需要绘制复杂形状,我建议用以下方案:
- 能用矩形就别用路径——clipRect() 永远是最快的
- 用 BitmapShader 替代 clipPath()——提前生成圆角 Bitmap,绘制时直接贴图
- 考虑使用 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(),再查裁剪,最后考虑硬件加速