iOS ☞ SDWebimage 内存暴增问题
前言
相信很多开发都用过 SDWebimage
来解决 UITableView/UICollectionView
滑动卡顿等问题,而且很多公司在面试的时候都会被问到 SDWebimage
运行流程等问题。
运行流程
以最常用的 UIImageView
为例:
UIImageView+WebCache
:setImageWithURL:placeholderImage:options:
先显示placeholderImage
, 同时由SDWebimageManager
根据URL
在本地查找图片。SDWebimageManager
:downloadWithURL:delegate:options:userInfo:
SDWebImageManager
是将UIImageView+WebCache
同SDImageCache
链接起来的类。SDImageCache
:queryDiskCacheForKey:delegate:userInfo
用来根据CacheKey
查找图片是否已经在缓存中。- 如果内存中已经有图片缓存,
SDWebimageManager
会回调SDImageCacheDelegate
:imageCache:didFindImage:forKey:userInfo:
- 而
UIImageView+WebCache
则回调SDWebImageManagerDelegate
:webImageManager:didFinishWithImage:
来显示图片。 - 如果内存中没有图片缓存,那么生成
NSInvocationOperation
添加到队列,从硬盘查找图片是否已经被下载。 - 根据
URLKey
在硬盘缓存目录下尝试读取图片文件。这一步是在NSOperation
中进行的操作,所以回主线程进行结果回调notifyDelegate:
。 - 如果上一操作从硬盘中读取到了图片,则将图片添加到内存缓存中(如果空闲内存过小,会先情况内存缓存)。
SDImageCacheDelegate
回调imageCache:didFindImage:forKey:userInfo:
。进而回调展示图片 - 如果没有从硬盘缓存目录中读取到图片,则说明所有缓存都不存在该图片,需要下载图片,回调
imageCache:didNotFindImageForKey:userInfo:
。 - 共享或重新生成一个下载器
SDWebimageDownloader
开始下载图片。 - 图片下载有
NSURLSession
来做,实现相关delegate
来判断图片下载中、下载完成和下载失败的状态 connection:didReceiveData:
中利用ImageIO
做了按图片下载进度加载效果。connectionDidFinishLoading:
数据下载完成后交给SDWebimageDecoder
做图片解码处理。- 图片解码处理在一个
NSOperationQueue
中完成,不会拖慢主线程UI。如果有需要对下载的图片进行二次处理,最好也在这里完成,效率会好很多。 - 在主线程
notifyDelegateOnMainThreadWithInfo:
宣告解码完成,imageDecoder:didFinishDecodingImage:userInfo:
回调给SDWebImageDownloader
。 imageDownloader:didFinishWithImage:
回调给SDWebImageManager
告知图片下载完成。- 通知所有的
downloadDelegates
下载完成,回调给需要的地方展示图片。 - 将图片保存到
SDImageCache
中,内存缓存和硬盘缓存同时保存。 - 写文件到硬盘在单独的
NSInvocationOperation
中完成,避免拖慢主线程。 - 如果是在
iOS
上运行,SDImageCache
在初始化的时候会注册notification
到UIApplicationDidReceiveMemoryWarningNotification
以及UIApplicationWillTerminateNotification
, 在内存警告的时候清理内存图片缓存,应用结束的时候清理过期图片。 SDWebImagePrefetcher
可以预先下载图片,方便后续使用。
以上就是SDWebImage运行的基本流程,开发者应该或多或少的了解过或者背过。那么现在有一个问题:
上述步骤中,哪些是卡UI的?如果将图片更换为从本地获取,UITableView/UICollectionView 在滚动时还会不会卡顿?
图片加载的工作流
概况来说,从磁盘中加载一张图片,并将它显示到屏幕上,中间的主要工作流程如下:
- 假设我们使用
+imageWithContentsOfFile:
方法从磁盘中加载一张图片,此时的图片并没有解压缩; - 然后将生成的
UIImage
赋值给UIImageView
- 接着一个隐式的
CATransaction
捕获到了UIImageView
图层树的变化 - 在主线程的下一个
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.
其实,位图就是一个像素数组,数组中的每个像素就代表着图片中的一个点。我们在应用中经常用的的 JPEG
和 PNG
图片就是位图。
下面是一张 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
即可。width
和height
:位图的宽度和高度,分别赋值为图片的像素宽度和像素高度即可bitsPerComponent
:像素的每个颜色分量使用的bit
数,在RGB
颜色空间下指定8
即可bytesPerRow
: 位图的每一行使用的字节数,大小至少为width * bytes pre pixel
字节。有意思的是,当我们指定0
时,系统不仅会为我们自动计算,而且还会进行cache line alignment
的优化。space
:就是我们前面提到的颜色空间,一般使用RGB
即可bitmapInfo
:就是前面提到的位图的布局信息。
到这里,你已经掌握了强制解压缩图片需要用到的最核心的函数。
SDWebImage 引发内存问题
SDWebImage
的底层实现都是使用 CGBitmapContextCreate
函数来进行的。
但是既然这么做是为了优化性能问题,那么为什么又会存在严重的内存问题呢?
在 SDWebImage
的 issue
中有相关的讨论:
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
第一次退到后台或者收到内存警告时,该图片的缓存才会清空,其他情况会一直存在与全局缓存中。
解决方案
- 按需开启和关闭
shouldCacheImagesInMemory
- 设置
maxMemoryCost
- 手动调用
-clearMemory
方法
iOS ☞ SDWebimage 内存暴增问题相关推荐
- iOS内存暴增问题追查与使用陷阱
iOS平台的内存使用引用计数的机制,并且引入了半自动释放机制:这种使用上的多样性,导致开发者在内存使用上非常容易出现内存泄漏和内存莫名的增长情况: 本文会介绍iOS平台的内存使用原则与使用陷阱: 深度 ...
- android 图库 imgcache.idx,iOS开发 - 关于列表图片渲染内存暴增问题
关于列表图片渲染内存暴增问题 - (void)viewDidLoad { [super viewDidLoad]; [SDImageCache sharedImageCache].config.sho ...
- glibc(ptmalloc)内存暴增问题解决
from:http://blog.chinaunix.net/uid-18770639-id-3385860.html 点击(此处)折叠或打开 #include <stdio.h> #in ...
- 分析 Go time.After 引起内存暴增 OOM 问题
还没正式上班,朋友来个电话让我帮忙排查一个问题.说是用 golang 写的牛逼的调度服务出现了内存泄露问题,Go 内存在任务暴增的时候增长很诡异. 从上线部署起,只要上游任务一上量就 oom 了.大过 ...
- 加载大量图片内存暴增导致闪退 Terminated due to memory issue(内存暴增SDWebImage加载高清大图崩溃)
上传图片一定要压缩,一定要压缩,一定要压缩.(目前手机拍摄的图片一张几M,上传后不压缩,如果几十张一块加载展示时内存画面有点美!如果是后台上传除了需要高清以外的图也需要压缩处理) 下载大量图片时一定要 ...
- BitArray虽好,但请不要滥用,又一次线上内存暴增排查
一:背景 1. 讲故事 前天写了一篇大内存排查在园子里挺火,这是做自媒体最开心的事拉,干脆再来一篇满足大家胃口,上个月我写了一篇博客提到过使用bitmap对原来的List<CustomerID& ...
- 关于Services.exe开机CPU内存使用暴增解决方案
这两天系统(Windows Server 2003 SP2)开机,发现Services.exe进程CPU使用率暴增并且伴随内存狂耗,内存和虚拟内存可以在10分钟之内耗尽.我3G内存呀,外加2G虚拟内存 ...
- 流量暴增,掌门教育如何基于 Spring Cloud Alibaba 构建微服务体系?
作者 | 童子龙 掌门教育基础架构部架构师 **导读:**本文整理自作者于 2020 年云原生微服务大会上的分享<掌门教育云原生落地实践>,本文主要介绍了掌门教育云原生落地实践,主要围绕 ...
- Android 内存暴减的秘密?!
作者:杨超,腾讯移动客户端开发 工程师 商业转载请联系腾讯WeTest获得授权,非商业转载请注明出处. 原文链接:http://wetest.qq.com/lab/view/362.html WeTe ...
最新文章
- Matlab与数据结构 -- 对向量的排序
- “去了太空就别回来了!”贝索斯还没“上天”,就遭美国 5 万多人请愿:不准重返地球...
- 在应用程序中替换Linux中Glibc的malloc的四种方法
- 《AR与VR开发实战》——2.7 3D物体识别
- 2020 操作系统第三天复习(知识点总结)
- Linux监控工具介绍系列——smem
- Java-静态方法、非静态方法
- 怎样才能培养孩子良好的用餐习惯
- php微框架 flight源码阅读
- Hibernate:More than one row with the given identifier was found解决办法
- wps一直显示正在备份怎么办_笔记本电脑显示器一直闪动怎么办
- 规律、逻辑规律与悖论
- 数据库连接池为啥要用 ThreadLocal?不用会怎么样?
- 图片节点html,Qunee for HTML5 - 中文 : 节点图片
- 让Visio2007/2003支持UML2.2
- 全国31个省市2001-2017年平均受教育年限学习数据集
- 化学实用计算机技能,实用化学化工计算机软件基础
- 世界各国发展指标(1960-2019)
- 区分单音节,双音节和多音节
- php-fpm彻底解决502(php-fpm多开、nginx限制并发、定时重启)解决网站卡顿的终极奥义