前言

相信很多开发都用过 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.

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

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

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

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

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

事实上,不管是 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 eachpixel is specified by `space', which may also specify a destination colorprofile.  Note that the only legal case when `space' can be NULL is whenalpha is specified as kCGImageAlphaOnly.The number of bits for each componentof a pixel is specified by `bitsPerComponent'. The number of bytes per pixelis equal to `(bitsPerComponent * number of components + 7)/8'. Each row ofthe bitmap consists of `bytesPerRow' bytes, which must be at least`width * bytes per pixel' bytes; in addition, `bytesPerRow' must be aninteger 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 freedwhen the context is deallocated. `bitmapInfo' specifies whether the bitmapshould contain an alpha channel and how it's to be generated, along withwhether 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);

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

1. Pixel Format 像素格式

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

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

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

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);

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

  • 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 */
};

它同样也提供了三方面的 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).

可以知道,当图片不包含 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);

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

  • 小端模式还是大端模式
  • 数据以 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.

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

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

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

解决方案

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

iOS ☞ SDWebimage 内存暴增问题相关推荐

  1. iOS内存暴增问题追查与使用陷阱

    iOS平台的内存使用引用计数的机制,并且引入了半自动释放机制:这种使用上的多样性,导致开发者在内存使用上非常容易出现内存泄漏和内存莫名的增长情况: 本文会介绍iOS平台的内存使用原则与使用陷阱: 深度 ...

  2. android 图库 imgcache.idx,iOS开发 - 关于列表图片渲染内存暴增问题

    关于列表图片渲染内存暴增问题 - (void)viewDidLoad { [super viewDidLoad]; [SDImageCache sharedImageCache].config.sho ...

  3. glibc(ptmalloc)内存暴增问题解决

    from:http://blog.chinaunix.net/uid-18770639-id-3385860.html 点击(此处)折叠或打开 #include <stdio.h> #in ...

  4. 分析 Go time.After 引起内存暴增 OOM 问题

    还没正式上班,朋友来个电话让我帮忙排查一个问题.说是用 golang 写的牛逼的调度服务出现了内存泄露问题,Go 内存在任务暴增的时候增长很诡异. 从上线部署起,只要上游任务一上量就 oom 了.大过 ...

  5. 加载大量图片内存暴增导致闪退 Terminated due to memory issue(内存暴增SDWebImage加载高清大图崩溃)

    上传图片一定要压缩,一定要压缩,一定要压缩.(目前手机拍摄的图片一张几M,上传后不压缩,如果几十张一块加载展示时内存画面有点美!如果是后台上传除了需要高清以外的图也需要压缩处理) 下载大量图片时一定要 ...

  6. BitArray虽好,但请不要滥用,又一次线上内存暴增排查

    一:背景 1. 讲故事 前天写了一篇大内存排查在园子里挺火,这是做自媒体最开心的事拉,干脆再来一篇满足大家胃口,上个月我写了一篇博客提到过使用bitmap对原来的List<CustomerID& ...

  7. 关于Services.exe开机CPU内存使用暴增解决方案

    这两天系统(Windows Server 2003 SP2)开机,发现Services.exe进程CPU使用率暴增并且伴随内存狂耗,内存和虚拟内存可以在10分钟之内耗尽.我3G内存呀,外加2G虚拟内存 ...

  8. 流量暴增,掌门教育如何基于 Spring Cloud Alibaba 构建微服务体系?

    作者 | 童子龙  掌门教育基础架构部架构师 **导读:**本文整理自作者于 2020 年云原生微服务大会上的分享<掌门教育云原生落地实践>,本文主要介绍了掌门教育云原生落地实践,主要围绕 ...

  9. Android 内存暴减的秘密?!

    作者:杨超,腾讯移动客户端开发 工程师 商业转载请联系腾讯WeTest获得授权,非商业转载请注明出处. 原文链接:http://wetest.qq.com/lab/view/362.html WeTe ...

最新文章

  1. Matlab与数据结构 -- 对向量的排序
  2. “去了太空就别回来了!”贝索斯还没“上天”,就遭美国 5 万多人请愿:不准重返地球...
  3. 在应用程序中替换Linux中Glibc的malloc的四种方法
  4. 《AR与VR开发实战》——2.7 3D物体识别
  5. 2020 操作系统第三天复习(知识点总结)
  6. Linux监控工具介绍系列——smem
  7. Java-静态方法、非静态方法
  8. 怎样才能培养孩子良好的用餐习惯
  9. php微框架 flight源码阅读
  10. Hibernate:More than one row with the given identifier was found解决办法
  11. wps一直显示正在备份怎么办_笔记本电脑显示器一直闪动怎么办
  12. 规律、逻辑规律与悖论
  13. 数据库连接池为啥要用 ThreadLocal?不用会怎么样?
  14. 图片节点html,Qunee for HTML5 - 中文 : 节点图片
  15. 让Visio2007/2003支持UML2.2
  16. 全国31个省市2001-2017年平均受教育年限学习数据集
  17. 化学实用计算机技能,实用化学化工计算机软件基础
  18. 世界各国发展指标(1960-2019)
  19. 区分单音节,双音节和多音节
  20. php-fpm彻底解决502(php-fpm多开、nginx限制并发、定时重启)解决网站卡顿的终极奥义

热门文章

  1. #四、股市操作方法大道可否至简?
  2. 2019腾讯区块链白皮书(附完整版下载)
  3. Netty实现聊天室
  4. [简洁版]youtube-dl下载命令
  5. Plantuml类图用法
  6. WIN7下硬盘安装centos 7
  7. 读《微波工程(第三版)》笔记 (10:终端接负载的无耗传输线)
  8. 【技巧】EXCEL如何按行找出最大三个数并标记
  9. 帝国CMS7.2 手机网站使用教程
  10. 大数据分析的思维方式有哪些