一、动画性能优化:让每一帧都丝滑流畅
做Android动画开发这么多年,我踩过最多的坑,就是性能问题。你想想看,一个动画明明逻辑写对了,跑起来却卡成PPT,用户直接给你差评。说白了,动画性能优化不是锦上添花,而是必修课。
今天咱们就聊四个核心方向:硬件加速、帧率监控、避免过度绘制、内存泄漏排查。这四块搞定了,你的动画基本就稳了。
1.1 硬件加速:让GPU帮你干活
硬件加速这个概念,我刚开始接触时也觉得玄乎。其实说白了,就是让GPU分担CPU的绘图工作。Android从3.0开始支持硬件加速,到4.0以后默认开启。
但有个坑——不是所有操作都能硬件加速。比如Canvas.clipPath()在某些设备上就会回退到软件绘制。我在项目中遇到过,一个自定义View用了clipPath做圆形裁剪,结果在低端机上帧率直接掉到20fps。
核心原则:
- 尽量使用View的translationX/Y、rotation、alpha等属性做动画
- 避免在onDraw中创建Bitmap或Path对象
- 使用LayerType.HARDWARE为复杂View开启离屏缓冲
// 开启硬件层加速
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB) {
view.setLayerType(View.LAYER_TYPE_HARDWARE, null);
}
// 动画结束后记得关闭,否则浪费GPU内存
view.animate().translationX(100f).withEndAction {
view.setLayerType(View.LAYER_TYPE_NONE, null);
}.start();
我的习惯:做复杂动画时,我会在动画开始前开启硬件层,结束后立即关闭。这样既保证了流畅度,又不会长期占用GPU资源。
1.2 帧率监控:用数据说话
优化不能靠感觉。你得知道每一帧到底花了多少时间。Android提供了几个工具,我常用的有三个:
| 工具 | 用途 | 我的使用场景 |
|---|---|---|
| Choreographer | 获取每一帧的回调时间 | 监控动画帧率,定位卡顿帧 |
| FrameMetrics | 获取详细的帧渲染耗时 | 分析具体哪个阶段耗时高 |
| Profile GPU Rendering | 可视化GPU渲染管线 | 快速定位过度绘制 |
// 使用Choreographer监控帧率
class FpsMonitor {
private var lastFrameTime = 0L
private var frameCount = 0
private var fps = 0f
fun start() {
Choreographer.getInstance().postFrameCallback(object : Choreographer.FrameCallback {
override fun doFrame(frameTimeNanos: Long) {
val currentTime = System.currentTimeMillis()
if (lastFrameTime != 0L) {
val delta = currentTime - lastFrameTime
if (delta > 0) {
fps = 1000f / delta
}
}
lastFrameTime = currentTime
frameCount++
// 每秒打印一次FPS
if (frameCount % 60 == 0) {
Log.d("FpsMonitor", "当前帧率: $fps")
}
Choreographer.getInstance().postFrameCallback(this)
}
})
}
}
注意:Choreographer的回调是在UI线程执行的,不要在回调里做耗时操作。我曾经在回调里写了个Log,结果影响了帧率测量,搞了半天才发现是日志拖了后腿。
1.3 避免过度绘制:少画就是快
过度绘制,就是同一像素被重复绘制了多次。你想想看,一个按钮背景画了三次,那GPU就得算三次,帧率自然上不去。
Android提供了调试GPU过度绘制工具,打开后界面会变成彩色:
- 蓝色:过度绘制1次(正常)
- 绿色:过度绘制2次(可接受)
- 粉色:过度绘制3次(需要优化)
- 红色:过度绘制4次以上(必须优化)
我在项目中遇到过最夸张的情况——一个列表项,背景层叠了5层,整个屏幕都是红色的。排查后发现是主题、布局、代码里各设了一次背景色。
优化策略:
- 移除不必要的背景色,只保留最外层
- 使用merge标签减少布局层级
- 自定义View时,用canvas.clipRect()限制绘制区域
- 使用ViewStub延迟加载不立即显示的视图
// 自定义View中限制绘制区域
override fun onDraw(canvas: Canvas) {
// 只绘制可见区域
canvas.clipRect(0, 0, width, visibleHeight)
// 继续绘制...
super.onDraw(canvas)
}
1.4 内存泄漏排查:别让动画成为内存杀手
动画导致内存泄漏,这个坑我踩过不止一次。最常见的情况:Activity销毁了,但动画还在跑,持有了Activity的引用,导致整个Activity无法被回收。
为什么会这样?因为动画内部使用了Handler或Runnable,如果没在onStop或onDestroy中取消,它们会一直持有外部引用。
我曾经... 写了一个无限循环的旋转动画,忘记在Activity销毁时取消。结果用户来回切换页面几次,内存直接飙到200MB,最后被系统杀掉了。从那以后,我养成了习惯:所有动画都要在生命周期结束时取消。
// 安全的动画管理方式
class SafeAnimActivity : AppCompatActivity() {
private val animators = mutableListOf<Animator>()
fun startSafeAnim(view: View) {
val anim = ObjectAnimator.ofFloat(view, "translationX", 0f, 100f)
anim.duration = 1000
anim.repeatCount = ValueAnimator.INFINITE
animators.add(anim)
anim.start()
}
override fun onStop() {
super.onStop()
// 取消所有动画,防止内存泄漏
animators.forEach { it.cancel() }
animators.clear()
}
}
排查工具:我一般用LeakCanary配合MAT(Memory Analyzer Tool)。LeakCanary能直接告诉你哪个对象泄漏了,MAT可以分析泄漏路径。两个工具配合使用,基本能搞定90%的内存泄漏问题。
知识体系总览
下面这张图,是我梳理的动画性能优化核心脉络。你可以把它当作一个检查清单,每次做动画优化时对照着看:
嗯,这四个方面其实环环相扣。硬件加速是基础,帧率监控是手段,避免过度绘制是方法,内存泄漏排查是保障。做动画优化时,我习惯先开帧率监控看看问题在哪,然后针对性地优化硬件加速或减少过度绘制,最后再检查一下有没有内存泄漏。
记住一个原则:动画性能优化不是一次性工作,而是持续的过程。每次新增动画或修改布局后,都跑一遍这些检查,才能保证你的动画始终丝滑流畅。