
Android 自定义 View 绘制优化:从掉帧到丝滑的全链路实践
自定义 View 是 Android 开发绕不开的话题。但很多同学写完能跑、却忘了性能。本文从绘制原理出发,拆解 onMeasure、onDraw、硬件加速、属性动画等关键环节,带你把卡顿率打到最低。
一、先搞懂:View 绘制到底慢在哪里
在动手优化之前,我们要先理解 Android 的渲染流水线:
measure → layout → draw,生成 DisplayList每一帧的预算是 16.6ms(60fps)。一旦主线程超时,就会掉帧。常见的"杀手"有三个:
onDraw() 里频繁 new 对象(触发 GC)onMeasure/onLayout 里做了 多余的重复计算"性能问题的本质:在错误的时机,做了错误数量的工作。"
二、onDraw 优化:对象复用是第一原则
❌ 常见错误:在 onDraw 里 new 对象
onDraw() 每秒可能被调用 60 次,在里面 new 对象会持续产生垃圾,触发 GC 导致卡顿:
// ❌ 错误示范:每帧 new 对象
override fun onDraw(canvas: Canvas) {
val paint = Paint() // 每帧都 new!
paint.color = Color.RED
val rect = RectF(
0f, 0f, width.toFloat(),
height.toFloat()
) // 也是每帧 new!
canvas.drawRoundRect(
rect, 12f, 12f, paint
)
}✅ 正确做法:类级别复用
将 Paint、RectF、Path 等对象提升为成员变量,只在尺寸变化时更新:
class RoundRectView(
ctx: Context,
attrs: AttributeSet? = null
) : View(ctx, attrs) {// ✅ 成员变量,只初始化一次
private val mPaint = Paint(
Paint.ANTI_ALIAS_FLAG
).apply {
color = Color.RED
}
private val mRect = RectF()override fun onSizeChanged(
w: Int, h: Int,
oldW: Int, oldH: Int
) {
// 尺寸变化时才更新 RectF
mRect.set(
0f, 0f,
w.toFloat(),
h.toFloat()
)
}override fun onDraw(
canvas: Canvas
) {
canvas.drawRoundRect(
mRect, 12f, 12f, mPaint
)
}
} ⚠️ 注意:Paint(Paint.ANTI_ALIAS_FLAG) 比 Paint().apply { isAntiAlias = true } 效率更高,因为前者通过 native flag 直接初始化。
三、clipRect 与 Layer:精准控制绘制范围
用 clipRect 减少过度绘制
如果你的自定义 View 有多个区域叠加绘制(例如卡片折叠效果),用 canvas.clipRect() 剪裁绘制区域,让 GPU 跳过不可见区域:
override fun onDraw(
canvas: Canvas
) {
// 保存画布状态
canvas.save()// 只绘制上半部分
canvas.clipRect(
0f, 0f,
width.toFloat(),
height / 2f
)
drawCardBackground(canvas)canvas.restore()// 再绘制下半部分
canvas.save()
canvas.clipRect(
0f, height / 2f,
width.toFloat(),
height.toFloat()
)
drawCardContent(canvas)
canvas.restore()
}saveLayer 要慎用
canvas.saveLayer() 会创建一个离屏缓冲区(Offscreen Buffer),用于实现透明度混合、PorterDuff 模式等效果。但它的代价很高:
如果只是做圆角裁剪,更推荐用 BitmapShader + 自定义 Paint,或者在 API 21+ 使用 ViewOutlineProvider:
// API 21+ 推荐方式,硬件加速友好
myView.apply {
outlineProvider = object :
ViewOutlineProvider() {
override fun getOutline(
view: View,
outline: Outline
) {
outline.setRoundRect(
0, 0,
view.width,
view.height,
16f
)
}
}
clipToOutline = true
}四、硬件加速:哪些操作不支持?
Android 3.0 起引入硬件加速,大部分 Canvas 操作都已支持 GPU 加速。但仍有少数 API 在硬件加速模式下 不支持或行为不同:
❌ 不支持如果你的 View 用了不支持硬件加速的 API,可以局部关闭:
// 只对单个 View 关闭硬件加速
myView.setLayerType(
View.LAYER_TYPE_SOFTWARE, null
)💡 提示:不要对整个 Activity 关闭硬件加速,那会造成全局性能下降。只在最小必要范围内关闭。
五、invalidate 的正确姿势:按需局部刷新
invalidate() 会触发整个 View 重绘。如果你的 View 很大,而只有一小块区域需要更新,用带参数的重载:
// 只刷新指定区域,减少绘制量
invalidate(
dirtyLeft, dirtyTop,
dirtyRight, dirtyBottom
)// 或者传入 Rect
invalidate(mDirtyRect)用 ValueAnimator 驱动动画,别用 Handler
很多同学用 Handler 定时 postDelay 来驱动动画,这会导致帧率不稳定。正确姿势是用 ValueAnimator,它与 Choreographer 同步,精确跟随 VSync 信号:
private var mProgress = 0fprivate val mAnimator =
ValueAnimator
.ofFloat(0f, 1f)
.apply {
duration = 600
interpolator =
DecelerateInterpolator()
addUpdateListener { va ->
mProgress =
va.animatedValue as
Float
invalidate()
}
}fun startAnim() {
mAnimator.start()
}六、性能检测工具实战
GPU 过度绘制调试
在开发者选项中开启 "调试 GPU 过度绘制",颜色含义:
Systrace / Perfetto 定位卡帧
用 Perfetto 抓取 trace,重点关注 主线程 的 Choreographer#doFrame 耗时。如果某帧超过 16ms,展开查看是 measure、layout 还是 draw 的锅。
# 录制 10 秒 trace
adb shell perfetto \
-c - --txt -o \
/data/misc/perfetto-traces/t.pb \
<<EOF
buffers { size_kb: 63488 }
data_sources {
config {
name: "track_event"
}
}
duration_ms: 10000
EOF自定义 Trace 埋点
在自定义 View 里加入 Trace 标记,方便在 Perfetto 中精确定位:
override fun onDraw(
canvas: Canvas
) {
Trace.beginSection(
"MyView#onDraw"
)
try {
drawBackground(canvas)
drawContent(canvas)
} finally {
Trace.endSection()
}
}七、总结:一张清单搞定自定义 View 优化
每次写完自定义 View,对照这张清单自查:
✅ Paint、RectF、Path 等对象已提升为成员变量
✅ 复杂计算已移入 onSizeChanged(),不在 onDraw() 里重算
✅ 多区域绘制已用 clipRect() 减少过度绘制
✅ 使用了硬件加速不支持的 API 时,已局部关闭加速
✅ 动画用 ValueAnimator 驱动,已移除 Handler 定时刷新
✅ 用 Perfetto + GPU 过度绘制工具验证过实际效果
自定义 View 的性能优化没有银弹,但有方法论:先度量、再定位、再优化。不要凭感觉猜测瓶颈,让工具说话。
如果你正在做复杂的自定义 View(图表、游戏 UI、粒子效果),也可以考虑迁移到 Compose Canvas API,它在声明式框架下对重组范围控制更精细,结合 drawWithCache 可以进一步减少重复计算。这是下期的主题,感兴趣的话记得关注~
— END —
如果有帮助,点个「在看」支持一下 👇