现在项目是用SVGA做动画,现在运用的是SVGAPlayer这个库。由于该库很久没有更新,有些需求很难去满意一些场景,因此根据源码上做了部分优化。

Demo下载:SVGAPlayer_OptimizedDemo (Readme还没更新,等代码注释写好后再同步)

起因:最近有个需求,需求动态修正某个动画的难道区间,也就是一时要播整个动画,一时只播某个范围。虽然说SVGAPlayerstartAnimationWithRange:reverse:这个办法能够操控播映区域,可是我觉得运用起来比较费事,如下:

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才这么设计。

【iOS】简略重构了SVGAPlayer

优化

详细优化了什么呢?其实也没优化啥,主要有两个:

一、同步了烘托进程,避免重复构建

在本来的代码中有这样的写法:

- (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;
    }
    ...省掉...
}

当我们运用时,一般都会这么干:

player.videoItem = videoItem;
[player startAnimation];

这样在第一次播映时,大概率会重复调用了两次draw办法(由于-setVideoItem:办法中运用了异步),假如新翻开的页面有多个SVGA而且都是比较复杂的动画,重复的draw或许会对GPU和CPU有比较显着的担负(卡顿)。

因此我对此加了线程保护,确保不会重复draw

#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的设置:

- (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居然不供给静音的功用,这儿我补上了:

/// 是否静音
@property (nonatomic, assign) BOOL isMute;

详细实现是将其内部的音频播映器的音量设置成0,现在能够随时翻开/封闭静音了。

二、回转播映

这是本来就有的功用,不过只能经过-startAnimationWithRange:reverse:办法去设置,不是不可,只是调用该办法每次都会重置loopCount,假如设置了loops,那么调用这办法(还有-startAnimation也相同)都会重置次数的统计,对于某些需求计数的当地就不太友爱。

对此我内部进行了从头设计,改用特点去进行回转:

/// 是否回转播映
@property (nonatomic, assign) BOOL isReversing;

【iOS】简略重构了SVGAPlayer

运用起来便利多了,而且不会重置完结次数(我另外供给了-resetLoopCount办法专门去重置)。

三、动态播映区间

这个也是本来的功用,相同只能经过-startAnimationWithRange:reverse:办法去设置,源码内部运用currentRange来办理的(不知道为啥不揭露),仅仅经过这办法去设置我觉得太费事了,而且是经过NSRange设置开始帧和长度,不好把控。

我改成了运用准确的帧数(下标)开始帧数 startFrame完毕帧数 endFrame 来设置播映区间,为了避免越界的情况呈现,我内部做了防护,而且外部不同单一设置这两个值,得经过我的办法去一起设置:

#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;

经过以上办法即可修正播映区域,可在动画进程中修正:

【iOS】简略重构了SVGAPlayer

此处需求阐明一下,startFrameendFrame绝对帧数值,而且startFrame <= endFrame

也就是说,正常播映时是startFrame ~> endFrame的进程,而回转播映(isReversing为YES)则是endFrame ~> startFrame的进程。

为了不被搞混,供给了这两个计算特点便利运用:

/// 头部帧数 = isReversing ? endFrame : startFrame
@property (readonly) NSInteger leadingFrame;
/// 尾部帧数 = isReversing ? startFrame : endFrame
@property (readonly) NSInteger trailingFrame;

四、其他

最主要的是以上三点,其他的就是一些比较琐碎的,例如供给了中止播映后的场景选择:

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;

另外还有一些比较常用的特点揭露拜访,例如:

/// 当时帧数
@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的运用方式:

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只需求:

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