一、动画性能优化:让每一帧都丝滑流畅

做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层,整个屏幕都是红色的。排查后发现是主题、布局、代码里各设了一次背景色。

优化策略:

  1. 移除不必要的背景色,只保留最外层
  2. 使用merge标签减少布局层级
  3. 自定义View时,用canvas.clipRect()限制绘制区域
  4. 使用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%的内存泄漏问题。

知识体系总览

下面这张图,是我梳理的动画性能优化核心脉络。你可以把它当作一个检查清单,每次做动画优化时对照着看:

动画性能优化 硬件加速 帧率监控 避免过度绘制 内存泄漏排查 GPU分担CPU绘图 LayerType控制 避免clipPath等操作 Choreographer回调 FrameMetrics分析 Profile GPU Rendering 移除冗余背景 merge/ViewStub clipRect限制区域 生命周期取消动画 LeakCanary + MAT

嗯,这四个方面其实环环相扣。硬件加速是基础,帧率监控是手段,避免过度绘制是方法,内存泄漏排查是保障。做动画优化时,我习惯先开帧率监控看看问题在哪,然后针对性地优化硬件加速或减少过度绘制,最后再检查一下有没有内存泄漏。

记住一个原则:动画性能优化不是一次性工作,而是持续的过程。每次新增动画或修改布局后,都跑一遍这些检查,才能保证你的动画始终丝滑流畅。

公众号:蓝海资料掘金营,微信deep3321