13、动画与硬件加速:硬件加速原理、图层类型(LAYER_TYPE_HARDWARE)、动画性能优化技巧

说到动画性能,很多同学第一反应是「用属性动画别用补间动画」、「减少嵌套布局」。这些都对,但真正决定动画能不能跑满 60fps 的,往往是底层那个看不见的东西——硬件加速

我最早接触硬件加速是在做一款电商首页的轮播图时。那时候用 ViewPager + Fragment,切换动画总是掉帧。后来发现是每次切换都触发了完整的 View 重绘。嗯,从那以后我就开始认真研究硬件加速了。

13.1 硬件加速到底在加速什么?

说白了,硬件加速就是把 View 的绘制工作从 CPU 搬到 GPU。CPU 擅长逻辑运算,GPU 擅长并行渲染。动画的本质是每帧都在改变视图状态,如果每次改变都让 CPU 去重新计算像素点,那效率太低了。

硬件加速的核心流程是这样的:

  1. 记录绘制指令:View 的 onDraw() 不再直接画到屏幕上,而是生成一系列 OpenGL 指令。
  2. 缓存显示列表:这些指令被缓存起来,形成「显示列表」(Display List)。
  3. GPU 执行:每帧只需要把显示列表提交给 GPU,GPU 负责渲染。

你想想看,如果动画只是改变 View 的位置、缩放、透明度,那显示列表本身根本没变,只是变换矩阵变了。GPU 处理这种变换简直是小菜一碟。

关键结论:硬件加速对 translationX/YscaleX/Yrotationalpha 这类属性动画效果最好,因为它们只改变变换矩阵,不触发重绘。

下面这张图展示了硬件加速与软件绘制的核心区别:

硬件加速 vs 软件绘制 软件绘制(CPU) 每帧:onDraw() 完整执行 CPU 逐像素计算 直接写入帧缓冲区 硬件加速(GPU) 首次:生成显示列表 动画帧:仅更新变换矩阵 GPU 并行渲染

13.2 LAYER_TYPE_HARDWARE 的正确用法

Android 提供了三种图层类型:

图层类型 常量值 适用场景
LAYER_TYPE_NONE 0 默认,每次重绘都走 onDraw()
LAYER_TYPE_SOFTWARE 1 用 Bitmap 缓存,适合不支持硬件加速的旧设备
LAYER_TYPE_HARDWARE 2 用 GPU 纹理缓存,适合复杂动画

我个人的习惯是:只在动画开始前设置 LAYER_TYPE_HARDWARE,动画结束后立即恢复为 NONE。为什么?因为硬件图层会占用 GPU 纹理内存,一直开着反而浪费资源。

最佳实践

// 动画开始前
view.setLayerType(View.LAYER_TYPE_HARDWARE, null)

// 执行动画
view.animate()
    .translationX(200f)
    .setDuration(300)
    .withEndAction {
        // 动画结束后恢复
        view.setLayerType(View.LAYER_TYPE_NONE, null)
    }
    .start()

这里有个坑——如果动画过程中 View 的内容频繁变化(比如文字更新、图片加载),硬件图层反而会变慢。因为每次内容变化都要重新上传纹理到 GPU,这个开销比 CPU 直接绘制还大。

我曾经踩过的坑

做一个列表项的展开动画,里面包含一个网络加载的图片。我提前设置了 LAYER_TYPE_HARDWARE,结果动画卡成狗。后来发现是图片加载完成后触发了纹理重新上传。解决方案是:等图片加载完再开启动画,或者用 LAYER_TYPE_SOFTWARE。

13.3 动画性能优化技巧

下面这些技巧是我在多个项目里验证过的,每个都解决过实际的性能问题。

13.3.1 使用 ViewPropertyAnimator 而非 ObjectAnimator

ViewPropertyAnimator 是专门为 View 的属性动画优化的。它内部做了批量处理,多个属性可以合并到同一帧提交。而 ObjectAnimator 每次只改变一个属性,多个动画同时跑时会有额外开销。

// 推荐:ViewPropertyAnimator
view.animate()
    .translationX(100f)
    .translationY(200f)
    .alpha(0.5f)
    .setDuration(300)
    .start()

// 不推荐:多个 ObjectAnimator 同时执行
ObjectAnimator.ofFloat(view, "translationX", 100f).start()
ObjectAnimator.ofFloat(view, "translationY", 200f).start()
ObjectAnimator.ofFloat(view, "alpha", 0.5f).start()

13.3.2 避免在动画过程中触发 measure/layout

动画只改变 translationX/YscaleX/Y 这些属性时,不会触发 measure 和 layout。但如果你改变了 layoutParams 或调用了 requestLayout(),那就会触发完整的布局流程。这个开销非常大。

我记得有一次排查一个列表滑动动画卡顿的问题,发现是某个 View 在动画过程中调用了 setVisibility(View.GONE),导致父布局重新 measure。改成用 setAlpha(0f) 配合 setClickable(false) 就解决了。

13.3.3 使用 outline 裁剪而非 clipChildren

如果你需要圆形裁剪或圆角裁剪,用 ViewOutlineProvider 替代 clipChildren。前者是硬件加速友好的,后者会触发软件绘制。

// 硬件加速友好的圆形裁剪
view.outlineProvider = object : ViewOutlineProvider() {
    override fun getOutline(view: View, outline: Outline) {
        outline.setOval(0, 0, view.width, view.height)
    }
}
view.clipToOutline = true

13.3.4 减少过度绘制

过度绘制(Overdraw)是动画性能的隐形杀手。每多一层绘制,GPU 就要多处理一层像素。用开发者选项里的「显示过度绘制区域」来检查,红色区域越多,性能越差。

我常用的优化手段:

  • 去掉不必要的背景(特别是 RecyclerView 的 item 背景)
  • merge 标签减少布局层级
  • 自定义 View 时用 canvas.clipRect() 限制绘制区域

13.3.5 使用 Profile GPU Rendering 定位问题

Android 开发者选项里有个「Profile GPU Rendering」,打开后可以看到每帧的绘制时间。绿色横线是 16ms 基准线,超过这条线就意味着掉帧。

我一般这样分析:

  • 蓝色条太高:说明显示列表创建/更新耗时,检查是否频繁触发 invalidate()
  • 红色条太高:说明 GPU 渲染耗时,检查是否有复杂路径或大量 Bitmap
  • 黄色条太高:说明 CPU 等待 GPU 完成,检查是否开启了硬件图层但内容频繁变化

13.4 实战:一个高性能的点赞动画

最后分享一个我实际用过的点赞动画优化方案。需求是点击按钮后,一个心形图标从按钮位置飘到屏幕顶部,同时缩放并淡出。

// 优化后的点赞动画
fun animateHeart(anchorView: View) {
    val heart = ImageView(context).apply {
        setImageResource(R.drawable.ic_heart)
        setLayerType(View.LAYER_TYPE_HARDWARE, null) // 开启硬件加速
    }
    
    // 将心形添加到根布局,避免受父容器裁剪影响
    val rootView = anchorView.rootView as ViewGroup
    rootView.addView(heart, LayoutParams(48.dp, 48.dp))
    
    // 使用 AnimatorSet 组合多个属性
    val animator = AnimatorSet().apply {
        playTogether(
            ObjectAnimator.ofFloat(heart, View.TRANSLATION_Y, 0f, -500f),
            ObjectAnimator.ofFloat(heart, View.SCALE_X, 1f, 0.3f),
            ObjectAnimator.ofFloat(heart, View.SCALE_Y, 1f, 0.3f),
            ObjectAnimator.ofFloat(heart, View.ALPHA, 1f, 0f)
        )
        duration = 800
        interpolator = AccelerateInterpolator()
        addListener(object : AnimatorListenerAdapter() {
            override fun onAnimationEnd(animation: Animator) {
                heart.setLayerType(View.LAYER_TYPE_NONE, null) // 恢复
                rootView.removeView(heart)
            }
        })
    }
    animator.start()
}

这个方案的核心思路:

  • 把动画 View 加到根布局,避免被父容器裁剪
  • 动画开始前设置 LAYER_TYPE_HARDWARE
  • 动画结束后立即移除并恢复图层类型
  • 所有属性变化都在 GPU 层面完成,不触发重绘

性能数据对比(来自我的一次实际测试):

方案 平均帧耗时 掉帧次数(100次动画)
未优化(默认绘制) 22ms 47次
使用 LAYER_TYPE_HARDWARE 8ms 3次
优化后(硬件加速 + 根布局) 5ms 0次

嗯,这就是我关于硬件加速和动画性能优化的全部经验了。说白了,核心就三点:能用 GPU 就别用 CPU动画期间别触发重绘用完硬件图层记得关。记住这三条,你的动画性能基本不会差。

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