首页
学习
活动
专区
圈层
工具
发布
社区首页 >专栏 >Android 自定义 View 绘制优化:从掉帧到丝滑的全链路实践

Android 自定义 View 绘制优化:从掉帧到丝滑的全链路实践

作者头像
陆业聪
发布2026-03-10 14:38:32
发布2026-03-10 14:38:32
650
举报

Android 自定义 View 绘制优化:从掉帧到丝滑的全链路实践

自定义 View 是 Android 开发绕不开的话题。但很多同学写完能跑、却忘了性能。本文从绘制原理出发,拆解 onMeasureonDraw、硬件加速、属性动画等关键环节,带你把卡顿率打到最低。

一、先搞懂:View 绘制到底慢在哪里

在动手优化之前,我们要先理解 Android 的渲染流水线:

  1. 主线程 CPU:执行 measure → layout → draw,生成 DisplayList
  2. RenderThread:把 DisplayList 提交给 GPU
  3. GPU:光栅化 → 合成 → 显示

每一帧的预算是 16.6ms(60fps)。一旦主线程超时,就会掉帧。常见的"杀手"有三个:

  • onDraw() 里频繁 new 对象(触发 GC)
  • onMeasure/onLayout 里做了 多余的重复计算
  • 过度绘制(Overdraw)—— 同一像素被画了多次

"性能问题的本质:在错误的时机,做了错误数量的工作。"

二、onDraw 优化:对象复用是第一原则

❌ 常见错误:在 onDraw 里 new 对象

onDraw() 每秒可能被调用 60 次,在里面 new 对象会持续产生垃圾,触发 GC 导致卡顿:

代码语言:javascript
复制
// ❌ 错误示范:每帧 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 等对象提升为成员变量,只在尺寸变化时更新:

代码语言:javascript
复制
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 跳过不可见区域:

代码语言:javascript
复制
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 模式等效果。但它的代价很高:

  • 额外分配 GPU 内存
  • 需要把离屏缓冲区合成回主缓冲区

如果只是做圆角裁剪,更推荐用 BitmapShader + 自定义 Paint,或者在 API 21+ 使用 ViewOutlineProvider

代码语言:javascript
复制
// 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 在硬件加速模式下 不支持或行为不同

代码语言:javascript
复制
❌ 不支持

如果你的 View 用了不支持硬件加速的 API,可以局部关闭:

代码语言:javascript
复制
// 只对单个 View 关闭硬件加速
myView.setLayerType(
View.LAYER_TYPE_SOFTWARE, null
)

💡 提示:不要对整个 Activity 关闭硬件加速,那会造成全局性能下降。只在最小必要范围内关闭。

五、invalidate 的正确姿势:按需局部刷新

invalidate() 会触发整个 View 重绘。如果你的 View 很大,而只有一小块区域需要更新,用带参数的重载:

代码语言:javascript
复制
// 只刷新指定区域,减少绘制量
invalidate(
dirtyLeft, dirtyTop,
dirtyRight, dirtyBottom
)// 或者传入 Rect
invalidate(mDirtyRect)

用 ValueAnimator 驱动动画,别用 Handler

很多同学用 Handler 定时 postDelay 来驱动动画,这会导致帧率不稳定。正确姿势是用 ValueAnimator,它与 Choreographer 同步,精确跟随 VSync 信号:

代码语言:javascript
复制
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 过度绘制",颜色含义:

  • 🟦 蓝色:绘制 1 次(合格)
  • 🟩 绿色:绘制 2 次(可接受)
  • 🟥 红色:绘制 4+ 次(需要优化)

Systrace / Perfetto 定位卡帧

用 Perfetto 抓取 trace,重点关注 主线程Choreographer#doFrame 耗时。如果某帧超过 16ms,展开查看是 measure、layout 还是 draw 的锅。

代码语言:javascript
复制
# 录制 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 中精确定位:

代码语言:javascript
复制
override fun onDraw(
canvas: Canvas
) {
Trace.beginSection(
"MyView#onDraw"
)
try {
drawBackground(canvas)
drawContent(canvas)
} finally {
Trace.endSection()
}
}

七、总结:一张清单搞定自定义 View 优化

每次写完自定义 View,对照这张清单自查:

PaintRectFPath 等对象已提升为成员变量

✅ 复杂计算已移入 onSizeChanged(),不在 onDraw() 里重算

✅ 多区域绘制已用 clipRect() 减少过度绘制

✅ 使用了硬件加速不支持的 API 时,已局部关闭加速

✅ 动画用 ValueAnimator 驱动,已移除 Handler 定时刷新

✅ 用 Perfetto + GPU 过度绘制工具验证过实际效果

自定义 View 的性能优化没有银弹,但有方法论:先度量、再定位、再优化。不要凭感觉猜测瓶颈,让工具说话。

如果你正在做复杂的自定义 View(图表、游戏 UI、粒子效果),也可以考虑迁移到 Compose Canvas API,它在声明式框架下对重组范围控制更精细,结合 drawWithCache 可以进一步减少重复计算。这是下期的主题,感兴趣的话记得关注~

— END —

如果有帮助,点个「在看」支持一下 👇

本文参与 腾讯云自媒体同步曝光计划,分享自微信公众号。
原始发表:2026-03-10,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 陆业聪 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档