iOS简单优化Lottie-OC版源码内存占用降低50%以上

前言

刚加入了新项目团队不久,团队目前还在用着老旧的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方法就行啦。

相关推荐
2301_80107415几秒前
TypeScript异常处理
前端·javascript·typescript
小阿飞_2 分钟前
报错合计-1
前端
caperxi3 分钟前
前端开发中的防抖与节流
前端·javascript·html
霸气小男3 分钟前
react + antDesign封装图片预览组件(支持多张图片)
前端·react.js
susu10830189114 分钟前
前端css样式覆盖
前端·css
学习路上的小刘6 分钟前
vue h5 蓝牙连接 webBluetooth API
前端·javascript·vue.js
&白帝&6 分钟前
vue3常用的组件间通信
前端·javascript·vue.js
小白小白从不日白17 分钟前
react 组件通讯
前端·react.js
Redstone Monstrosity34 分钟前
字节二面
前端·面试
东方翱翔41 分钟前
CSS的三种基本选择器
前端·css