-
iOS内存管控实战(下)—实战篇
因文章单篇过长,按照 原理、分析工具 和 实战 拆分成上、中、下三部分,点击阅读。
三、内存优化实战
(一)图片压缩优化
近期我着手处理了一个多张高清大图裁剪时爆内存的问题,经过我的测试,原有的图片压缩逻辑在选了N张大图后,内存会飙升到2G左右。。。 确实不爆就怪了。
我们先从原理上简单介绍一下iOS加载图片时的内存的消耗情况。
1. iOS图像渲染原理
(1)图像渲染管线 (Image Rendering Pipeline)
从 MVC 架构的角度来说,UIImage 代表了 Model,UIImageView 代表了 View. 那么渲染的过程我们可以这样很简单的表示:
Model 负责加载数据,View 负责展示数据。
但实际上,渲染的流程还有一个很重要的步骤:解码(Decode)。
为了了解Decode,首先我们需要了解Buffer这个概念。
(2)缓冲区 (Buffers)
Buffer 在计算机科学中,通常被定义为一段连续的内存,作为某种元素的队列来使用。
下面让我们来了解几种不同类型的 Buffer。
Image Buffers
Image Buffers 代表了图片(Image)在内存中的表示。每个元素代表一个像素点的颜色,Buffer 大小与图像大小成正比
:) 注意这里说的图像大小指的是:图像分辨率,不是图像的文件大小。例如:有一个 590KB 的图片,分辨率是 2048px * 1536px,它实际使用的内存不是 590KB,而是2048 * 1536 * 4(4 bytes per pixel)) = 12 MB ,这是很恐怖的,不过这也是我们可以着重优化的地方。
frame buffer
frame buffer 代表了一帧在内存中的表示。
Data Buffers
Data Buffers 代表了图片文件(Image file)在内存中的表示。这是图片的元数据,不同格式的图片文件有不同的编码格式。Data Buffers不直接描述像素点。 因此,Decode这一流程的引入,正是为了将Data Buffers转换为真正代表像素点的Image Buffer
因此,图像渲染管线,实际上是像这样的:
2. 图片裁剪优化
项目中原来的图片裁剪使用的是UIKit提供的API:
1 | extension UIImage { |
UIKit处理大分辨率图片时,往往容易出现OOM,原因是-[UIImage drawInRect:]在绘制时,先解码图片,再生成原始分辨率大小的bitmap,这是很耗内存的。解决方法是使用更低层的ImageIO接口,避免中间bitmap产生。
苹果官方在Performance Best Practices也给出了相关的建议:
Use Core Graphics or Image I/O functions to crop or downsample, such as the functions CGImageCreateWithImageInRect or CGImageSourceCreateThumbnailAtIndex.
所以我们可以将图片裁剪的API接口替换成 ImageIO :
1 | extension UIImage { |
3. 图片压缩优化
在原项目代码中,有需求需要将图片使用 UIImageJPEGRepresentation
将图片压缩到目标文件大小,代码逻辑大致如下:
1 | while (imageData.length / 1024 > 1024 * maxSize && imageCompressRate > 0) { |
可以看到这里采用的是使用 UIImageJPEGRepresentation 进行循环压缩,假设我们选取的是60MB的图片,而目标是1MB,可能使用 UIImageJPEGRepresentation 将压缩因子从1压缩到0.1都无法达成目标,但这个过程却要调用10次 UIImageJPEGRepresentation 进行压缩。
那么在一个方法里循环调用 UIImageJPEGRepresentation 会不会对内存产生压力呢? 我们用下面代码进行测试:
1 | - (void)viewDidLoad { |
然后观察Memory,可以发现明显出现了内存抖动:
峰值最高时内存达到了60MB,也就是说使用 UIImageJPEGRepresentation 进行图片压缩时,压缩过程中所开辟的内存并不会主动释放掉,而是等整个方法跑完之后才会进行释放,解决这类问题正式我们 @autoreleasepool 关键词的强项,在官方给出的 autoreleasepool 介绍中有这么一段话:
1 | If you write a loop that creates many temporary objects.You may use an autorelease pool block inside the loop to dispose of those objects before the next iteration. Using an autorelease pool block in the loop helps to reduce the maximum memory footprint of the application. |
那么我们将 @autoreleasepool 加入我们的优化方案中:
1 | - (void)viewDidLoad { |
但我们神奇地发现!使用 autoreleasepool 并没有减轻我们的内存抖升问题,毛用都没有!(暂时还想不明白为什么一直没释放这块内存,打算今年WWDC有机会问一下apple工程师)
那怎么办?我们要优化我们检索到最佳图片的路径,不要使用压缩因子递减0.1的方法,而是采用更有效率的检索方式,这里可以采用两种方式:
- 二分查找法
- 反比例模型计算法
其中 反比例模型计算法是我自己摸索的压缩方式,仅供参考。
5. 小插曲
1. JPEF压缩因子为1时,图像甚至会变大
Why image size get increased after UIImagePNGRepresentation?
2. 图片裁剪参数scale是什么含义
scale 本质是缩放因子。
图像的尺寸
image.size并不是实际的像素,只是显示在屏幕的尺寸。
显示的尺寸 = 实际的像素 / 缩放比例
获取实际的像素
宽:CGImageGetWidth(image.CGImage)
高:CGImageGetHeight(image.CGImage)
image.size.width = CGImageGetWidth(image.CGImage) / image.scale
image.size.height = CGImageGetHeight(image.CGImage) / image.scale