【iOS】简单重构了SVGAPlayer

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

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

起因:最近有个需求,需要动态修改某个动画的莫非区间,也就是一时要播整个动画,一时只播某个范围。虽然说SVGAPlayerstartAnimationWithRange: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;

通过以上方法即可修改播放区域,可在动画过程中修改:

此处需要说明一下,startFrameendFrame绝对 帧数值,而且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

相关推荐
晴空了无痕12 小时前
游戏客户端架构设计与实战:从模块化到性能优化
游戏·性能优化
谢尔登12 小时前
【React】React 性能优化
前端·react.js·性能优化
我爱松子鱼13 小时前
mysql之InnoDB Buffer Pool 深度解析与性能优化
android·mysql·性能优化
weixin_4258782321 小时前
Redis复制性能优化利器:深入解析replica-lazy-flush参数
数据库·redis·性能优化
奔跑吧邓邓子1 天前
【Python爬虫(36)】深挖多进程爬虫性能优化:从通信到负载均衡
开发语言·爬虫·python·性能优化·负载均衡·多进程
web135085886351 天前
全面指南:使用JMeter进行性能压测与性能优化(中间件压测、数据库压测、分布式集群压测、调优)
jmeter·中间件·性能优化
程序员远仔1 天前
【Vue.js 和 React.js 的主要区别是什么?】
前端·javascript·css·vue.js·react.js·性能优化·html5
哈里哈气2 天前
某手sig3-ios算法 Chomper黑盒调用
objective-c·ios逆向·frida·chomper
敢嗣先锋2 天前
鸿蒙5.0实战案例:基于ArkUI启动冷启动过程最大连续丢帧数问题分析思路&案例
性能优化·移动开发·多线程·harmonyos·arkui·鸿蒙开发
小塵2 天前
【MySQL 优化】什么是回表?什么是索引覆盖?
后端·mysql·性能优化