SVGAPlayer优化
SVGAPlayer
什么是 SVGA?
SVGA 是一种跨平台的开源动画格式,同时兼容 iOS / Android / Web。SVGA 除了使用简单,性能卓越,同时让动画开发分工明确,各自专注各自的领域,大大减少动画交互的沟通成本,提升开发效率。动画设计师专注动画设计,通过工具输出 svga 动画文件,提供给开发工程师在集成 svga player 之后直接使用。
SVGA 原理
SVGA 使用
- 通过 SVGAParser 从 asset/file/http 等解析 svga 文件,解析完后得到一个 SVGAVideoEntity
- new 一个 SVGADrawable,将 SVGAVideoEntity 传入
- 将 SVGADrawable 设置给 SVGAImageView
- 最后 SVGAImageView 调用 startAnimation 开启动画
SVGA 格式
svga 格式用的是 protobuffer 封装的,
- SVGAParser 会把 SVGA 文件解析成 MovieEntity,解析完 onComplete 回调后会封装成 SVGAVideoEntity 给业务调用方
- 业务调用方的 SVGAImageView 会设置一个 SVGADrawable,在 SVGADrawable 中会持有 SVGAVideoEntity
- SVGADrawble 交给 SVGACanvasDrawer 去绘制
- SVGAImageView 中 startAnimation,通过属性动画,不停的绘制对应的帧
SVGA 帧播放
SVGA 是如何按照既定的帧率去播放的?
ValueAnimator
Ref
https://jfson.github.io/2018/06/21/49-svga/
https://svga.io/article.html
https://www.yuque.com/u1451991/izlc0x/tu4rph?#nxn60
SVGAPlayer 缺陷(基于 v2.5.3 版本)
线程相关问题
多余的线程解析
问题:
在 SVGAImageView 中配置了 source
属性,会直接新建一个 Thread 来进行 parse,parse 完后会进行动画播放
1
2
3
4
val parser = SVGAParser(context)
Thread {
parser.parse(it, callback)
}.start()
解决:去掉了 SVGAImageView 每次解析资源都会开启的 Thread:SVGAParser 内部本身就有线程池了,没必要在外部开启(分析后仅发现 open asset 可能会耗时,已经放到了 SVGAParser 内部线程池中执行
1
2
3
4
5
6
7
8
9
private fun parserSource(source: String) {
val refImgView = WeakReference<SVGAImageView>(this)
val parser = SVGAParser(context)
if (source.startsWith("http://") || source.startsWith("https://")) {
parser.decodeFromURL(URL(source), createParseCompletion(refImgView))
} else {
parser.decodeFromAssets(source, createParseCompletion(refImgView))
}
}
SVGAParser 线程池设计
1
2
3
4
5
6
7
8
9
// v2.5.3
threadPoolExecutor = Executors.newCachedThreadPool()
// v2.6.1改进
private val threadNum = AtomicInteger(0)
internal var threadPoolExecutor = Executors.newCachedThreadPool { r ->
Thread(r, "SVGAParser-Thread-${threadNum.getAndIncrement()}")
}
SVGAParser 现有线程池配置:算是合理的
SVGAParser 设计用的是 newCachedThreadPool
,newCachedThreadPool 是应对快速解析资源的,任何解析的任务都会立即得到响应,如果采用固定线程数的配置肯定是要有缓冲队列,让任务排队等待处理得不到及时响应。在这种角度看 newCachedThreadPool 是合理的。newCachedThreadPool
这种配置只有超密集的创建成百上千的线程时,才会有 OOM 的可能性,但是这对于 App 来说这是极其少见且不正常的。
对此我认为:不用担心 newCachedThreadPool 潜在问题,这在 SVGA 这种场景下几乎不可能发生,而且最终创建的线程可能就那几个,不会太大规模。如果你的场景对实时性要求不高或者有着自己的考虑,可以自行通过 SVGAParser.setThreadPoolExecutor() 实现自己的线程池。
改进:
给线程池中的每个线程命名
存在的内存泄漏问题
ParseCompletion
造成的泄漏
- SVGAImageView#loadAttrs 中使用的匿名内部类
SVGAParser.ParseCompletion
会导致持有 SVGAImageView 的强引用,造成内存泄漏。
解决:WeakReference 引用 SVGAImageView
1
2
3
4
5
6
7
8
private fun createParseCompletion(ref: WeakReference<SVGAImageView>): SVGAParser.ParseCompletion {
return object : SVGAParser.ParseCompletion {
override fun onComplete(videoItem: SVGAVideoEntity) {
ref.get()?.startAnimation(videoItem)
}
override fun onError() {}
}
}
ValueAnimator 的 AnimatorListener 造成的泄漏
- 在 SVGAImageView 中配置了
source
的 xml 属性会自动解析 svga 播放,而其中的 Listener 是一个匿名内部类,持有了 SVGAImageView,导致了泄漏
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
private fun loadAttrs(attrs: AttributeSet) {
val typedArray = context.theme.obtainStyledAttributes(attrs, R.styleable.SVGAImageView, 0, 0)
// ...
typedArray.getString(R.styleable.SVGAImageView_source)?.let {
val parser = SVGAParser(context)
Thread {
val callback: SVGAParser.ParseCompletion = object : SVGAParser.ParseCompletion {
override fun onComplete(videoItem: SVGAVideoEntity) {
this@SVGAImageView.post {
videoItem.antiAlias = antiAlias
setVideoItem(videoItem)
(drawable as? SVGADrawable)?.scaleType = scaleType
if (autoPlay) {
startAnimation()
}
}
}
override fun onError() {}
}
if(it.startsWith("http://") || it.startsWith("https://")) {
parser.parse(URL(it), callback)
} else {
parser.parse(it, callback)
}
}.start()
}
typedArray.recycle()
}
- 手动调用 SVGAImageView#startAnimation(SVGARange?, Boolean) 内的 AnimatorListener 也会有内存泄露
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
fun startAnimation(range: SVGARange?, reverse: Boolean = false) {
animator.addUpdateListener { // 匿名内部类
// ...
}
animator.addListener(object : Animator.AnimatorListener { // 匿名内部类
// ...
override fun onAnimationEnd(animation: Animator?) {
// ...
}
// ...
})
}
override fun onDetachedFromWindow() {
super.onDetachedFromWindow()
clearAudio()
animator?.cancel()
animator?.removeAllListeners()
animator?.removeAllUpdateListeners()
}
原因: onDetachedFromWindow 方法不一定每次都会执行(Android泄漏模式:View中的订阅)故 onDetachedFromWindow 中的解绑逻辑失效(还未 attach 的 View 不会执行 detach 操作)
匿名内部类 AnimatorListener/AnimatorUpdateListener 会持有外部类 SVGAImageView 造成内存泄漏
解决: AnimatorListener/AnimatorUpdateListener 中引用的 SVGAImageView 用一个 WeakReference 包裹
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
private class AnimatorListener(view: SVGAImageView) : Animator.AnimatorListener {
private val weakReference = WeakReference<SVGAImageView>(view)
override fun onAnimationRepeat(animation: Animator?) {
weakReference.get()?.callback?.onRepeat()
}
override fun onAnimationEnd(animation: Animator?) {
weakReference.get()?.onAnimationEnd(animation)
}
override fun onAnimationCancel(animation: Animator?) {
weakReference.get()?.isAnimating = false
}
override fun onAnimationStart(animation: Animator?) {
weakReference.get()?.isAnimating = true
}
}
private class AnimatorUpdateListener(view: SVGAImageView) : ValueAnimator.AnimatorUpdateListener {
private val weakReference = WeakReference<SVGAImageView>(view)
override fun onAnimationUpdate(animation: ValueAnimator?) {
weakReference.get()?.onAnimatorUpdate(animation)
}
}
SVGAParser context 泄漏
1
2
3
4
5
6
7
8
9
10
11
class SVGAParser(private var mContext: Context?) {
companion object {
private var mShareParser = SVGAParser(null)
fun shareParser(): SVGAParser {
return mShareParser
}
}
fun init(context: Context) {
mContext = context
}
}
原因:
SVGAParser 是个静态实例,持有了一个 context,如果传入的是一个 Activity,可能导致内存泄漏
解决: 用 context.applicationContext
1
2
3
class SVGAParser(context: Context?) {
private var mContext = context?.applicationContext
}
内存设计缺陷问题
bitmap 未 recycle?
Bitmap 没有做 recycle,只是简单的把缓存 list 清理掉
bitmap 未及时释放
App 对内存的消耗很大经常 OOM。最终定位到 SVGAVideoEntity
在 SVGA 播放播放完成后,SVGAImageView 永远会持有最后一个 SVGADrawable 的引用,而这个 SVGADrawable 持有包含大量 Bitmap 集合的 SVGAVideoEntity ,这样一来就导致了一块很大的内存空间不会被 GC ,在 App 内存紧张或者多个 SVGAImageView 同时播放动画的时候很容易 OOM。
SVGAImageView 引用关系:
SVGAImageView 里面持有 SVGADrawable,SVGADrawable 持有了 SVGAVideoEntity
1
2
3
4
5
6
7
8
9
10
11
open class SVGAImageView : ImageView {
private var mVideoItem: SVGAVideoEntity? = null
private var animator: ValueAnimator? = null
fun setVideoItem(videoItem: SVGAVideoEntity?, dynamicItem: SVGADynamicEntity?) {
// ...
val drawable = SVGADrawable(videoItem, dynamicItem ?: SVGADynamicEntity())
drawable.cleared = clearsAfterStop
setImageDrawable(drawable)
this.mVideoItem = videoItem
}
}
SVGAVideoEntity 中有个 HashMap 保存了大量 Bitmap
1
2
3
class SVGAVideoEntity {
internal var images = HashMap<String, Bitmap>()
}
解决:
Bitmap 等资源的清理工作:在 SVGA 播放播放完成后,onDetachedFromWindow 时,清理 Drawable、Bitmap 的强引用,达到可 GC 的目的,这一步效果很明显。
具体 v2.6.1 的源码:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
override fun onDetachedFromWindow() {
super.onDetachedFromWindow()
stopAnimation(clearsAfterDetached)
if (clearsAfterDetached) {
clear()
}
}
fun clear() {
getSVGADrawable()?.cleared = true
getSVGADrawable()?.clear()
// 清除对 drawable 的引用
setImageDrawable(null)
}
// SVGADrawable
fun clear() {
videoItem.audioList.forEach { audio ->
audio.playID?.let {
if (SVGASoundManager.isInit()){
SVGASoundManager.stop(it)
}else{
videoItem.soundPool?.stop(it)
}
}
audio.playID = null
}
videoItem.clear()
}
// SVGAVideoEntity
internal var imageMap = HashMap<String, Bitmap>()
fun clear() {
if (SVGASoundManager.isInit()) {
this.audioList.forEach {
it.soundID?.let { id -> SVGASoundManager.unload(id) }
}
soundCallback = null
}
soundPool?.release()
soundPool = null
audioList = emptyList()
spriteList = emptyList()
imageMap.clear()
}
bitmap 的采样问题
- 创建 Btimap 时根据 View Size 计算采样率,减少内存消耗。
在 finalize 中清理资源
去掉 finalize() 方法
去掉了 SVGAVideoEntity 对于 finalize 的实现,finalize 特不稳定,已经改为 clear() 方法,在合适时机手动清理。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
protected fun finalize() {
this.soundPool?.release()
this.soundPool = null
this.images.clear()
}
// v2.6.1改成
fun clear() {
if (SVGASoundManager.isInit()) {
this.audioList.forEach {
it.soundID?.let { id -> SVGASoundManager.unload(id) }
}
soundCallback = null
}
soundPool?.release()
soundPool = null
audioList = emptyList()
spriteList = emptyList()
imageMap.clear()
}
为什么不用 finalize() 方法?
其他
其他
验证后的数据
用了一个比较容易出现问题的低端测试机,抓了一下内存情况:
可以看到在一个大 SVGA 动画播放完毕后,内存是可以被回收的。
锯齿一样的内存情况不要慌,没有办法 App 里面播放的动效太多了,这也是低端机正常 GC 的表现