首页 > 应用开发 > IT综合 > 动效开发不踩坑:几种动效实现方案对比与实战选型
作者:vivo 互联网大前端团队 - Xu Jie
本文从 Android 渲染系统内核出发,系统拆解 Native、Lottie、PAG、SurfaceView、TextureView五种动效方案的底层原理,通过多维度量化测试建立性能评估模型,结合实战场景提供可落地的优化策略与选型体系,帮助开发者突破动效性能瓶颈。
1分钟看图掌握核心要点👇
随着Android设备硬件性能的提升,用户对动效的流畅度、复杂度要求日益提高。从简单的View属性动画到复杂的品牌营销动效,不同场景下的技术选型直接影响应用的性能表现(如帧率、内存占用)和开发效率。然而,多数开发者仅停留在会用层面,缺乏对渲染机制、性能瓶颈的深度理解,导致动效场景出现卡顿、OOM、兼容性问题。
本文将从 内核原理 → 量化测试 → 优化实践 → 工程落地 四个维度,构建完整的动效技术知识体系,核心解决三大问题:
要理解动效性能差异,需先掌握Android渲染系统的核心机制 —— 这是所有动效方案的底层依赖。
Android渲染系统遵循 CPU → GPU →屏幕 的流水线模型,关键链路如下:
关键链路
应用层绘制指令(Canvas/OpenGL)
→ CPU 预处理(布局计算、绘制指令封装)
→ RenderThread 调度
→ GPU 渲染(顶点着色、片元着色、纹理采样)
→ FrameBuffer 帧缓冲 → VSYNC 信号同步 → 屏幕显示
这个流程涉及下面几个概念:
为了方便探究,我们选择一个60Hz的手机做后面一系列的验证。
2.2.1 资源的加载时长
动效资源的加载时长是指:从触发动效资源加载(如调用setAnimation()、load()方法)开始,到动效资源完全解析、解码、初始化完成,达到可第一次播放状态的总时长。
本文不是去探讨资源加载是在主线程还是子线程,只关注加载时长本身,所以后面都默认在主线程加载资源的耗时作为分析标准。
资源加载的几种类型:动效载体文件(Lottie JSON/JSONZ、PAG文件、帧序列图、矢量资源)、依赖资源(动效中的位图、字体、渐变素材)。
2.2.2 每帧刷新时间 16.67ms
本质:这是屏幕硬件的固定刷新间隔,由手机屏幕刷新率决定,和动效、App、系统软件无关。
性质:固定不变、不可修改(除非切换屏幕刷新率,比如从60Hz切到120Hz,刷新时间就变成8.3ms)。它是一个时间窗口—— 系统必须在这个窗口内完成一帧的绘制、提交,才能保证这一帧被正常显示,不出现掉帧。
核心作用:作为动效性能评估的合格标尺,规定了每帧绘制的最长允许耗时,超过这个时间窗口,就会触发掉帧。为了能够从代码角度直观评估刷帧是否在规定时间内,我们可以使用下面的代码在动效执行的时候给予监听。
private var lastFrameTime: Long = 0
private val frameCallback = object : Choreographer.FrameCallback {
override fun doFrame(frameTimeNanos: Long) {
if (lastFrameTime != 0L) {
val frameInterval = (frameTimeNanos - lastFrameTime) / 1_000_000f
android.util.Log.d(TAG, "【VSYNC帧间隔】= $frameInterval ms")
}
lastFrameTime = frameTimeNanos
Choreographer.getInstance().postFrameCallback(this)
}
}
private fun verifyChoreographerAndVSYNC() {
// 核心API:注册Choreographer的帧回调,绑定VSYNC信号
Choreographer.getInstance().postFrameCallback(frameCallback)
// 跟随Choreographer/VSYNC的动画
执行动画的代码...
}若打印的结果如下:
2026-01-1919:57:38.58710712-10712 NativeAnim...tivity_TAG com.ne.animationproject D 【VSYNC帧间隔】= 16.244213 ms说明帧间隔严格稳定在16.67ms左右(60Hz 屏幕),无任何波动,证明Choreographer 的帧回调完全绑定VSYNC信号,你的动画也是符合要求的。
需要说明的是,后面的动效效果验证过程中,会对比硬件加速开启和关闭对动效性能的影响,同时会去观察刷帧是否正常,以Lottie动效举例,在播放相同动效文件前提下,硬件加速开启和关闭对刷帧的频率是不影响的。
lottie动效-关闭硬件加速:
2026-02-0219:58:09.49614588-14588 NativeAnim...tivity_TAG com.ne.animationproject V 【Lottie更新】动画进度: 0.06199528 threadName = Thread[main,5,main] <078>
2026-02-0219:58:09.51214588-14588 NativeAnim...tivity_TAG com.ne.animationproject D 【Lottie帧同步】帧间隔=16.58ms, 进度=0.06 threadName = Thread[main,5,main] <080>
2026-02-0219:58:09.51414588-14588 NativeAnim...tivity_TAG com.ne.animationproject V 【Lottie更新】动画进度: 0.07754053 threadName = Thread[main,5,main] <081>
2026-02-0219:58:09.54914588-14588 NativeAnim...tivity_TAG com.ne.animationproject D 【Lottie帧同步】帧间隔=33.18ms, 进度=0.08 threadName = Thread[main,5,main] <086>
2026-02-0219:58:09.54914588-14588 NativeAnim...tivity_TAG com.ne.animationproject V 【Lottie更新】动画进度: 0.10866043 threadName = Thread[main,5,main] <087>
2026-02-0219:58:09.57214588-14588 NativeAnim...tivity_TAG com.ne.animationproject D 【Lottie帧同步】帧间隔=16.60ms, 进度=0.11 threadName = Thread[main,5,main] <092>lottie动效-开启硬件加速:
2026-02-0219:55:04.70413911-13911 NativeAnim...tivity_TAG com.ne.animationproject V 【Lottie更新】动画进度: 0.26404682 threadName = Thread[main,5,main] <127>
2026-02-0219:55:04.72513911-13911 NativeAnim...tivity_TAG com.ne.animationproject D 【Lottie帧同步】帧间隔=16.58ms, 进度=0.26 threadName = Thread[main,5,main] <132>
2026-02-0219:55:04.72513911-13911 NativeAnim...tivity_TAG com.ne.animationproject V 【Lottie更新】动画进度: 0.27959943 threadName = Thread[main,5,main] <133>
2026-02-0219:55:04.72913911-13911 NativeAnim...tivity_TAG com.ne.animationproject D 【Lottie帧同步】帧间隔=16.59ms, 进度=0.28 threadName = Thread[main,5,main] <135>
2026-02-0219:55:04.73013911-13911 NativeAnim...tivity_TAG com.ne.animationproject V 【Lottie更新】动画进度: 0.29515955 threadName = Thread[main,5,main] <136>
2026-02-0219:55:04.76113911-13911 NativeAnim...tivity_TAG com.ne.animationproject D 【Lottie帧同步】帧间隔=16.59ms, 进度=0.30 threadName = Thread[main,5,main] <141>结论:刷帧的频率是不变的,一直维持在16.67ms,如下图所示
但是也存在不是这个值的情况,一般是卡顿丢帧导致的,如下图所示
2.2.3 单帧真实绘制耗时
本质:每帧的绘制时间是指动效完成一帧内容绘制、提交的实际执行耗时,从接收到VSYNC信号开始绘制到绘制完成并提交给GPU/SurfaceFlinger为止的总时间。
性质:动态波动、受多种因素影响,没有固定值。影响它的因素包括:动效复杂度(图层多少、路径是否复杂)、设备性能(中低端机vs高端机)、当前系统负载(是否有其他App在抢占资源)、绘制优化程度(是否减少Draw Call、避免离屏渲染)。
核心作用:反映动效绘制的真实性能开销,是需要被优化、被验证是否符合标尺要求的对象。
在这里我们只考虑性能方面,那么对于性能衡量的3个标准有了,总结一下动效执行的核心瓶颈集中在:
瓶颈点,也是要突破的技术难点,下面就通过Android常见的几种动效方案的原理介绍,说明每种动效有什么优劣势。
Native 动效按照常使用的类型,可以概括为下面几类:
以上动画归结起来如下:
3.1.1 验证视图动画和属性动画的区别是View的属性是否变化
/**
* 验证原理(核心重点,最容易理解的优化点):
* 一、View动画(TranslateAnimation/ScaleAnimation等):
* 1. 本质:修改的是 View的绘制矩阵(AnimationMatrix),只改变「视觉上的绘制位置/大小」,不改变View的真实属性(translationX/left/top等)
* 2. 开销:每次onDraw都会执行「矩阵变换计算」,CPU需要把原始坐标 × 变换矩阵,得到绘制坐标,有固定开销
* 3. 致命问题:动画结束后,View会瞬间回弹到原始位置,因为真实属性没变;且点击事件还是响应原始位置
* 二、属性动画(ObjectAnimator/ValueAnimator):
* 1. 本质:直接修改 View的「真实绘制属性」(translationX/alpha/scaleX等),这些属性是View的成员变量
* 2. 开销:硬件加速下,这些属性是GPU的「渲染状态参数」,无需CPU矩阵变换,绘制时直接用属性值,零额外开销
* 3. 优势:动画结束后,View停留在目标位置,真实属性同步修改,点击事件响应新位置
*/
private fun verifyViewAnimVsPropAnim(){
android.util.Log.d(TAG, "===== 开始验证:View动画 VS 属性动画 =====")
// View动画:平移动画,从0到500,执行矩阵变换
val viewAnim = TranslateAnimation(0f, 500f, 0f, 0f).apply {
duration = 3000
fillAfter = true// 动画结束后保持视觉状态,但真实属性不变
}
viewAnimView.startAnimation(viewAnim)
Handler(Looper.getMainLooper()).postDelayed({
android.util.Log.d(TAG, "View动画执行中,真实translationX = ${viewAnimView.translationX}") // 打印:0.0 → 真实属性没变
}, 500)
// 属性动画:平移动画,从0到500,直接修改translationX属性
val propAnim = ObjectAnimator.ofFloat(propAnimView, "translationX", 0f, 500f).apply {
duration = 3000
}
propAnim.start()
Handler(Looper.getMainLooper()).postDelayed({
android.util.Log.d(TAG, "属性动画执行中,真实translationX = ${propAnimView.translationX}") // 打印:持续变化 → 真实属性已改
}, 500)
// 动画结束后,点击两个View,能看到:红色View(View动画)点击新位置无响应,蓝色View(属性动画)点击新位置有响应
}打印的结果如下:
2026-01-2215:23:29.14426874-26874 NativeAnim...tivity_TAG com.ne.animationproject D ===== 开始验证:View动画 VS 属性动画 =====
2026-01-2215:23:29.64826874-26874 NativeAnim...tivity_TAG com.ne.animationproject D View动画执行中,真实translationX = 0.0
2026-01-2215:23:29.64826874-26874 NativeAnim...tivity_TAG com.ne.animationproject D 属性动画执行中,真实translationX = 31.812086结论:
3.1.2 工作原理解读
属性动画修改translationX等属性后,最终要通过GPU合成显示到屏幕上,而CPU和GPU是两个独立的硬件单元,拥有各自独立的内存(CPU内存/堆内存、GPU显存),因此存在固有的数据同步流程:
这个 CPU更新参数→拷贝到GPU显存的流程,是异构计算的固有开销,无法避免。
属性动画的一些限制存在下面几点:
(1)单View、单属性动画下,同步开销极低(纳秒/微秒级),几乎无感知,不会影响帧率;
(2)多View、多属性同步动画(如网格布局所有Item 同时做入场动画)下,大量RenderNode参数需要同步,跨硬件拷贝的开销会累积,导致:
(3)低端设备上,硬件总线带宽较低,跨硬件拷贝的开销更明显,可能出现轻微的动画 “拖影”。而“拖影”其实就是掉帧等原因引起的。
"拖影"无法根除的原因:
该开销源于CPU与GPU的硬件独立性,只要依赖GPU加速显示,就必须进行跨硬件数据同步,除非放弃硬件加速(转而使用CPU软件绘制,开销更高),因此是固有且无法根除的。
上面这些理论内容还是比较抽象的,下面看下具体的表述,不管是View(视图)动画,还是属性动画,都需要使用到Canvas进行绘制,这一点从下面的profile的CPU Hotspots可以确认。
那我们可以直接来看Canvas绘制动画的性能表现如何,就知道不管是视图动画还是属性动画,存在什么优缺点了。
3.1.3 瓶颈在哪
再来看下硬件加速对Canvas绘制的作用(绘制10000个圆)
/**
* 验证原理(Android绘制的核心优化点):
* 1. 硬件加速开启时(默认开启):View的onDraw中的Canvas → 实际是「GLES20Canvas」的实例
* 所有canvas.drawCircle/drawRect/drawBitmap 等绘制指令 → 直接翻译成OpenGL ES 2.0的GPU绘制指令
* 光栅化(将矢量图形转像素点)的过程由GPU完成,跳过了CPU的软件光栅化步骤,效率提升10~100倍
* 2. 硬件加速关闭时:Canvas → 实际是「SoftwareCanvas」的实例
* 所有绘制指令都是CPU软件计算,光栅化也由CPU完成,效率极低,绘制复杂图形会卡顿
* 3. 验证方式:反射+类型判断 打印Canvas真实类型 + 绘制大量图形对比耗时
*/
private fun verifyHardwareAccelerateCanvas(){
android.util.Log.d(TAG, "===== 开始验证:硬件加速Canvas类型验证 =====")
// 1. 硬件加速【开启】状态下,打印Canvas真实类型(默认开启)
performanceView.post {
performanceView.setLayerType(View.LAYER_TYPE_HARDWARE, null)
// 设置绘制回调
performanceView.onDrawFinish = { canvasType, costTime ->
android.util.Log.d(TAG, "硬件加速开启 → Canvas真实类型 = LAYER_TYPE_HARDWARE")
android.util.Log.d(TAG, "硬件加速开启 → 绘制耗时 = $costTime ms")
}
// 触发系统绘制流程(关键:必须用invalidate请求重绘)
performanceView.needDrawBigTask = true
performanceView.invalidate()
}
// 2. 验证【硬件加速关闭】状态(延迟1秒确保第一次绘制完成)
performanceView.postDelayed({
performanceView.setLayerType(View.LAYER_TYPE_SOFTWARE, null)
performanceView.onDrawFinish = { canvasType, costTime ->
android.util.Log.d(TAG, "硬件加速关闭 → Canvas真实类型 = LAYER_TYPE_SOFTWARE")
android.util.Log.d(TAG, "硬件加速关闭 → 绘制耗时 = $costTime ms")
}
performanceView.needDrawBigTask = true
performanceView.invalidate()
}, 1000)
}硬件加速开启情况下打印的结果如下:
2026-02-0220:24:45.09017488-17488 NativeAnim...tivity_TAG com.ne.animationproject D 硬件加速关闭 → Canvas真实类型 = View.LAYER_TYPE_SOFTWARE
2026-02-0220:24:45.09017488-17488 NativeAnim...tivity_TAG com.ne.animationproject D 硬件加速关闭 → 绘制耗时 = 34 ms threadName = Thread[main,5,main]结论:可以看到,硬件加速可以降低动画的绘制时间,即使开启了硬件加速,仍然存在一些弊端,无法做到快速的绘制完成一帧,可以看到每帧的绘制耗时超过了16.67ms,达到了20ms,出现了掉帧问题。
另外,Canvas绘制动画时是运行在主线的,可以通过下面这张图看到:
这里使用Canvas绘制圆的时候,是绘制了10000个圆,硬件加速开启情况下,我们改为100个圆,看下绘制时间:
2026-02-0220:25:47.29317488-17488 NativeAnim...tivity_TAG com.ne.animationproject D 硬件加速开启 → Canvas真实类型 = View.LAYER_TYPE_HARDWARE
2026-02-0220:25:47.29317488-17488 NativeAnim...tivity_TAG com.ne.animationproject D 硬件加速开启 → 绘制耗时 = 20 ms threadName = Thread[main,5,main]结论:可以看到,硬件加速开启情况下,绘制100个圆的时间完全是满足要求的,无掉帧问题。
到这里:总结一下,属性动画的瓶颈主要还是渲染时长问题,为了解决这个问题,开启硬件加速可以提升native动画的绘制性能,但是当做复杂动画的时候,可能会出现掉帧问题;动画简单的时候,不会出现掉帧问题。
Lottie被称为JSON驱动的矢量渲染引擎,核心是它渲染的动画素材是 矢量图形,而非位图(像素图),且渲染过程完全由 JSON 动画描述文件驱动,无需依赖像素资源。
概念说明
lottie动画有自己的优势,关键有两个:
3.2.1 跨平台
Lottie 的矢量渲染引擎之所以能跨 Android、iOS、前端等平台,核心是它依赖的是各平台标准的矢量绘制能力:
3.2.2 可以实现复杂的动画
因为是矢量图,矢量图的绘制过程大致有三步:
lottie动画可以支持复杂路径动画、渐变、蒙版(基于矢量参数),基于此,很多开发工程师和交互设计师都会选择使用lottie动画。但是,lottie动画有没有什么缺点呢?这一点其实在很多开发工程师分析应用ANR的时候会进行思考,那么我们就做一下验证和剖析。
3.2.3 工作原理解读
Lottie 的核心是跨平台动画描述→原生渲染,架构分为三层:
1.解析层
解析AE导出的JSON文件,构建LottieComposition
(包含图层、关键帧、特效信息),Lottie-
Composition不是简单的 “数据容器”,而是动画元数据&执行上下文的综合体,核心字段:
// 源码简化版
publicclassLottieComposition {
publicfinal Map<Long, Layer> layers; // 图层ID→图层映射(支持图层引用)
publicfinal List<Keyframe<?>> keyframes; // 全局关键帧池(复用关键帧数据)
publicfinalfloat frameRate; // 动画帧率(AE导出的原始帧率,如30/60fps)
publicfinalfloat startFrame; // 动画起始帧
publicfinalfloat endFrame; // 动画结束帧
publicfinal Size bounds; // 动画画布尺寸(决定渲染视口)
publicfinal Map<String, LottieImageAsset> images; // 位图资源映射(ImageLayer用)
publicfinal Map<String, LottieFont> fonts; // 字体资源映射(TextLayer用)
}2.场景层
基于Composition构建Layer树(ShapeLayer/
ImageLayer/TextLayer), Lottie 6.0 支持的Layer类型覆盖 AE 核心图层,且每种Layer有专属渲染逻辑:
3.渲染层
通过 Canvas/OpenGL 将Layer树逐帧渲染到屏幕,渲染层的核心是将 Layer 树的动态状态转化为屏幕像素,Lottie 6.0 提供Canvas管线和OpenGL管线,且支持离屏渲染。
关键底层逻辑
OpenGL管线会将Layer绘制指令(如drawPath/drawCircle)转化为GPU着色器指令,路径的数学参数传递给GPU,由GPU完成光栅化跳过CPU像素计算); 两种管线共享Layer树的动画状态`,仅绘制阶段的实现不同,上层无感知(符合 “场景层-渲染层” 解耦设计)。
离屏渲染(Offscreen Rendering)
Lottie对蒙版、混合模式、PreCompLayer等场景会触发离屏渲染:
3.2.4 瓶颈在哪
1.JSON解析、图层构建、动画数据预处理等流程的耗时
2026-01-2316:25:32.32923631-23631 NativeAnim...tivity_TAG com.ne.animationproject D 【Lottie JSON解析】耗时: 520ms <815>
2026-01-2316:25:32.32923631-23631 NativeAnim...tivity_TAG com.ne.animationproject D 【Lottie信息】总帧数: 31.99, 时长: 1066.0ms <816>JSON解析以及图层渲染,竟然达到500ms,这是因为JSON 文件为文本格式,解析时需大量字符串处理与对象创建,JSON文本内容大致如下:
现在我们可以回答两个问题:
(1)要是开启硬件加速,JSON文件的解析速度可以提升吗?
结果是不会提速,因为View级硬件加速仅作用于 Lottie 的渲染层(Canvas/OpenGL绘制),与解析层的JSON初始化无关,因此耗时不变;
但是当你给整个Activity设置了硬件加速后,系统层面优化进程的CPU/内存调度,间接释放主线程资源,让JSON解析(主线程执行)更快,因此耗时减少。
经过验证,Activity设置加速后的JSON解析时间减少到200ms。
(2)可以把JSON解析的耗时操作放在子线程吗?
虽然可以在子线程解析JSON文件,但是解析的时间仍然是跟主线程一样,所以仍然没有节省掉解析文件的时间。
2.绘制一帧,真实需要多长时间
直接看硬件加速开启情况下,单帧渲染的时间大致如下:
2026-02-0220:19:20.05916214-16214 CustomLottie_TAG com.ne.animationproject D Lottie单帧渲染耗时:21 ms <298>
2026-02-0220:19:20.07516214-16214 CustomLottie_TAG com.ne.animationproject D Lottie单帧渲染耗时:11 ms <302>
2026-02-0220:19:20.12516214-16214 CustomLottie_TAG com.ne.animationproject D Lottie单帧渲染耗时:20 ms <308>可以看到,每帧的渲染耗时,有的会超过16.67ms,有的不会,表现在VSYNC同步帧上面,如下:
2026-02-0220:19:20.06116214-16214 NativeAnim...tivity_TAG com.ne.animationproject V 【Lottie更新】动画进度: 0.9487896 threadName = Thread[main,5,main] <300>
2026-02-0220:19:20.07816214-16214 NativeAnim...tivity_TAG com.ne.animationproject D 【Lottie帧同步】帧间隔=16.59ms, 进度=0.95 threadName = Thread[main,5,main] <303>
2026-02-0220:19:20.07916214-16214 NativeAnim...tivity_TAG com.ne.animationproject V 【Lottie更新】动画进度: 0.96435076 threadName = Thread[main,5,main] <304>
2026-02-0220:19:20.09516214-16214 NativeAnim...tivity_TAG com.ne.animationproject D 【Lottie帧同步】帧间隔=16.59ms, 进度=0.96 threadName = Thread[main,5,main] <305>
2026-02-0220:19:20.09716214-16214 NativeAnim...tivity_TAG com.ne.animationproject V 【Lottie更新】动画进度: 0.9799048 threadName = Thread[main,5,main] <306>
2026-02-0220:19:20.12716214-16214 NativeAnim...tivity_TAG com.ne.animationproject D 【Lottie帧同步】帧间隔=33.18ms, 进度=0.98 threadName = Thread[main,5,main] <309>由于有些帧的绘制时间超过16.67ms,所以无法在16.67ms这个时间内完成一帧的刷新,只能在下一次刷新的时候完成上屏,如上面日志的33.18ms所示,对于人眼来说,虽然没有明显感知,但是可以确定的是,json动画文件播放时,是存在掉帧问题的。
总结一下:JSON动画可以依靠硬件加速提升渲染时间,渲染时间在可接受范围内,但是JSON文件的初始化时间却省不了,所以当你有个很复杂的JSON动效文件的时候,比如JSON动画文件里面的图层、元素过多的时候,或者矢量路径等较为复杂的时候,很容易出现掉帧问题,需要慎重考虑其是否会让你引入这个动画文件的页面更加的卡顿,从而导致ANR等问题的产生。
PAG是腾讯自主研发的一套完整的动画工作流解决方案,助力于将 AE 动画方便快捷的应用于各平台终端。和Lottie、SVGA相比,支持的AE特性更多,支持的平台更广(增加了 mac OS、Windows和Linux),性能方面也做了深层次的优化,支持图层编辑,可以与视频编辑场景紧密结合。目前已经广泛应用于公司内外几十款APP,包括国民级APP 微信、QQ、腾讯视频、QQ 音乐、QQ 空间等,PAG 的流程类似Lottie,设计师使用AE设计好动画以后,通过PAGExporter插件读取AE工程文件,根据具体需求选择矢量导出、BMP预合成、混合导出方式中的一种导出一个PAG二进制文件,客户端对该PAG二进制文件进行解码、渲染,各端共享一套C++实现,平台端只做接口封装,具体的工作流程可以在PAG官网获取。不过PAG需要默认开启硬件加速,否则播放在一些机型上无法播放。
打开一个pag文件,如下图所示,这是一个十六进制表述的二进制文件:
下面我们重点介绍PAG的工作原理。
3.3.1 工作原理解读
PAG 采用 C++内核 + 跨平台 API架构,核心及优势分为:.pag文件是二进制文件,对于同一个动画文件来说,导出.pag相对于导出成JSON格式,体积可以减少50%以上。除此之外,.pag文件采用ProtoBuf序列化,解析速度比JSON 快,打开一个800kb的pag文件,只需要5ms,相对于前面的JSON文件打开需要耗时500ms,速度得到了极大的提升;
2026-01-2721:30:31.71023458-23458 NativeAnim...tivity_TAG com.ne.animationproject D 【PAG 文件解析】耗时: 5ms <782>
2026-01-2721:30:31.71023458-23458 NativeAnim...tivity_TAG com.ne.animationproject D PAG文件加载完成,总时长:1000000ms <783>3.3.2 瓶颈在哪
无论是属性动画,还是lottie动画,都需要经过Canvas绘制,而Canvas的绘制指令构建是在CPU端完成的(无论硬件加速与否),Lottie 要绘制矢量路径、图层,必然会调用Canvas的核心绘制 API(如drawPath()、drawColor()、drawBitmap())。
下面是通过CPU Profiler录制方法调用轨迹,直接抓到这些API的调用,且调用栈来自Lottie 相关类,就是Lottie 基于Canvas绘制的最直接证据。
而对于PAG动画来说,前面提到,必须开启硬件加速,并且运行在GPU上,所以就没有Canvas的绘制,那现在要看的就是PAG动画的绘制性能如何。
绘制线程与绘制时间
2026-02-0411:18:50.2817140-7310 PagViewActivity_TAG com.ne.animationproject D 【PAG绘制】onAnimationUpdate触发, 距离上次耗时: 15.89ms, 线程: tgfx_JNIEnvironment <310>
2026-02-0411:18:50.2937140-7311 PagViewActivity_TAG com.ne.animationproject D 【PAG绘制】onAnimationUpdate触发, 距离上次耗时: 12.07ms, 线程: tgfx_JNIEnvironment <312>
2026-02-0411:18:50.3137140-7309 PagViewActivity_TAG com.ne.animationproject D 【PAG绘制】onAnimationUpdate触发, 距离上次耗时: 14.30ms, 线程: tgfx_JNIEnvironment <314>
2026-02-0411:18:50.3267140-7311 PagViewActivity_TAG com.ne.animationproject D 【PAG绘制】onAnimationUpdate触发, 距离上次耗时: 12.23ms, 线程: tgfx_JNIEnvironment <317>PAG动画依赖tgfx引擎(腾讯为PAG打造的跨平台2D渲染引擎),而tgfx_JNIEnvironment 线程是PAG动画帧绘制的CPU 端核心工作线程,负责完成每帧动画的前置处理工作,具体职责包括:
简单说:这个线程的耗时,是PAG动画单帧总耗时的主要组成部分(占比通常70% 以上),它的耗时超标,直接决定了单帧无法在规定时间内完成处理。
既然运行在子线程,那就不会阻塞UI线程,再来看单帧的绘制耗时,跟上面的Lottie动画输出相同的动画,耗时基本维持在16.67ms以内,这是因为PAG使用了中间数据→局部位图的多级缓存与帧优化:做到了静态帧自动复用,减少了无效计算。
下面是PAG动画绘制一帧的时间示意,在动画重复执行的过程中,绘制时间其实是更短且趋于平稳的。
总结一下:PAG与Lottie性能对比(测试基线:Android15旗舰机/800KB同等复杂度动画文件/连续播放10次取平均值):
3.4.1 工作原理解读
SurfaceView的动效实现依赖四个核心组件,它们各司其职,构成完整的绘制闭环,这是理解工作原理的基础:
SurfaceView:承载Surface的容器,嵌入在View层级中,但绘制逻辑与View体系解耦,仅负责在屏幕上预留显示区域。SurfaceView参与主窗口的measure,layout,只不过draw的时候不是在当前主窗口的画布上draw的。在主窗口的画布上绘制的是黑色区域,通过下面这张示意图可以很清晰的知道布局关系
Surface:核心的独立绘制缓冲区,由系统SurfaceFlinger服务管理,拥有自己的像素缓存(显存),不与普通View共享缓冲区。动效的所有绘制内容都渲染在Surface上,这是它与普通View动效的核心区别。
SurfaceHolder:Surface的管理者和代理,提供lockCanvas()/unlockCanvasAndPost()等核心方法,负责缓冲区的锁定、解锁、提交,以及监听Surface的创建、销毁、尺寸变化(通过SurfaceHolder.Callback)
独立绘制线程:非主线程(自定义Thread/HandlerThread),负责执行动效的帧循环绘制,避免阻塞主线程(ANR),这是高性能动效的关键保障。
2026-02-0311:23:43.11510500-12244 AnimationS...ceView_TAG com.ne.animationproject D drawFrame threadName = Thread[Thread-1036,5,main]
2026-02-0311:23:43.13110500-12245 AnimationS...ceView_TAG com.ne.animationproject D drawFrame threadName = Thread[Thread-1037,5,main]
2026-02-0311:23:43.14810500-12246 AnimationS...ceView_TAG com.ne.animationproject D drawFrame threadName = Thread[Thread-1038,5,main]通过下图可以看得更清晰
通过工作原理,我们可能有这样的想法:如果自定义View需要进行频繁的刷新,或是刷新时数据处理量比较大,我们是不是可以使用SurfaceView来取代View,这时候不用考虑16.67ms的上屏限制了?带着这个疑问,我们做进一步的探究。
3.4.2 瓶颈在哪
SurfaceView的绘制依赖SurfaceFlinger管理的有限缓冲区队列(通常是双缓冲或三缓冲),这是其最核心的性能瓶颈,与编码规范无关,再来看下几个概念:
性能表现
2026-02-0609:51:16.62515462-20382 AnimationS...ceView_TAG com.ne.animationproject D 单帧绘制耗时:1.81ms / 1806μs | 帧间隔:16.62ms | 实时FPS:60 <849>
2026-02-0609:51:16.64015462-20383 AnimationS...ceView_TAG com.ne.animationproject D drawFrame threadName = Thread-4784 <850>
2026-02-0609:51:16.65915462-20384 AnimationS...ceView_TAG com.ne.animationproject D 单帧绘制耗时:1.71ms / 1708μs | 帧间隔:16.63ms | 实时FPS:60 <853>可以看到,绘制一帧的时间较短。
要说SurfaceView的瓶颈,在于Surface的创建与销毁完全由系统控制,与View的生命周期不完全同步。例如:
为了解决SurfaceView的这些核心问题,Android 4.0(API 14)引入了TextureView,它从底层设计上重构了渲染链路,完美规避了SurfaceView与View生命周期不同步的痛点:
3.5.1 工作原理解读
1.TextureView解决SurfaceView核心问题的底层逻辑
(1)生命周期完全对齐View体系
TextureView是普通View子类,其渲染载体 SurfaceTexture 的创建 / 销毁由开发者完全掌控,而非系统强制干预:
(2)渲染上下文持久化,无重建开销
TextureView的OpenGL纹理、解码器状态等渲染上下文,绑定的是SurfaceTexture而非系统级Surface:
(3)回调机制可靠,资源释放可控
TextureView的SurfaceTextureListener回调
(onSurfaceTextureAvailable/onSurfaceTextureDestroyed)与View生命周期强绑定:
2.TextureView 解决问题的同时,保留了高性能渲染能力
3.5.2 瓶颈在哪
虽然TextureView解决了SurfaceView的核心痛点,但并非所有场景都适用:
示意图大致如下:
第1步筛选:按照UI要求,这三个圆颜色可以变化,那么对于Lottie动画和PAG动画来说不适合,因为这两种方案都是建立在固定的资源文件上进行播放的,无法修改颜色。所以目前只能使用Native动画里面的Canvas绘制、以及SurfaceView绘制。
第2步筛选:三个圆需要不停的滚动,也就是说动画需要持续执行。另外这个动效是作为背景视图进行展示,所以不应该频繁的占用主线程去刷新,不然会导致前景刷新的掉帧等问题。可以把Canvas实现动画给过滤掉。
第3步筛选:动效的要求是圆圈的绘制,不是特别复杂的视频绘制,所以使用轻量级的TextureView就可以了。
下面基于TextureView实现上面的效果
代码流程大致如下:
// TextureView 实现三色圆轨迹动效核心伪代码
classApertureTextureViewextendsTextureViewimplementsSurfaceTextureListener {
// 1. 初始化参数
private Paint circlePaint;
private List<Circle> circleList; // 三色圆对象
private boolean isAnimRunning = false;
private RenderThread renderThread; // 子线程渲染
// 2. Surface创建回调(初始化)
@Override
publicvoidonSurfaceTextureAvailable(SurfaceTexture surface, int width, int height){
initCircleData(width, height); // 初始化圆的轨迹和颜色
startRenderThread(); // 启动渲染线程
}
// 3. 渲染线程核心逻辑
privateclassRenderThreadextendsThread {
@Override
publicvoidrun(){
long lastFrameTime = System.currentTimeMillis();
while (isAnimRunning) {
// 3.1 计算帧间隔(控制60FPS)
long currentTime = System.currentTimeMillis();
long frameInterval = Math.min(currentTime - lastFrameTime, 50);
lastFrameTime = currentTime;
// 3.2 更新圆的位置(轨迹计算)
updateCirclePosition(frameInterval);
// 3.3 绘制帧
Canvas canvas = lockCanvas();
if (canvas != null) {
canvas.drawColor(Color.TRANSPARENT, PorterDuff.Mode.CLEAR);
drawAllCircles(canvas); // 绘制三个圆
unlockCanvasAndPost(canvas);
}
// 3.4 帧率控制
long cost = System.currentTimeMillis() - currentTime;
if (cost < 16) {
Thread.sleep(16 - cost);
}
}
}
}
// 4. 生命周期清理
@Override
publicvoidonSurfaceTextureDestroyed(SurfaceTexture surface){
isAnimRunning = false;
renderThread.join(1000); // 等待线程结束
returntrue;
}
}1. 线程使用情况
仍然是在子线程运行,不会阻塞UI线程:
2. 刷帧的耗时
根据日志可以确认,每帧的绘制时间在10ms以内,不影响上屏:
2026-02-1217:36:07.69125937-25978 ApertureView com.ne.shareelement D 帧总耗时: 8ms <840>
2026-02-1217:36:07.69125937-25978 ApertureView com.ne.shareelement D ├─ 动画计算: 1ms <841>
2026-02-1217:36:07.69125937-25978 ApertureView com.ne.shareelement D ├─ 渲染绘制: 7ms <842>
2026-02-1217:36:07.69125937-25978 ApertureView com.ne.shareelement D └─ 帧间隔: 16.0ms <843>
2026-02-1217:36:07.70325937-25978 ApertureView com.ne.shareelement D 帧总耗时: 10ms <888>
2026-02-1217:36:07.70325937-25978 ApertureView com.ne.shareelement D ├─ 动画计算: 0ms <889>
2026-02-1217:36:07.70325937-25978 ApertureView com.ne.shareelement D ├─ 渲染绘制: 10ms <890>
2026-02-1217:36:07.70325937-25978 ApertureView com.ne.shareelement D └─ 帧间隔: 16.0ms <891>3. 效果展示
如果想在该图层上面添加交互,也是可以的。
动效方案的选择核心是「匹配场景需求 + 平衡性能 / 开发成本」—— 前文通过底层原理拆解和实战验证,已明确各方案的性能瓶颈与适用边界,本节将从选择原则、场景化选型、决策流程、避坑细则 四个维度,给出可直接落地的团队选型规范。
选型需围绕「性能优先、成本次之、体验兜底」的核心逻辑,优先满足以下原则(按优先级从高到低):
基于前文验证的性能数据和瓶颈,对各方案的选型维度做精细化补充(表格为团队落地核心参考):
前文 “三色圆轨迹背景动效” 的选型过程,完全匹配上述原则:
为避免选型混乱,建议团队遵循以下标准化决策流程(可直接嵌入研发规范):
总结一下:
来自 “ ITPUB博客 ” ,链接:https://blog.itpub.net/69912579/viewspace-3173532/ ,如需转载,请注明出处,否则将追究法律责任。