前言
刚加入了新项目团队不久,团队目前还在用着老旧的Lottie-2.5.3的版本(最后一个OC的版本,之后的版本已经切换到了Swift,当然本次发现问题和OC或者Swift版本关系不大)在一次正常的版本开发过程中,偶然发现了在一个首页自动切换播放声音卡片场景下,放着不动的情况下,内存的占用会不停的增加直至OOM。最后问题定位到是因为Lottie源码的原因导致的,下面是代码分析。
代码分析
经过一系列漫长的分析,最终发现是Lottie源码LOTLayerContainer类里面关于图片解码这块的问题导致的。下面是没修改前Lottie对图片解码的源码;
ini
UIImage *image;
if ([asset.imageName hasPrefix:@"data:"]) {
// Contents look like a data: URL. Ignore asset.imageDirectory and simply load the image directly.
NSURL *imageUrl = [NSURL URLWithString:asset.imageName];
NSData *imageData = [NSData dataWithContentsOfURL:imageUrl];
image = [UIImage imageWithData:imageData];
} else if (asset.rootDirectory.length > 0) {
NSString *rootDirectory = asset.rootDirectory;
if (asset.imageDirectory.length > 0) {
rootDirectory = [rootDirectory stringByAppendingPathComponent:asset.imageDirectory];
}
NSString *imagePath = [rootDirectory stringByAppendingPathComponent:asset.imageName];
id<LOTImageCache> imageCache = [LOTCacheProvider imageCache];
if (imageCache) {
image = [imageCache imageForKey:imagePath];
if (!image) {
image = [UIImage imageWithContentsOfFile:imagePath];
[imageCache setImage:image forKey:imagePath];
}
} else {
image = [UIImage imageWithContentsOfFile:imagePath];
}
} else {
NSString *imagePath = [asset.assetBundle pathForResource:asset.imageName ofType:nil];
image = [UIImage imageWithContentsOfFile:imagePath];
}
这里主要有两个问题:
1、Lottie虽然有LOTImageCache这个接口类去处理图片的缓存,但是默认是没有实现的,也就是说默认是没有缓存的,而且对于正常使用者来说很难发现它有缓存设计,并且去实现它。PS:Swift版本之后的Lottie直接在init方法里面就支持CustomImageProvider,估计就是考虑到了这个问题。后面实现了ImageCache接口后内存直接降低30%
2、图片的解码直接使用了[UIImage imageWithContentsOfFile:imagePath]这个系统方法。实话说这个系统方法效率着实有点低,一来它是显示到屏幕再解码(不会提前解码BitMap),二来它是全量解码如果图片Data太大,内存占用非常高。所以我们经常看到Lottie播放一个动画瞬间增长100多m内存,然后完毕释放,也是这个原因。
解决方案
既然我们找到原因了,那解决方案就非常简单了。
首先我们实现LOTImageCache的接口,并且在load方法直接设置一个ImageCache。新建一个XXXImageCache类实现这个接口,代码如下:
less
@interface XXXLOTImageCache()<LOTImageCache>
@property (nonatomic, strong) NSCache *imageCache;
@end
@implementation XXXLOTImageCache
+ (void)load {
[LOTCacheProvider setImageCache:[[XXXLOTImageCache alloc] init]];
}
- (instancetype)init {
self = [super init];
if (self) {
_imageCache = [[NSCache alloc] init];
CGFloat memoryGBUnit = ceil([OPSDevice memoryTotal] / 1024.0);
if (memoryGBUnit > 6) {
memoryGBUnit = 6;
}
//50 per GB Memory
_imageCache.countLimit = 50 * memoryGBUnit;
//Unit 50 mb per GB Memory
_imageCache.totalCostLimit = 1024 * 1024 * (50 * memoryGBUnit);
[[NSNotificationCenter defaultCenter] addObserver:self
selector:@selector(didReceiveMemoryWarning:)
name:UIApplicationDidReceiveMemoryWarningNotification
object:nil];
}
return self;
}
- (void)didReceiveMemoryWarning:(NSNotification *)notification {
[self.imageCache removeAllObjects];
}
- (LOTImage *)imageForKey:(NSString *)key {
return [self.imageCache objectForKey:key];
}
- (void)setImage:(LOTImage *)image forKey:(NSString *)key {
[self.imageCache setObject:image forKey:key];
}
第二点是自实现图片解码替换系统方法,这里主要采用ImageIO的增量解码方案,以及提前加载BitMap,主要参考了SDWebImageCache的解码思路,效果还是挺明显的,内存继续下降了20%以上。代码如下,直接替换UIImage的系统方法即可
ini
+ (UIImage *)decodeImageWithPath:(NSString *)imagePath {
if (![[NSFileManager defaultManager] fileExistsAtPath:imagePath]) {
return nil;
}
@autoreleasepool {
NSData *imageData = [NSData dataWithContentsOfFile:imagePath];
CGImageSourceRef imageSource = CGImageSourceCreateIncremental(NULL);
CGImageSourceUpdateData(imageSource, (__bridge CFDataRef)imageData, YES);
CFDictionaryRef properties = CGImageSourceCopyPropertiesAtIndex(imageSource, 0, NULL);
size_t _width = 0;
size_t _height = 0;
if (properties) {
CFTypeRef val = CFDictionaryGetValue(properties, kCGImagePropertyPixelHeight);
if (val) CFNumberGetValue(val, kCFNumberLongType, &_height);
val = CFDictionaryGetValue(properties, kCGImagePropertyPixelWidth);
if (val) CFNumberGetValue(val, kCFNumberLongType, &_width);
CFRelease(properties);
}
CGImageRef decompressedImageRef = CGImageSourceCreateImageAtIndex(imageSource, 0, NULL);
if ((_width + _height) == 0) {
return nil;
}
UIImage *compressedImage = [UIImage imageWithCGImage:decompressedImageRef];
CGImageRelease(decompressedImageRef);
CFRelease(imageSource);
imageSource = NULL;
compressedImage = [LOTLayerContainer decompressedImageWithImage:compressedImage];
return compressedImage;
}
}
+ (nullable UIImage *)decompressedImageWithImage:(nullable UIImage *)image {
if (!image) {
return image;
}
CGImageRef imageRef = image.CGImage;
// device color space
CGColorSpaceRef colorspaceRef = OPRCGColorSpaceGetDeviceRGB();
BOOL hasAlpha = OPRCGImageRefContainsAlpha(imageRef);
// iOS display alpha info (BRGA8888/BGRX8888)
CGBitmapInfo bitmapInfo = kCGBitmapByteOrder32Host;
bitmapInfo |= hasAlpha ? kCGImageAlphaPremultipliedFirst : kCGImageAlphaNoneSkipFirst;
size_t width = CGImageGetWidth(imageRef);
size_t height = CGImageGetHeight(imageRef);
CGContextRef context = CGBitmapContextCreate(NULL,
width,
height,
8,
0,
colorspaceRef,
bitmapInfo);
if (context == NULL) {
return image;
}
// Draw the image into the context and retrieve the new bitmap image without alpha
CGContextDrawImage(context, CGRectMake(0, 0, width, height), imageRef);
CGImageRef imageRefWithoutAlpha = CGBitmapContextCreateImage(context);
UIImage *imageWithoutAlpha = [[UIImage alloc] initWithCGImage:imageRefWithoutAlpha scale:image.scale orientation:image.imageOrientation];
CGContextRelease(context);
CGImageRelease(imageRefWithoutAlpha);
return imageWithoutAlpha;
}
结果
优化前后对比,内存占用不管是启动还是进房间的场景,直接下降了50%以上,效果还是非常明显的。
结语
以上就是这次优化Lottie实现的整个过程,如果你也是用的OC版的Lottie赶紧行动起来吧,把源码拖到项目里面,简单改改我上面说到的点就能达到效果。如果你用的是Swift版本,那么也可以使用上面的思路,自实现一个Custom的ImageProvider,然后封装一个通用的Lottie方法就行啦。