• iOS ☞ SDWebimage 内存暴增问题


    前言

    相信很多开发都用过 SDWebimage 来解决 UITableView/UICollectionView 滑动卡顿等问题,而且很多公司在面试的时候都会被问到 SDWebimage 运行流程等问题。

    运行流程

    以最常用的 UIImageView 为例:

    1. UIImageView+WebCachesetImageWithURL:placeholderImage:options: 先显示 placeholderImage, 同时由 SDWebimageManager 根据 URL 在本地查找图片。
    2. SDWebimageManager: downloadWithURL:delegate:options:userInfo: SDWebImageManager 是将UIImageView+WebCacheSDImageCache 链接起来的类。
    3. SDImageCachequeryDiskCacheForKey:delegate:userInfo 用来根据 CacheKey 查找图片是否已经在缓存中。
    4. 如果内存中已经有图片缓存,SDWebimageManager 会回调 SDImageCacheDelegateimageCache:didFindImage:forKey:userInfo:
    5. UIImageView+WebCache 则回调 SDWebImageManagerDelegate: webImageManager:didFinishWithImage: 来显示图片。
    6. 如果内存中没有图片缓存,那么生成 NSInvocationOperation 添加到队列,从硬盘查找图片是否已经被下载。
    7. 根据 URLKey 在硬盘缓存目录下尝试读取图片文件。这一步是在 NSOperation 中进行的操作,所以回主线程进行结果回调 notifyDelegate:
    8. 如果上一操作从硬盘中读取到了图片,则将图片添加到内存缓存中(如果空闲内存过小,会先情况内存缓存)。SDImageCacheDelegate 回调 imageCache:didFindImage:forKey:userInfo:。进而回调展示图片
    9. 如果没有从硬盘缓存目录中读取到图片,则说明所有缓存都不存在该图片,需要下载图片,回调 imageCache:didNotFindImageForKey:userInfo:
    10. 共享或重新生成一个下载器 SDWebimageDownloader 开始下载图片。
    11. 图片下载有 NSURLSession 来做,实现相关 delegate 来判断图片下载中、下载完成和下载失败的状态
    12. connection:didReceiveData: 中利用 ImageIO 做了按图片下载进度加载效果。
    13. connectionDidFinishLoading: 数据下载完成后交给 SDWebimageDecoder 做图片解码处理。
    14. 图片解码处理在一个 NSOperationQueue 中完成,不会拖慢主线程UI。如果有需要对下载的图片进行二次处理,最好也在这里完成,效率会好很多。
    15. 在主线程 notifyDelegateOnMainThreadWithInfo: 宣告解码完成,imageDecoder:didFinishDecodingImage:userInfo: 回调给 SDWebImageDownloader
    16. imageDownloader:didFinishWithImage: 回调给 SDWebImageManager 告知图片下载完成。
    17. 通知所有的 downloadDelegates 下载完成,回调给需要的地方展示图片。
    18. 将图片保存到 SDImageCache 中,内存缓存和硬盘缓存同时保存。
    19. 写文件到硬盘在单独的 NSInvocationOperation 中完成,避免拖慢主线程。
    20. 如果是在 iOS 上运行,SDImageCache 在初始化的时候会注册 notificationUIApplicationDidReceiveMemoryWarningNotification 以及 UIApplicationWillTerminateNotification, 在内存警告的时候清理内存图片缓存,应用结束的时候清理过期图片。
    21. SDWebImagePrefetcher 可以预先下载图片,方便后续使用。

    以上就是SDWebImage运行的基本流程,开发者应该或多或少的了解过或者背过。那么现在有一个问题:
    上述步骤中,哪些是卡UI的?如果将图片更换为从本地获取,UITableView/UICollectionView 在滚动时还会不会卡顿?

    图片加载的工作流

    概况来说,从磁盘中加载一张图片,并将它显示到屏幕上,中间的主要工作流程如下:

    1. 假设我们使用 +imageWithContentsOfFile: 方法从磁盘中加载一张图片,此时的图片并没有解压缩;
    2. 然后将生成的 UIImage 赋值给 UIImageView
    3. 接着一个隐式的 CATransaction 捕获到了 UIImageView 图层树的变化
    4. 在主线程的下一个 RunLoop 到来时,Core Animation 提交了这个隐式的 Transaction,这个过程可能会对图片进行 copy 操作,而受图片是否字节对齐等因素的影响,这个 copy 操作可能会涉及以下部分或全部步骤:
      • 分配内存缓冲区用于管理文件 IO 和解压缩操作
      • 将文件数据从磁盘读取到内存中
      • 将压缩的图片数据解码成未压缩的位图形式,这是一个非常耗时的 CPU 操作
      • 最后 Core Animation 使用未压缩的位图数据渲染 UIImageView 的图层。

    由上面的步骤可知,图片的解压缩是一个非常耗时的 CPU 操作,并且它默认是在主线程中执行。那么当需要加载图片较多时,就会对我们应用的响应性造成严重的影响,尤其是在快速滑动的列表上,这个问题会表现的更加突出。

    为什么需要解压缩

    既然图片的解压缩需要消耗大量的 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.
    
    • 1

    其实,位图就是一个像素数组,数组中的每个像素就代表着图片中的一个点。我们在应用中经常用的的 JPEGPNG 图片就是位图。

    下面是一张 PNG 图片,像素为 30 × 30,文件大小为 843B

    UIImage *image = [UIImage imageNamed:@"image"];
    CFDataRef rawData = CGDataProviderCopyData(CGImageGetDataProvider(image.CGImage));
    
    • 1
    • 2

    就可以获取到这个图片的原始像素数据,大小为 3600B

    解压缩后的图片大小(3600) = 图片的像素宽(30) * 图片的像素高(30) * 每个像素所占的字节数(4)
    
    • 1

    事实上,不管是 JPEG 还是 PNG 图片,都是一种压缩的位图图形格式。只不过 PNG 图片是无损压缩,并且支持 alpha 通道,而 JPEG 图片则是有损压缩,可以指定 0~100% 的压缩比。

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

    强制解压缩

    既然图片的解压缩不可避免,也不想让它在主线程执行,影响应用的响应性,那么是否有比较好的解决方案呢?答案是肯定的。

    当未解压缩的图片将要渲染到屏幕时,系统会在主线程对图片进行解压缩,而如果图片已经解压缩了,系统就不会再对图片进行解压缩。因此,也就有了业内的解决方案,在子线程提前对图片进行强制解压缩。

    而强制解压缩的眼里就是对图片进行重新绘制,得到一张新的解压缩后的位图。其中,用到的最核心的函数时 CGBitmapContextCreate

    /* Create a bitmap context. The context draws into a bitmap which is `width'
       pixels wide and `height' pixels high. The number of components for each
       pixel is specified by `space', which may also specify a destination color
       profile.  Note that the only legal case when `space' can be NULL is when
       alpha is specified as kCGImageAlphaOnly.The number of bits for each component
       of a pixel is specified by `bitsPerComponent'. The number of bytes per pixel
       is equal to `(bitsPerComponent * number of components + 7)/8'. Each row of
       the bitmap consists of `bytesPerRow' bytes, which must be at least
       `width * bytes per pixel' bytes; in addition, `bytesPerRow' must be an
       integer multiple of the number of bytes per pixel. `data', if non-NULL,
       points to a block of memory at least `bytesPerRow * height' bytes.
       If `data' is NULL, the data for context is allocated automatically and freed
       when the context is deallocated. `bitmapInfo' specifies whether the bitmap
       should contain an alpha channel and how it's to be generated, along with
       whether the components are floating-point or integer. */
    
    CG_EXTERN CGContextRef __nullable CGBitmapContextCreate(void * __nullable data,
        size_t width, size_t height, size_t bitsPerComponent, size_t bytesPerRow,
        CGColorSpaceRef cg_nullable space, uint32_t bitmapInfo)
        CG_AVAILABLE_STARTING(10.0, 2.0);
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20

    这个函数用于创建一个位图上下文,用来绘制一张宽 width 像素,高 height 像素的位图。这个函数的注释比较长,参数也比较难理解,但是先别急,先来了解下相关知识,然后再回来理解这些参数,就比较简单了。

    1. Pixel Format 像素格式

    位图其实就是一个像素数组,而像素格式则是用来描述每个像素的组成格式,它包括以下信息:

    • Bits per component: 一个像素中每个独立的颜色分量使用的 bit
    • Bits per pixel: 一个像素使用的总 bit
    • Bytes per row: 位图中的每一行使用的字节数

    有一点需要注意的是,对于位图来说,像素格式并不是随意组合的,目前 Apple 平台支持 17 种格式:
    Pixel Format

    2. Color and Color Spaces 颜色空间

    什么是颜色空间呢? 在 Quartz 中,一个颜色是由一组值来表示的,比如(0,0,1)。而颜色空间是用来说明如何解析这些值的。

    3. Color Spaces and Bitmap Layout 位图布局

    像素格式是用来描述每个像素的组成格式的,比如每个像素使用的总 bit 数。而要想确保 Quartz 能够正确的解析这些 bit 所代表的含义,我们还需要提供位图的布局信息 CGBitmapInfo

    typedef CF_OPTIONS(uint32_t, CGBitmapInfo) {
        kCGBitmapAlphaInfoMask = 0x1F,
    
        kCGBitmapFloatInfoMask = 0xF00,
        kCGBitmapFloatComponents = (1 << 8),
    
        kCGBitmapByteOrderMask     = kCGImageByteOrderMask,
        kCGBitmapByteOrderDefault  = (0 << 12),
        kCGBitmapByteOrder16Little = kCGImageByteOrder16Little,
        kCGBitmapByteOrder32Little = kCGImageByteOrder32Little,
        kCGBitmapByteOrder16Big    = kCGImageByteOrder16Big,
        kCGBitmapByteOrder32Big    = kCGImageByteOrder32Big
    } CG_AVAILABLE_STARTING(__MAC_10_0, __IPHONE_2_0);
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13

    它主要提供了三方面的布局信息:

    • alpha 的信息
    • 颜色分量是否为浮点数
    • 像素格式的字节顺序

    其中 alpha 的信息由枚举值 CGImageAlphaInfo 来表示:

    typedef CF_ENUM(uint32_t, CGImageAlphaInfo) {
        kCGImageAlphaNone,               /* For example, RGB. */
        kCGImageAlphaPremultipliedLast,  /* For example, premultiplied RGBA */
        kCGImageAlphaPremultipliedFirst, /* For example, premultiplied ARGB */
        kCGImageAlphaLast,               /* For example, non-premultiplied RGBA */
        kCGImageAlphaFirst,              /* For example, non-premultiplied ARGB */
        kCGImageAlphaNoneSkipLast,       /* For example, RBGX. */
        kCGImageAlphaNoneSkipFirst,      /* For example, XRGB. */
        kCGImageAlphaOnly                /* No color data, alpha data only */
    };
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10

    它同样也提供了三方面的 alpha 信息:

    • 是否包含 alpha
    • 如果包含 alpha,那么 alpha 信息所处的位置,在像素的最低有效位,比如 RGBA,还是最高有效位,比如 ARGB
    • 如果包含 alpha ,那么每个颜色分量是否已经乘以 alpha 的值,这种做法可以加速图片的渲染时间,因为它避免了渲染时的额外乘法运算。比如对于 RGB 颜色空间,用已经乘以 alpha 的数据来渲染图片,每个像素都可以避免 3 次乘法运算,即红色乘以 alpha,绿色乘以 alpha,蓝色乘以 alpha。

    那么在解压缩图片的时候应该使用哪个值呢? 根据官方文档对 UIGraphicsBeginImageContextWithOptions 函数的讨论:

    You use this function to configure the drawing environment for rendering into a bitmap. 
    The format for the bitmap is a ARGB 32-bit integer pixel format using host-byte order. If the opaque parameter is YES, 
    the alpha channel is ignored and the bitmap is treated as fully opaque (kCGImageAlphaNoneSkipFirst | kCGBitmapByteOrder32Host). 
    Otherwise, each pixel uses a premultipled ARGB format (kCGImageAlphaPremultipliedFirst | kCGBitmapByteOrder32Host).
    
    • 1
    • 2
    • 3
    • 4

    可以知道,当图片不包含 alpha 的时候使用 kCGImageAlphaNoneSkipFirst,否则使用 kCGImageAlphaPremultipliedFirst。字节顺序使用 32 位主机顺序 kCGBitmapByteOrder32Host

    像素格式的字节顺序是由枚举值 CGImageByteOrderInfo 来表示:

    typedef CF_ENUM(uint32_t, CGImageByteOrderInfo) {
        kCGImageByteOrderMask     = 0x7000,
        kCGImageByteOrder16Little = (1 << 12),
        kCGImageByteOrder32Little = (2 << 12),
        kCGImageByteOrder16Big    = (3 << 12),
        kCGImageByteOrder32Big    = (4 << 12)
    } CG_AVAILABLE_STARTING(__MAC_10_12, __IPHONE_10_0);
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7

    它主要提供了两个方面的字节顺序信息

    • 小端模式还是大端模式
    • 数据以 16 位还是 32 位为单位。

    4. CGBitmapContextCreate 参数

    • data:如果不为 NULL,那么它应该指向一块大小至少为 bytesPerRow * height 字节的内存,如果为 NULL,那么系统就会为我们自动分配和释放所需的内存,所以一般指定 NULL 即可。
    • widthheight:位图的宽度和高度,分别赋值为图片的像素宽度和像素高度即可
    • bitsPerComponent:像素的每个颜色分量使用的 bit 数,在 RGB 颜色空间下指定 8 即可
    • bytesPerRow: 位图的每一行使用的字节数,大小至少为 width * bytes pre pixel 字节。有意思的是,当我们指定 0 时,系统不仅会为我们自动计算,而且还会进行 cache line alignment 的优化。
    • space:就是我们前面提到的颜色空间,一般使用 RGB 即可
    • bitmapInfo:就是前面提到的位图的布局信息。

    到这里,你已经掌握了强制解压缩图片需要用到的最核心的函数。

    SDWebImage 引发内存问题

    SDWebImage 的底层实现都是使用 CGBitmapContextCreate 函数来进行的。

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

    SDWebImageissue 中有相关的讨论:

    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.
    
    • 1
    • 2
    • 3
    • 4
    • 5

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

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

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

    解决方案

    1. 按需开启和关闭 shouldCacheImagesInMemory
    2. 设置 maxMemoryCost
    3. 手动调用 -clearMemory 方法
  • 相关阅读:
    3.8 如何在小红书上蹭热点,这里有8个方法【玩赚小红书】
    项目实战(计划任务,Mybatis拦截器,SpringMVC)
    形态学操作—开运算
    配置SSH免密登录(CentOS 7.9操作系统)
    Lambda表达式 Stream流
    【FreeRTOS(六)】队列
    云表:MES系统是工业4.0数字化转型的核心
    java计算机毕业设计springboot+vue青少年编程在线考试系统
    一、Linux 入门、VM 与 Linux 的安装
    新建Mybatis项目
  • 原文地址:https://blog.csdn.net/LiqunZhang/article/details/126158821