SDWebImage加载大量图片崩溃
场景
最近在UITableView
和UICollectionView
中分别碰到了用sd_setImage
加载大量图片导致内存泄漏而崩溃的问题,尝试过如下方法
-
让服务端将webp图片的大小改小,基本已经保持每张图片在100k上下
-
设置最大并发下载数
swift
SDWebImageDownloader.shared.config.maxConcurrentDownloads = 10
- 在cell的
prepareForReuse
方法中取消下载
swift
override func prepareForReuse() {
super.prepareForReuse()
imageView.sd_cancelCurrentImageLoad()
}
崩溃频率情况有所改善,但是在非常快速的滑动和请求下还是招架不住内存激增而崩溃。
在这种情况下应该就不是图片本身有什么问题了,毕竟一张图才70k,就算屏幕上有30张图片也才3M左右,不可能导致内存泄漏,这个时候有两个考虑的方向
考虑方向
-
缓存的频繁读写
-
SDWebImage在图片下载完成后是不是对图片进行了其他解析处理
带着这两个问题我们来扒一扒源码。
SDWebImage
版本:5.18.6
以下源码分析部分为了便于理解,只保留关键部分
源码分析
sd_setImage
方法使用url、占位符、自定义选项和上下文为imageView设置image
。下载是异步 和缓存的。
参数说明
-
url:图片的url。
-
placeholder:最初要设置的图像,直到图像请求完成。
-
options:下载图像时使用的选项。是一个枚举,下面会详细说明
-
context :context包含不同的选项来执行指定的更改或进程,请参阅
SDWebImageContextOption
。它容纳了options
枚举不能容纳的额外对象。
objc
typedef NSDictionary<SDWebImageContextOption, id> SDWebImageContext;
- progressBlock:下载图像时调用的块
- 注意进度块在后台队列中执行
objc
typedef void(^SDImageLoaderProgressBlock)(NSInteger receivedSize,
NSInteger expectedSize,
NSURL * _Nullable targetURL);
completedBlock
:当操作完成时调用的块。这个块没有返回值,它以请求的UIImage作为第一个参数。如果发生错误,image参数为nil,第二个参数可能包含NSError。第三个参数是一个布尔值,表示图像是从本地缓存还是从网络中检索的。第四个参数是原始图片的url。
objectivec
typedef void(^SDExternalCompletionBlock)(UIImage * _Nullable image,
NSError * _Nullable error,
SDImageCacheType cacheType,
NSURL * _Nullable imageURL);
optinos - SDWebImageOptions
-
retryFailed
: 图片加载失败后,允许后续重新尝试加载。 -
lowPriority
: 图片下载应该在 UI 交互期间延后进行,减少对主线程的影响。 -
progressiveLoad
: 图片下载的过程中逐步显示图片,类似于浏览器的行为。 -
refreshCached
: 如果图片已经被缓存,使用这个选项将强制重新从网络下载图片并刷新缓存。 -
continueInBackground
: 允许应用在进入后台后继续下载图片。 -
handleCookies
: 在请求图片时处理和发送存储在 NSHTTPCookieStore 中的 cookies。 -
allowInvalidSSLCertificates
: 允许下载器使用无效的 SSL 证书。在生产环境中应该谨慎使用。 -
highPriority
: 图片下载任务应立即开始,而不是在队列中等待。 -
delayPlaceholder
: 在图片下载完成之前,不要显示占位符。 -
transformAnimatedImage
: 允许转换类库来处理和缓存动画图片。 -
avoidAutoSetImage
: 下载完成后,不要自动将下载的图片设置到 UIImageView 上。你可能想要在完成回调中手动设置图片。 -
scaleDownLargeImages
: 如果原始图片的尺寸大于设备的尺寸,自动缩小图片尺寸以节省内存。 -
queryDiskSync
: 同步从磁盘加载图片。默认情况下,磁盘加载是异步的。 -
queryMemoryData
: 同步查询内存缓存,异步查询磁盘缓存。 -
queryMemoryDataSync
: 同步查询内存缓存和磁盘缓存。 -
fromLoaderOnly
: 只从下载器加载图片,直接跳过缓存查询。 -
fromCacheOnly
: 只从缓存加载图片,不进行网络请求。 -
forceTransition
: 总是为图片加载强制执行过渡动画,即使图片已经缓存在内存中。 -
matchAnimatedImageClass
:用来指示缓存系统应该返回与请求的图像类类型匹配的对象。具体来说,如果请求的是一个动画图像类(例如SDAnimatedImage
),设置这个选项会让缓存系统尝试返回一个动画图像类实例,而不是基础的UIImage
对象。
context - SDWebImageContextOption
SDWebImageContext
是一个键值字典,其中的键定义在 SDWebImage
框架里,通常以 SDWebImageContext
为前缀。下面是一些常用的键和它们对应含义的例子:
-
storeCacheType
: 指定图片应该存储在哪个缓存类型中(例如,只存储到磁盘、只存储到内存等)。 -
customManager
: 允许为当前加载操作指定一个自定义的SDWebImageManager
。 -
setImageOperationKey
: 用于支持自定义的加载操作,可以为特定的加载操作设置一个唯一的键。 -
imageScaleFactor
: 图片加载完成后,调整图片的缩放因子。 -
imageThumbnailPixelSize
: 加载图片时生成缩略图的目标像素大小。 -
imageTransformer
: 指定一个SDImageTransformer
对象,用于在图片解码后进行转换(例如,裁剪、缩放等)。 -
cacheKeyFilter
: 一个SDWebImageCacheKeyFilter
对象,用于转换图像的缓存键。 -
imageCache
:一个实现了SDImageCache
协议的对象(若未实现则用默认的SDImageCache
单例对象)
调用流程
sd_internalSetImageWithURL
在所有的封装方法中,最终调用的是在UIView+WebCache.m
文件中sd_internalSetImageWithURL
方法
objectivec
// 1. 从context中获取SDWebImageContextSetImageOperationKey对应的值
// 若为空则设置为NSStringFromClass([self class]);用于跟踪操作或图像类
NSString *validOperationKey = context[SDWebImageContextSetImageOperationKey];
// 将该key赋值给UIView的关联对象sd_latestOperationKey
self.sd_latestOperationKey = validOperationKey;
// 若options中 不 包含avoidAutoSetImage,取消上次加载
···
[self sd_cancelImageLoadOperationWithKey:validOperationKey];
// 2. 创建SDWebImageLoadState对象,存入当前进度(0)和url
[self sd_setImageLoadState:loadState forKey:validOperationKey];
// 3. 从context中获取SDWebImageManager,若为空则用SDWebImageManager单例
···
manager = [SDWebImageManager sharedManager];
··· // progress处理
// 4. 创建SDWebImageCombinedOperation对象 并执行,存入SDOperationsDictionary字典中
可以看到主要调用SDWebImageManeger
的loadImageWithURL
SDWebImageManager - loadImageWithURL
- 前置判断和预处理
objectivec
// 常规判断,比如OC的类型检查不如swift严格,如果传入的是NSString,内部会封装成NSURL
···
// 创建SDWebImageCombinedOperation对象
// 若context和option中包含一些预处理的话,执行预处理。
// 比如imageTransformer,cacheKeyFilter, cacheSerializer
SDWebImageOptionsResult *result =
[self processedResultForURL:url options:options context:context];
- 下面就开始条目从缓存中加载图像
objectivec
[self callCacheProcessForOperation:operation
url:url
options:result.options
context:result.context
progress:progressBlock
completed:completedBlock];
最长的步骤如下
- 不使用转换器的步骤:
-
从缓存中查询图像
-
下载数据和图像
-
将图像存储到缓存中
- 使用转换器的步骤:
-
查询转换图像从缓存,若miss
-
从缓存中查询原始图像,若miss
-
下载数据和图像
-
在CPU中做变换
-
将原始图像存储到缓存中
-
存储转换后的图像到缓存
以下为SDImageCache
的queryImageForKey
流程
SDWebImageManeger - callCacheProcessForOperation
若optinons
中 不包含 fromLoaderOnly
,则先执行缓存查询
- 调用imageCache(默认单例对象/自定义)的
queryImageForKey
方法获取缓存,以下为默认处理SDImageCache - queryImageForKey
objectivec
SDImageCacheOptions cacheOptions = 0;
if (options & SDWebImageQueryMemoryData)
cacheOptions |= SDImageCacheQueryMemoryData;
if (options & SDWebImageQueryMemoryDataSync)
cacheOptions |= SDImageCacheQueryMemoryDataSync;
if (options & SDWebImageQueryDiskDataSync)
cacheOptions |= SDImageCacheQueryDiskDataSync;
if (options & SDWebImageScaleDownLargeImages)
cacheOptions |= SDImageCacheScaleDownLargeImages;
if (options & SDWebImageAvoidDecodeImage)
cacheOptions |= SDImageCacheAvoidDecodeImage;
if (options & SDWebImageDecodeFirstFrameOnly)
cacheOptions |= SDImageCacheDecodeFirstFrameOnly;
if (options & SDWebImagePreloadAllFrames)
cacheOptions |= SDImageCachePreloadAllFrames;
if (options & SDWebImageMatchAnimatedImageClass)
cacheOptions |= SDImageCacheMatchAnimatedImageClass;
return [self queryCacheOperationForKey:key options:cacheOptions context:context cacheType:cacheType done:completionBlock];
可以看到会先判断option中是否包含缓存相关的配置,用与或处理得出cacheOptions,调用queryCacheOperationForKey
方法找图,并执行解析 等操作。分为查询memoryCache 和diskCache两部分。
以下为先查询memoryCache部分
objectivec
// 1. 若不请求缓存,则返回空
if (queryCacheType == SDImageCacheTypeNone) {
if (doneBlock) {
doneBlock(nil, nil, SDImageCacheTypeNone);
}
return nil;
}
// 2. 若不为磁盘缓存,则查询内存缓存。即在SDImageCache的memoryCache对象中根据key查找。
UIImage *image;
if (queryCacheType != SDImageCacheTypeDisk) {
image = [self imageFromMemoryCacheForKey:key];
}
// 若从内存中找到图片
// 2.1 若option包含decodeFirstFrameOnly,且桢数大于1(如gif动图),获取首桢图片
image = [[UIImage alloc] initWithCGImage:image.CGImage
scale:image.scale
orientation:image.imageOrientation];
// 2.2 若option包含matchAnimatedImageClass,且context中若包含的animatedImageClass(表示期望的类)与实际获取的image类型不匹配,则将image置空
if (options & SDImageCacheMatchAnimatedImageClass) {
// Check image class matching
Class animatedImageClass = image.class;
Class desiredImageClass = context[SDWebImageContextAnimatedImageClass];
if (desiredImageClass && ![animatedImageClass isSubclassOfClass:desiredImageClass]) {
image = nil;
}
}
// 3. 若配置中表示只查询memory,则将当前获取流程完成的image返回
BOOL shouldQueryMemoryOnly = (queryCacheType == SDImageCacheTypeMemory) || (image && !(options & SDImageCacheQueryMemoryData));
if (shouldQueryMemoryOnly) {
if (doneBlock) {
doneBlock(image, nil, SDImageCacheTypeMemory);
}
return nil;
}
也就是说,如果只查询memoryCache,只会返回Image对象,不会返回data。
以下为查询diskCache部分
若image不为空且指定queryMemoryDataSync,或image为空且指定queryDiskDataSync,则表示应该同步查询磁盘缓存,否则异步。
同步与异步执行流程相同。
objectivec
// 1. 从磁盘获取data数据
NSData *diskData = [self.diskCache dataForKey:key];
若上面memoryCache中获取到的image非空,则配合data一起返回(不管diskData有没有)
若从磁盘中获取的diskData非空
- 若contetx中包含
storeCacheType
,值为cacheTypeAll
/cacheTypeMemory
,打标shouldCacheToMomery
为true
获取context中的imageThumbnailPixelSize
(缩略图尺寸)
若缩略图尺寸的width和height都大于0,则shouldCacheToMomery
置为false。
**因为最终返回的是缩略图,不应该写到full-size图片的key的缓存中!!! **
- 根据上面对storeCacheType和imageThumbnailPixelSize的处理,加上笔者偶然给context加上imageThumbnailPixelSize之后就不崩溃了,可以猜测是因为磁盘的频繁读写处理不当导致内存激增而崩溃
- 特殊情况:如果用户在list中查询相同URL的图像,为了避免多次解码并将相同的图像对象写入磁盘缓存,会再次查询并检查内存缓存。(没看太懂)
objectivec
// Special case: If user query image in list for the same URL, to avoid decode and write same image object into disk cache multiple times, we query and check memory cache here again.
if (shouldCacheToMomery && self.config.shouldCacheImagesInMemory) {
diskImage = [self.memoryCache objectForKey:key];
}
- 若上面获取的diskImage为空,将diskData解析为
UIImage
对象并解档
objectivec
UIImage *image = SDImageCacheDecodeImageData(data,
key,
[[self class] imageOptionsFromCacheOptions:options],
context);
[self _unarchiveObjectWithImage:image forKey:key];
// 存入内存
if (shouldCacheToMomery && diskImage && self.config.shouldCacheImagesInMemory) {
NSUInteger cost = diskImage.sd_memoryCost;
[self.memoryCache setObject:diskImage forKey:key cost:cost];
}
然后将解析的图片配合data返回。
拿到上述queryImageForKey
流程的结果后
-
若被取消,抛出错误;
-
若未命中缓存,调用
context
中的cacheKeyFilter
,获取到查询键(也就是自定义的查询键与SDWebImage自己生成的查询键不同的话)并调用callOriginalCacheProcessForOperation
查询原始缓存
objectivec
NSString *originKey = [self originalCacheKeyForURL:url context:context];
BOOL mayInOriginalCache = ![key isEqualToString:originKey];
if (mayInOriginalCache) {
[self callOriginalCacheProcessForOperation:operation
url:url
options:options
context:context
progress:progressBlock
completed:completedBlock];
return;
}
- 否则不管前面获取的
cachedImage
是否为空,都走到callDownloadProcessForOperation
(为什么?)
- 提问:为什么命中了缓存还是要执行到下载流程
SDWebImageManeger - callDownloadProcessForOperation
综上所述该方法调用有两个时机
-
若
optinons
中包含fromLoaderOnly
-
在缓存查询流程中未获取 到
cachedImage
-
从
context
中获取imageLoader
,若无则为默认单例SDWebImageDownloader
-
一系列判断,检查是否需要从网络下载图片
-
若不需要下载,且
cachedImage
为空,直接返回 -
若不需要下载,且
cachedImage
不为空,将图片返回 -
以下为需要下载的流程(此时不关心
cachedImage
是否为空)
objectivec
// 1. 若cachedImage不为空,且options中包含refreshCached,表示需要刷新缓存
if (cachedImage && options & SDWebImageRefreshCached) {
// 先将获取到的cachedImage抛出,然后继续执行下载流程。
// 将缓存的图像传递给loader。loader应该检查远程图像是否与缓存图像相等。(确保速度)
···
}
具体执行的方法为SDWebImageDownloader
的requestImageWithURL
获取到SDWebImageDownloader
的requestImageWithURL
的结果后
-
若被取消,返回错误
-
若cachedImage不为空,且指定要刷新缓存,但远端指定不修改缓存的图像
例如HTTP响应304代码,不处理(不处理???那不是回调出不去了)
objectivec
if (cachedImage &&
options & SDWebImageRefreshCached
&& [error.domain isEqualToString:SDWebImageErrorDomain]
&& error.code == SDWebImageErrorCacheNotModified) {}
-
若请求被取消或报错,抛出错误(不抛出cachedImage)
-
执行转换流程
objectivec
[self callTransformProcessForOperation:operation
url:url
options:options
context:context
originalImage:downloadedImage
originalData:downloadedData
cacheType:SDImageCacheTypeNone
finished:finished
completed:completedBlock];
SDWebImageDownloader - callTransformProcessForOperation
什么情况下需要转换呢,看源码可知:
-
downloadedImage
不为空 -
context
包含imageTransformer
-
downloadImage
不是动图,也不是矢量图
判断条件如下所示
objectivec
id<SDImageTransformer> transformer = context[SDWebImageContextImageTransformer];
if ([transformer isEqual:NSNull.null]) {
transformer = nil;
}
// transformer check
BOOL shouldTransformImage = originalImage && transformer;
shouldTransformImage = shouldTransformImage && (!originalImage.sd_isAnimated || (options & SDWebImageTransformAnimatedImage));
shouldTransformImage = shouldTransformImage && (!originalImage.sd_isVector || (options & SDWebImageTransformVectorImage));
下一步要继续判断,下载下来的图是否是缩略图,若为缩略图也不存储
objectivec
// thumbnail check
BOOL isThumbnail = originalImage.sd_isThumbnail;
NSData *cacheData = originalData;
UIImage *cacheImage = originalImage;
if (isThumbnail) {
cacheData = nil; // thumbnail don't store full size data
originalImage = nil; // thumbnail don't have full size image
}
若需要转换,调用transformer
的transformedImageWithImage
方法,执行存储流程
若不需要转换,则走到存储流程,将下载到的原始图片存储
SDWebImageDownloader - callStoreOriginCacheProcessForOperation
objectivec
// 1. 若context中包含originalStoreCacheType表示存储类型,则为该类型。否则默认磁盘存储
SDImageCacheType originalStoreCacheType = SDImageCacheTypeDisk;
if (context[SDWebImageContextOriginalStoreCacheType]) {
originalStoreCacheType = [context[SDWebImageContextOriginalStoreCacheType] integerValue];
}
// 如果originalStoreCacheType是disk,因为我们不需要再次存储原始数据,从originalStoreCacheType中剥离disk
if (cacheType == SDImageCacheTypeDisk) {
if (originalStoreCacheType == SDImageCacheTypeDisk) // 若为磁盘,置为None
originalStoreCacheType = SDImageCacheTypeNone;
if (originalStoreCacheType == SDImageCacheTypeAll) // 若为All,置为Memory
originalStoreCacheType = SDImageCacheTypeMemory;
}
// 2. 从context中获取cacheSerializer,使用自定义序列化者,否则不进行额外序列化处理
id<SDWebImageCacheSerializer> cacheSerializer = context[SDWebImageContextCacheSerializer];
// 3. 执行存储
// 3.1 encodedDataWithImage
NSData *encodedData =
[[SDImageCodersManager sharedManager] encodedDataWithImage:image
format:format
options:context[SDWebImageContextImageEncodeOptions]];
// 3.2 memoryCache setObject + _storeImageDataToDisk
[self.memoryCache setObject:image forKey:key cost:cost];
[self _storeImageDataToDisk:encodedData forKey:key];
// 3.3 归档
[self _archivedDataWithImage:image
forKey:key];
至此所有流程都已结束,将结果抛出。
解决方案
-
传入图片质量参数,在原图url后面拼上webP的参数,降低原图大小
-
在sd_setImage的context参数中加上imageThumbnailPixelSize,传入图片当前在屏幕中的尺寸scale4倍(保证显示清晰)
objectivec
// quality: (0, 100]
func _setWebPImage(_ originUrl: URL?, imageSize: CGSize?, quality: MyWebPImageQuality = .origin) {
var holder = UIImage(named: "loading_placeholder")
if let imageSize = imageSize {
holder = holder?.placeImage(in: imageSize)
}
guard let originUrl = originUrl else {
image = holder
return
}
var finalQuality = max(1, quality.rawValue)
finalQuality = min(100, quality.rawValue)
var urlString = originUrl.absoluteString
// 原始质量 不对url处理
guard finalQuality < 100 else {
sd_setImage(with: originUrl, placeholderImage: holder)
return
}
let formatPrefix = "?x-oss-process=image/format,webp/quality,q_"
if let range = urlString.range(of: formatPrefix) {
urlString = String(urlString[urlString.startIndex..<range.lowerBound])
}
var context: [SDWebImageContextOption: Any]?
if quality.rawValue <= 10,
let imageSize = imageSize {
let imageThumbnailPixelSize = CGSizeMake(imageSize.width * 8, imageSize.height * 8)
context = [.imageThumbnailPixelSize : imageThumbnailPixelSize]
}
urlString.append("\(formatPrefix)\(finalQuality)")
sd_setImage(with: URL(string: urlString),
placeholderImage: holder,
context: context)
}
上面查看源码我们知道如果设置了imageThumbnailPixelSize,不会触发缓存的读&写。
待研究
-
shouldUseWeakMemoryCache
-
SDImageCacheDecodeImageData
-
SDWebImageCombinedOperation
封装的作用