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

相关推荐
EnCi Zheng9 小时前
M5-markconv自定义CSS样式指南 [特殊字符]
前端·css·python
kyriewen9 小时前
你的网页慢,用户不说直接走——前端性能监控教你“读心术”
前端·性能优化·监控
广州华水科技9 小时前
北斗GNSS变形监测在大坝安全监测中的应用与优势分析
前端
前端老石人9 小时前
前端开发中的 URL 完全指南
开发语言·前端·javascript·css·html
CAE虚拟与现实9 小时前
五一假期闲来无事,来个前段、后端的说明吧
前端·后端·vtk·three.js·前后端
Sarvartha9 小时前
三目运算符
linux·服务器·前端
晓晨的博客9 小时前
ROS1录制的bag包转换为ROS2格式
前端·chrome
Wect9 小时前
LeetCode 72. 编辑距离:动态规划经典题解
前端·算法·typescript
donecoding9 小时前
别再让 pnpm 跟着 nvm 跑了!独立安装终极指南
前端·node.js·前端工程化
GISer_Jing9 小时前
AI全栈转型_TS后端学习路线
前端·人工智能·后端·学习