欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页

如何实现一个不占内存的帧动画

程序员文章站 2024-03-26 08:17:08
...

我分到另一个项目里的时候,发现首页有一个比较炫酷的动画,比较好奇是怎么实现,当时同事和我说用的是airbnb的一个库:Lottile,iOS和安卓都有,甚至RN版本都有。
大致就是ui生成一个json文件,客户端只要导入就能用了,该库自己会完成json文件渲染成动画的过程。

后来有一期,因为需要实现组件化,资源只能自己打包成bundle,导致图怎么都渲染不出来。只好临时弃用,换成了帧动画实现。一共600多帧的动画,不仅图片资源占的空间很大,而且运行时内存可以到290M,不用想就异常可怕了。并且安卓每个app分到的资源就只有120M。

思考能否自己生成一个GIF文件播放GIF是否可行。
做过视频的朋友应该都知道视频有关键帧和补偿帧的概念,而GIF的原理和视频一样,有关键帧和补偿帧,简单说只有第一张图是完整的,后面的只保存与前图不同的部分。所以GIF体积很小,效果却很好。

直接导入GIF肯定不可行,不同屏幕下的表现并不理想。

我就想到YYkit里有播放GIF的代码,那是否有生成GIF的办法。一看还真有。
YYFrameImage,其实也不是生成了gif。它是将图片组成一个数组而已。
性能优化的亮点还是在YYAnimationview里。
下面看了YYimage 再来说一说为什么YYimage播放帧动画会不占内存。

主要是代码里三点。

    for (int i = 0; i < max; i++, idx++) {
        @autoreleasepool {
            if (idx >= total) idx = 0;
            if ([self isCancelled]) break;
            __strong YYAnimatedImageView *view = _view;
            if (!view) break;
            LOCK_VIEW(BOOL miss = (view->_buffer[@(idx)] == nil));
            
            if (miss) {
                UIImage *img = [_curImage animatedImageFrameAtIndex:idx];
                img = img.yy_imageByDecoded;
                if ([self isCancelled]) break;
                LOCK_VIEW(view->_buffer[@(idx)] = img ? img : [NSNull null]);
                view = nil;
            }
        }
    }

自动释放池就不说了,他每一次播放一张,就释放一张,这个安卓的同事后来也做了,内存降到了30m左右。

// dynamically adjust buffer size for current memory.
- (void)calcMaxBufferCount {
    int64_t bytes = (int64_t)_curAnimatedImage.animatedImageBytesPerFrame;
    if (bytes == 0) bytes = 1024;
    
    int64_t total = _YYDeviceMemoryTotal();
    int64_t free = _YYDeviceMemoryFree();
    int64_t max = MIN(total * 0.2, free * 0.6);
    max = MAX(max, BUFFER_SIZE);
    if (_maxBufferSize) max = max > _maxBufferSize ? _maxBufferSize : max;
    double maxBufferCount = (double)max / (double)bytes;
    if (maxBufferCount < 1) maxBufferCount = 1;
    else if (maxBufferCount > 512) maxBufferCount = 512;
    _maxBufferCount = maxBufferCount;
}

动态管理了内存,避免了峰值。

- (UIImage *)animatedImageFrameAtIndex:(NSUInteger)index {
    if (_imagePaths) {
        if (index >= _imagePaths.count) return nil;
        NSString *path = _imagePaths[index];
        CGFloat scale = _NSStringPathScale(path);
        NSData *data = [NSData dataWithContentsOfFile:path];
        return [[UIImage imageWithData:data scale:scale] yy_imageByDecoded];
    } else if (_imageDatas) {
        if (index >= _imageDatas.count) return nil;
        NSData *data = _imageDatas[index];
        return [[UIImage imageWithData:data scale:[UIScreen mainScreen].scale] yy_imageByDecoded];
    } else {
        return index == 0 ? self : nil;
    }
}

这里也是关键,有人可能会问为什么不直接用uiimage直接转。uiimage生成图片并不是马上生成,都会到GPU上统一渲染,会造成大量的内存占用。

/**
 A proxy used to hold a weak object.
 It can be used to avoid retain cycles, such as the target in NSTimer or CADisplayLink.
 */
@interface _YYImageWeakProxy : NSProxy
@property (nonatomic, weak, readonly) id target;
- (instancetype)initWithTarget:(id)target;
+ (instancetype)proxyWithTarget:(id)target;
@end

这里的nsproxy 避免了循环引用。

最后播放时占有的内存1M都不到,但由于占的资源太多,依然不推荐帧动画的做法,之后会介绍Lottie是如何将Json文件渲染成动画的。