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方法就行啦。

相关推荐
程序员爱技术1 小时前
Vue 2 + JavaScript + vue-count-to 集成案例
前端·javascript·vue.js
并不会2 小时前
常见 CSS 选择器用法
前端·css·学习·html·前端开发·css选择器
衣乌安、2 小时前
【CSS】居中样式
前端·css·css3
兔老大的胡萝卜2 小时前
ppk谈JavaScript,悟透JavaScript,精通CSS高级Web,JavaScript DOM编程艺术,高性能JavaScript pdf
前端·javascript
低代码布道师2 小时前
CSS的三个重点
前端·css
耶啵奶膘4 小时前
uniapp-是否删除
linux·前端·uni-app
王哈哈^_^5 小时前
【数据集】【YOLO】【目标检测】交通事故识别数据集 8939 张,YOLO道路事故目标检测实战训练教程!
前端·人工智能·深度学习·yolo·目标检测·计算机视觉·pyqt
cs_dn_Jie6 小时前
钉钉 H5 微应用 手机端调试
前端·javascript·vue.js·vue·钉钉
开心工作室_kaic6 小时前
ssm068海鲜自助餐厅系统+vue(论文+源码)_kaic
前端·javascript·vue.js
有梦想的刺儿7 小时前
webWorker基本用法
前端·javascript·vue.js