iOS image 内存优化

iOS Image 内存优化

结论

在iOS 原生层,对于图片的使用,推荐使用 UIGraphicsImageRenderer API

也需要合理在不同的使用场景,根据场景需要类型使用正确的API ,同时对图片资源做加载和释放管理。

前因

看看上面的这个图,有没有考虑过,iOS 里面一张 4096 * 3072 尺寸的png图片占用多大内存呢 ?答案是惊人的 48M

计算公式是这样的 image width * image height * 4 /1024 / 1024 这里认为 image 属于RGBA8888

问题来了,内存都在哪里 ?

深究一下iOS 内存分配逻辑可以有下面的结论,iOS 内存部分分为三类,即:Data Buffer、Image Buffer、Frame Buffer

Data Buffer 是存储在内存中的原始数据,图像可以使用不同的格式保存,如 jpg、png。是Image 的文件内容。

Image Buffer 是图像在内存中的存在方式,用于存放图像具体素点信息。Image Buffer 的大小和图像的大小成正比。

Frame Buffer 和 Image Buffer 内容相同,不过其存储在 vRAM(video RAM)中,而 Image Buffer 存储在 RAM 中。

解码就是从 Data Buffer 生成 Image Buffer 的过程。Image Buffer 会占用带宽上传到 GPU 成为 Frame Buffer,最后GPU负责使用 Frame Buffer用于更新显示区域。

大致执行流程如上图,先经过载入,加载图像内容到内存成为Data Buffer , 然后就是经过Decode 过程,转化图像为GPU 可用的 Image Buffer ,在需要显示的时候Image Buffer data 会被上传到GPU 中成为Frame Buffer Data 进行相应渲染。

上图飙升的 48M 内存代码如下

1
2
3
4
5
6
7
8
9
10
//原图加载
-(void)test
{
UIImageView *imageView = [[UIImageView alloc]init];
UIImage *image = [UIImage imageNamed:@"bg_teacherLetter"];
imageView.image = image;
imageView.frame = CGRectMake(0, 0, 1367*0.5, 1089*0.5);
[self.view addSubview:imageView];
}

UIImage 是 iOS 中处理图像的高级类。创建一个 UIImage 实例只会加载 Data Buffer,将图像显示到屏幕上才会触发解码,也就是 Data Buffer 解码为 Image Buffer。Image Buffer 也关联在 UIImage 上。

imageNamed 这个常用API 存在一个内存问题,就是载入以后图片会被缓存到系统Cache 里面。 这是一种便捷的设计,比如可以快速在cache 里面查找到图片的缓存,同时也是一个弊端,内存就放在了cache 里面,一部分内存就会被持续占用。如果是比较小,又常用的图片,这么处理比较合适,但是针对于例子中的尺寸来讲,就非常不合适。

对于这个问题,大家通用的解决方案应该是 使用 imageWithContentsOfFile这个 API 来搞,根据苹果的解释是:使用这个方法创建的图片不会缓存于系统缓存内,开发者可在适当的时机对图片进行处理。因而,对于一些比较大的或不常使用的图片,我们应当使用imageWithContentsOfFile:进行创建。

1
2
3
4
5
6
7
8
9
10
11
-(void)withContexOfFile{
UIImageView *imageView = [[UIImageView alloc]init];
UIImage *image = [ViewController imageWithContentName:@"bg_teacherLetter@2x.png"];
imageView.image = image;
imageView.frame = CGRectMake(0, 0, 1367*0.5, 1089*0.5);
[self.view addSubview:imageView];
}
+ (UIImage *)imageWithContentName:(NSString *)name
{
return [UIImage imageWithContentsOfFile:[[NSBundle mainBundle] pathForResource:name ofType:nil]];
}

API 的本身用途是不在cache 里面缓存图片内容,但是图片占用的内存依然很大。

优点 : 图片不使用即释放内存,不存在图片常驻内存
缺点 : 每次使用都需要做IO的操作

适用于使用不频繁的大图加载

有没有更好的方式来降低图片内存 ?

答案,有! 参考 Image and Graphics Best Practices WWDC2018

所以引出这里的重点: UIGraphicsImageRenderer

代码先:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
-(void)resizeTest
{
UIImageView *imageView = [[UIImageView alloc]init];
UIImage *image = [UIImage imageNamed:@"bg_teacherLetter"];
image = [self resiImage:image size:CGSizeMake(1367*0.5, 1089*0.5)];
imageView.image = image;
imageView.frame = CGRectMake(0, 0, 1367*0.5, 1089*0.5);
[self.view addSubview:imageView];
}
- (UIImage*)resiImage:(UIImage *)image size:(CGSize)size{
UIGraphicsImageRenderer *re = [[UIGraphicsImageRenderer alloc]initWithSize:size];
return [re imageWithActions:^(UIGraphicsImageRendererContext * _Nonnull rendererContext) {
[image drawInRect:CGRectMake(0, 0, size.width, size.height)];
}];
}

先给出一组数据对比 图片尺寸同样缩放在 1367 / 2 , 1089 / 2

图1 使用 imageNamed API , 发现 Physical footprint 明显增加了 48M, 内存块儿在 IOSurface

IOSurface 内存块儿增加是因为 图片decode 之后生成的位图,内存会被分类到这里

图2 使用 UIGraphicsImageRenderer API

明显观察到的内容是 占用巨大的 IOSurface 不存在了,但是内存块儿多了一个 CG raster data 大小为 5824K

1367 * 1089 * 4 / 1024 = 5815.08984375 跟 5824 很接近是不是 ? 可是又会有一点儿感觉不对,图片的宽和高都没有按照缩放的size 进行计算。于是就这个问题又去查了一下。

UIGraphicsImageRenderer 实现的原理是,系统可以根据图片分辨率选择创建解码图片的格式,如选用SRGB format 格式,每个像素占用 4 字节,而Alpha 8 format,每像素只占用 1 字节,因此可以减少大量的解码内存占用。

使用 UIGraphicsImageRenderer 之后这张图片physical footprint增加了多少内存 ? 0.2M

那么优化了多少内存呢 (48 - 0.2) / 48 = 0.996 , 释放了 96% 的可用空间。