如何实现一个不占内存的帧动画
我分到另一个项目里的时候,发现首页有一个比较炫酷的动画,比较好奇是怎么实现,当时同事和我说用的是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文件渲染成动画的。
上一篇: Java方法定义