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

我分到另一个项目里的时候,发现首页有一个比较炫酷的动画,比较好奇是怎么实现,当时同事和我说用的是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文件渲染成动画的。

你可能感兴趣的:(如何实现一个不占内存的帧动画)