SDWebImage内存爆增问题及Image解压缩

3,746 阅读4分钟

前言

在一次Feed流开发中,出现滑动视图后内存爆增并且退出Feed界面内存没有被回收的问题。

分析定位

用instrument定位到的内存增长点

- (nullable UIImage *)sd_decompressedImageWithImage:(nullable UIImage *)image

定位到SDWebImage在对图片进行解压缩的时候内存爆增,但是从代码中看CreateRealse配对使用,看起来也没什么问题,那就应该不是内存泄漏了,应该是这几行代码在执行过程中产生的内存消耗,但是为什么消耗这么大???

补充知识:图片解压缩

图片加载过程

在说图片解压缩之前,先说说从磁盘加载一张图片并显示到屏幕上的步骤:

  1. 使用+imageWithContentsOfFile:从磁盘中加载一张图片,这时候这张图片并没有解压缩
  2. 将UIImage赋值给UIImageView
  3. 接着一个隐式的CATransition捕获到了UIImageView图层数的变化
  4. 在主线程的下一个runloop到来时,Core Animation提交了这个隐式过渡
    • 4.1 分配内存缓存区用于管理文件IO和解压操作
    • 4.2 将文件数据从磁盘读取到内存中
    • 4.3 将压缩的图片数据解码成未压缩的位图形式,这是个非常耗时的CPU操作
    • 4.4 最后Core Animation使用未压缩的位图数据渲染UIImageView涂层

上面的步骤,将压缩的图片进行解压缩时非常耗时的CPU操作,并且这个操作默认是在主线程执行,因此在需要加载多张大图的时候,就会造成严重的性能问题,比如卡顿。

图片解压缩的必要性

既然图片解压缩这么耗性能,我们可不可以不做呢?⚠️当然不能!

在研究为什么不可以不做图片解压缩操作之前,先了解下什么是位图:

苹果官方定义:
A bitmap image (or sampled image) is an array of pixels (or samples). Each pixel represents a single point in the image. JPEG, TIFF, and PNG graphics files are examples of bitmap images.

位图图像(或者采样图像)是像素(或者样本)的数组。每个像素代表图像中的单个点。JPEG、TIFF和PNG文件都是位图图像的例子。

我们知道,不管是JPEG还是PNG,都是一种压缩的位图图形格式。只不过PNG是无损压缩,支持alpha通道,而JPEG是有损压缩,可以指定压缩比例(0-100%)。

因此,在将磁盘中的图片渲染到屏幕之前,必须先要得到***原始像素数据***,才能执行后续绘制操作,这就是为什么图片解压缩操作不可省略。

继续内存问题

A: 如何解决主线程进行解压缩导致的卡顿等问题? Q: 将解压缩提前在后台线程执行掉,然后CGContextDrawImage() 绘制到画布中,将生成的图片存到缓存中,从而避免让系统区做额外的解压缩操作。这也是SDWebImage的底层实现。

但是既然这么做是为了优化性能问题,那为什么又会存在严重的内存问题呢??

在SDWebImage的issue中有相关的讨论:

harishkashyap commented on Dec 23, 2014
Its the memory issue again. decodedImageWithImage takes up huge memory and causes the app to crash. I have added an option to put this off in the library but defaulting to YES so there aren't any breaking changes. If you put off the decodeImageWithImage method in both image cache and image downloader then you shouldn't be seeing the VM: CG Raster data on the top consuming lots of memory
decodeImageWithImage is supposed to decompress images and cache them so the loading on tableviews/collectionviews become better. However, with large set of images being loaded, the experience worsened and the memory of uncompressed images even with thumbnails can consume GBs of memory. Putting this off only improved performance.

这个讨论中提到,-decodeImageWithImage这个方法用于将图片进行解压缩并缓存起来,以保证tableviews/collections交互流畅。但是如果加载高分辨率的图的话会适得其反,造成庞大的内存消耗。

CGBitmapContextCreate创建位图方法,每一个像素点都会分配一个空间来存储相关值,高分辨率的图像素点就多,也就需要分配更多的空间。这就是为什么解压缩操作会造成内存飙升。

而且在图片解压缩后,App第一次退到后台或者收到内存警告时,该图片的缓存才会被清空,其他情况会一直存在于全局缓存中。

解决方案

  1. 按需开启和关闭shouldCacheImagesInMemory
  2. 设置maxMemoryCost
  3. 手动调用-clearMemory方法