目前项目是用SVGA做动画,现在使用的是SVGAPlayer这个库。由于该库很久没有更新,有些需求很难去满足一些场景,因此基于源码上做了部分优化。
Demo下载:SVGAPlayer_OptimizedDemo (Readme还没更新,等代码注释写好后再同步)
- 核心代码:
SVGARePlayer(.h .m)
、SVGAVideoEntity+Extension(.h .m)
- 依赖:SVGAPlayer(基于
SVGAPlayer
重构的) - 加强版:
SVGAExPlayer.swift
,具体介绍在【iOS】SVGAParsePlayer - 便捷SVGA播放器,文章还没更新,等代码注释写好后再同步。
起因:最近有个需求,需要动态修改某个动画的莫非区间,也就是一时要播整个动画,一时只播某个范围。虽然说SVGAPlayer
有startAnimationWithRange:reverse:
这个方法可以控制播放区域,但是我觉得使用起来比较麻烦,如下:
swift
class ViewController: UIViewController {
let player = SVGAPlayer()
override func viewDidLoad() {
super.viewDidLoad()
player.frame = CGRect(x: 100, y: 100, width: 100, height: 100)
player.loops = 1
player.clearsAfterStop = false
player.delegate = self
view.addSubview(player)
SVGAParser().parse(withNamed: "animation_file", in: nil) { [weak self] videoItem in
guard let self else { return }
self.player.videoItem = videoItem
self.player.startAnimation()
}
}
}
extension ViewController: SVGAPlayerDelegate {
func svgaPlayerDidFinishedAnimation(_ player: SVGAPlayer!) {
if xxx {
// 某些条件下只播放 90~120帧
self.player.startAnimation(with: NSRange(location: 90, length: 120), reverse: false)
} else {
// 某些条件下要完整播放 0~120帧
self.player.startAnimation(with: NSRange(location: 0, length: 120), reverse: false)
}
}
}
看上去代码不多,首先loops要设置为1,其次clearsAfterStop要设置为false,最后监听回调来切换,这些都得看源码和调试多次才可以做到无缝切换播放区域。我还是觉得比较麻烦的,主要是不能以比较便捷的方式去动态修改ta的播放区域。
而且看了源码后,我感觉内部逻辑比较乱,感觉有优化的空间,由于项目多处使用,为了提升一下日后的扩展性,决定基于SVGAPlayer
重构一个新的SVGA播放器。
SVGARePlayer
SVGARePlayer
就是基于SVGAPlayer
重构一个新的SVGA播放器。起初是完全拷贝了SVGAPlayer
的代码,然后在此基础上进行重构,同样也是用Objective-C写的,外部接口基本跟SVGAPlayer
保持一致,而内部则是基本按我的风格进行修整 、删减 、加强 、封装 后的一个全新的播放器,这是为了能方便逐步替换项目中原来的SVGAPlayer
才这么设计。

优化
具体优化了什么呢?其实也没优化啥,主要有两个:
一、同步了渲染过程,防止重复构建
在原来的代码中有这样的写法:
objc
- (void)setVideoItem:(SVGAVideoEntity *)videoItem {
...省略...
[[NSOperationQueue mainQueue] addOperationWithBlock:^{
[self clear];
[self draw];
}];
}
- (void)startAnimation {
...省略...
if (self.videoItem == nil) {
...省略...
} else if (self.drawLayer == nil) {
self.videoItem = _videoItem;
}
...省略...
}
当我们使用时,一般都会这么干:
objc
player.videoItem = videoItem;
[player startAnimation];
这样在第一次播放时,大概率会重复调用了两次draw
方法(因为-setVideoItem:
方法中使用了异步),如果新打开的页面有多个SVGA并且都是比较复杂的动画,重复的draw
或许会对GPU和CPU有比较明显的负担(卡顿)。
因此我对此加了线程保护,确保不会重复draw
:
objc
#import <pthread.h>
static inline void _jp_dispatch_sync_on_main_queue(void (^block)(void)) {
if (pthread_main_np()) {
block();
} else {
dispatch_sync(dispatch_get_main_queue(), block);
}
}
- (void)setVideoItem:(SVGAVideoEntity *)videoItem {
...省略...
_jp_dispatch_sync_on_main_queue(^{
[self clear];
[self draw];
});
}
从作者的写法上应该是为了在子线程中也能设置这个videoItem
,尽量保持原来的逻辑,同步到主线程进行绘制。
二、防止CADisplayLink的内存泄露
使用CADisplayLink
一般都会使用NSProxy
然后转发给self
去执行对应方法,这是为了防止循环引用 。作者没有用不知道是不是为了性能的问题,为了安全我还是加上了,另外使用preferredFramesPerSecond
替换了frameInterval
的设置:
objc
- (void)__addLink {
[self __removeLink];
self.displayLink = [CADisplayLink displayLinkWithTarget:[_JPProxy proxyWithTarget:self] selector:@selector(__linkHandle)];
self.displayLink.preferredFramesPerSecond = self.videoItem.FPS;
[self.displayLink addToRunLoop:[NSRunLoop mainRunLoop] forMode:self.mainRunLoopMode];
}
当前项目没有发生内存泄露那是因为源码中重写了-willMoveToSuperview:
方法,在这里判断了父视图为空就移除定时器,不过这种是建立在播放器必须有被放到父视图上才能释放,万一有人没有把播放器add
到父视图上就播放了动画,那就会造成内存泄露了。
优化最主要的也就这两点,当然SVGARePlayer
可不仅只做了这些小优化,最主要是扩展了新的功能。
扩展
一、静音
SVGA动画可是有音频的,但SVGAPlayer居然不提供静音的功能,这里我补上了:
objc
/// 是否静音
@property (nonatomic, assign) BOOL isMute;
具体实现是将其内部的音频播放器的音量设置成0,现在可以随时打开/关闭静音了。
二、反转播放
这是原本就有的功能,不过只能通过-startAnimationWithRange:reverse:
方法去设置,不是不行,只是调用该方法每次都会重置loopCount
,如果设置了loops
,那么调用这方法(还有-startAnimation
也一样)都会重置次数的统计,对于某些需要计数的地方就不太友好。
对此我内部进行了重新设计,改用属性去进行反转:
objc
/// 是否反转播放
@property (nonatomic, assign) BOOL isReversing;

使用起来方便多了,并且不会重置完成次数(我另外提供了-resetLoopCount
方法专门去重置)。
三、动态播放区间
这个也是原本的功能,同样只能通过-startAnimationWithRange:reverse:
方法去设置,源码内部使用currentRange
来管理的(不知道为啥不公开),仅仅通过这方法去设置我觉得太麻烦了,而且是通过NSRange
设置起始帧和长度,不好把控。
我改成了使用准确的帧数(下标)起始帧数 startFrame 和 结束帧数 endFrame 来设置播放区间,为了防止越界的情况出现,我内部做了防护,而且外部不同单一设置这两个值,得通过我的方法去同时设置:
objc
#pragma mark 更换SVGA资源+设置播放区间
- (void)setVideoItem:(nullable SVGAVideoEntity *)videoItem
currentFrame:(NSInteger)currentFrame;
- (void)setVideoItem:(nullable SVGAVideoEntity *)videoItem
startFrame:(NSInteger)startFrame
endFrame:(NSInteger)endFrame;
- (void)setVideoItem:(nullable SVGAVideoEntity *)videoItem
startFrame:(NSInteger)startFrame
endFrame:(NSInteger)endFrame
currentFrame:(NSInteger)currentFrame;
#pragma mark 设置播放区间
/// 重置起始帧数为最小帧数(0),结束帧数为最大帧数(videoItem.frames)
- (void)resetStartFrameAndEndFrame;
/// 设置起始帧数,结束帧数为最大帧数(videoItem.frames)
- (void)setStartFrameUntilTheEnd:(NSInteger)startFrame;
/// 设置结束帧数,起始帧数为最小帧数(0)
- (void)setEndFrameFromBeginning:(NSInteger)endFrame;
- (void)setStartFrame:(NSInteger)startFrame endFrame:(NSInteger)endFrame;
- (void)setStartFrame:(NSInteger)startFrame endFrame:(NSInteger)endFrame currentFrame:(NSInteger)currentFrame;
通过以上方法即可修改播放区域,可在动画过程中修改:

此处需要说明一下,startFrame
和endFrame
是绝对 帧数值,而且startFrame <= endFrame
。
也就是说,正常播放时是startFrame ~> endFrame
的过程,而反转播放(isReversing
为YES)则是endFrame ~> startFrame
的过程。
为了不被搞混,提供了这两个计算属性方便使用:
objc
/// 头部帧数 = isReversing ? endFrame : startFrame
@property (readonly) NSInteger leadingFrame;
/// 尾部帧数 = isReversing ? startFrame : endFrame
@property (readonly) NSInteger trailingFrame;
四、其他
最主要的是以上三点,其他的就是一些比较琐碎的,例如提供了停止播放后的场景选择:
objc
typedef NS_ENUM(NSUInteger, SVGARePlayerStoppedScene) {
/// 停止后清空图层
SVGARePlayerStoppedScene_ClearLayers = 0,
/// 停止后留在最后
SVGARePlayerStoppedScene_StepToTrailing = 1,
/// 停止后回到开头
SVGARePlayerStoppedScene_StepToLeading = 2,
};
/// 设置属性控制:主动调用`stopAnimation`后的情景
@property (nonatomic, assign) SVGARePlayerStoppedScene userStoppedScene;
/// 设置属性控制:完成所有播放后(需要设置`loops > 0`)的情景
@property (nonatomic, assign) SVGARePlayerStoppedScene finishedAllScene;
/// 也可以调用方法自由控制
- (void)stopAnimation:(SVGARePlayerStoppedScene)scene;
另外还有一些比较常用的属性公开访问,例如:
objc
/// 当前帧数
@property (nonatomic, assign, readonly) NSInteger currentFrame;
/// 当前进度
@property (readonly) float progress;
/// 当前播放次数
@property (nonatomic, assign, readonly) NSInteger loopCount;
以上这些都是新增的特性,剩下的例如代理方法的回调 和素材替换等方法我都有保留,并且有所优化,用法跟SVGAPlayer基本保持一致,这里就不多介绍了。
One more thing...
SVGAExPlayer
SVGAExPlayer是继承于SVGARePlayer
,是ta的加强版,用Swift写的,除了原有功能外,还有以下新特性:
✅ 内置SVGA解析器;
✅ 带有播放状态且可控制;
✅ 可自定义下载器;
✅ 防止重复加载;
✅ 兼容 OC & Swift;
✅ API超级简单易用。
简单介绍一下,原来SVGAPlayer
的使用方式:
swift
let player = SVGAPlayer()
override func viewDidLoad() {
super.viewDidLoad()
...UI初始化...
// 1.创建 SVGA 动画解析器
let parser = SVGAParser()
// 2.加载 SVGA 动画文件
parser.parse(withNamed: "animation_name", in: nil) { [weak self] videoItem in
guard let self, videoItem else { return }
// 3.将 SVGA 动画加载到播放器中
self.player.videoItem = videoItem
// 4.开始播放动画
self.player.startAnimation()
}
}
用SVGAExPlayer
只需要:
swift
player.play("animation_name")
// 内部已经做好了重复加载的防护,只要名称一样,就不会有重复加载的问题,非常适合在可复用的滚动列表中使用。
player.play("animation_name")
player.play("animation_name")
player.play("animation_name")
player.play("animation_name")
player.play("animation_name")
......
其实这个是我之前写的【iOS】SVGAParsePlayer - 便捷SVGA播放器,之前是继承于SVGAPlayer
,现在换成了SVGARePlayer
并且完全适配了,变得更加强大。
- PS:由于最近工作比较忙,所以文章还没更新,等之后把注释都写好后再同步到文章和Github的Readme,现在这里只能先偷个懒。
Demo
-
SVGAPlayer_OptimizedDemo (Readme还没更新,等代码注释写好后再同步)
- 核心代码:
SVGARePlayer(.h .m)
、SVGAVideoEntity+Extension(.h .m)
- 依赖:SVGAPlayer(基于
SVGAPlayer
重构的) - 加强版:
SVGAExPlayer.swift
,具体介绍在【iOS】SVGAParsePlayer - 便捷SVGA播放器,文章还没更新,等代码注释写好后再同步。
- 核心代码:
-
- 一个用于快速预览
Lottie
&SVGA
的Mac小工具。 - 使用了
SVGAExPlayer
。
- 一个用于快速预览