前言
在iOS的图片加载框架中,SDWebImage占据了大半壁江山。它提供了UIImageView的一个分类,支持从网络中下载且缓存图片,并设置图片到对应的UIImageView控件或者UIButton控件。在项目中使用SDWebImage来管理图片加载相关操作可以极大地提高开发效率,让我们更加专注于业务逻辑实现。
SDWebImage简介
功能特性
- 提供了一个UIImageView的category用来加载网络图片并且对网络图片的缓存进行管理
- 采用异步方式来下载网络图片
- 采用异步方式,使用内存+磁盘来缓存网络图片,拥有自动的缓存过期处理机制
- 支持的图片格式包括 PNG,JPEG,GIF,Webp等
- 支持GIF动图(4.0 之前的动图效果并不是太好,4.0 以后基于 FLAnimatedImage加载动图)
- 支持后台图片解压缩处理
- 支持Arm64
- 同一个URL的图片不被重复下载
- 失效,虚假的URL不会被无限重试
- 耗时操作都在子线程,确保不会阻塞主线程
- 使用GCD和ARC
官方图解
主序列图(Main Sequence Diagram)
顶层API图(Top Level API Diagram)
整体类图(Overall Class Diagram)
SDWebImage功能
缓存
为了减少网络流量的消耗,我们都希望下载下来的图片缓存到本地,下次再去获取同一张图片时,可以直接从本地获取,而不再从远程服务器获取。这样做的另一个好处是提升了用户体验,用户第二次查看同一幅图片时,能快速从本地获取图片直接呈现给用户,不用经过繁琐的网络请求。
SDWebImage
提供了对图片缓存的支持,而该功能是由SDImageCache 类来完成的。该类负责处理内存缓存及一个可选的磁盘缓存。其中磁盘缓存的写操作是异步的,这样就不会对 UI 操作造成影响。
SDWebImage
的图片缓存采用的是 Memory(内存)
和 Disk(硬盘)
双重 Cache 机制,SDImageCache
中有一个叫做 memCache
的属性,它是一个 NSCache
对象,用于实现我们对图片的 Memory Cache内存缓存
,其实就是接受系统的内存警告通知,然后清除掉自身的图片缓存。对于 Disk Cache磁盘缓存
,SDWebImage
会将图片存放到 NSCachesDirectory
目录中,然后为每一个缓存文件生成一个 md5 文件名,存放到文件中。
查找缓存机制如下:
-
Memory(内存)中查找:
SDImageCache
类的queryDiskCacheForKey
方法用于查询图片缓存。queryDiskCacheForKey
方法先会查询 Memory Cache ,如果查找到就直接返回,反之进入下面的硬盘查找。 -
Disk(磁盘) 中查找:如果 Memory Cache 查找不到, 就会查询 Disk Cache。就是如果 Disk Cache 查询成功,会把得到的图片再次设置到 Memory Cache 中, 以便最大化那些高频率展现图片的效率。如果找不到就进入下面的网络下载。
-
网络下载:
imageDownloader
属性负责请求网络,下载图片数据。 如果下载失败,会把失败的图片地址写入failedURLs
集合。这个集合用于存放所有加载失败图片的URL,用于SDWebImage
的一个机制:对上次加载失败的图片拒绝再次加载。 也就是说,一张图片在本次会话加载失败了,如果再次加载就会直接拒绝,这样提高了图片加载的性能。如果下载图片成功了,接下来就会使用[self.imageCache storeImage]
方法将它写入缓存 ,同时也会写入硬盘,并且调用completedBlock
告诉前端显示图片。 -
Disk(硬盘)缓存清理策略:
SDWebImage
会在每次 APP 结束的时候执行清理任务。 清理缓存的规则分两步进行: 第一步先清除掉过期的缓存文件。 如果清除掉过期的缓存之后,空间还不够,那么执行第二步:按文件时间从早到晚排序,先清除最早的缓存文件,直到剩余空间达到要求。
内存缓存
内存缓存的处理是使用 NSCache
对象来实现的。NSCache
是一个类似于集合的容器。它存储 key-value 对,这一点类似于 NSDictionary 类。
我们通常用使用缓存来临时存储短时间使用但创建昂贵的对象。重用这些对象可以优化性能,因为它们的值不需要重新计算。另外一方面,这些对象对于程序来说不是紧要的,在内存紧张时会被丢弃,所以有Disk(硬盘)缓存清理策略。
磁盘缓存
SDWebImage
使用 NSFileManager
对象来实现磁盘缓存,图片存储的位置位于Cache文件夹。另外,SDImageCache
还定义了一个串行队列,来异步存储图片。
SDImageCache
提供了大量方法来缓存、获取、移除及清空图片。而对于每个图片,为了方便地在内存或磁盘中对它进行这些操作,我们需要一个 key
值来索引它。 在内存中,我们将其作为 NSCache
的 key
值,而在磁盘中,我们用这个 key
作为图片的文件名。 对于一个远程服务器下载的图片,其 url
理所当然作为这个 key
值。
组织架构
核心类
SDWebImageDownloader
: 负责维持图片的下载队列,是一个单例对象SDWebImageDownloaderOperation
: 负责真正的图片下载请求,一个自定义的并行Operation子类SDImageCache
: 负责SDWebImage
的缓存工作,是一个单例对象SDWebImageManager
: 是总的管理类,维护了一个SDWebImageDownloader
实例和一个SDImageCache
实例,是下载与缓存的桥梁SDWebImageDecoder
: 负责图片的解压缩SDWebImagePrefetcher
: 负责图片的预取UIImageView+WebCache
: 和其他的扩展都是与用户直接打交道的
概念框架
UIImageView+WebCache
和UIButton+WebCache
直接为表层的UIKit框架提供接口SDWebImageManger
负责处理和协调SDWebImageDownloader
和SDWebImageCache
,并与 UIKit层进行交互SDWebImageDownloaderOperation
真正执行下载请求,最底层的两个类为高层抽象提供支持
源码解读
从使用开始说UIImageView+WebCache
- 引用
#import "UIImageView+WebCache.h"
头文件 - 加载图片地址
UIImageView *imageView = [[UIImageView alloc] initWithFrame:self.view.bounds];
[self.view addSubview:imageView];
[imageView sd_setImageWithURL:[NSURL URLWithString:@"http://www.xxx.com/image.jpg"]];
- SDWebImage也提供了下列其他的加载方法:
- (void)sd_setImageWithURL:(nullable NSURL *)url {
[self sd_setImageWithURL:url placeholderImage:nil options:0 progress:nil completed:nil];
}
- (void)sd_setImageWithURL:(nullable NSURL *)url placeholderImage:(nullable UIImage *)placeholder {
[self sd_setImageWithURL:url placeholderImage:placeholder options:0 progress:nil completed:nil];
}
- (void)sd_setImageWithURL:(nullable NSURL *)url placeholderImage:(nullable UIImage *)placeholder options:(SDWebImageOptions)options {
[self sd_setImageWithURL:url placeholderImage:placeholder options:options progress:nil completed:nil];
}
- (void)sd_setImageWithURL:(nullable NSURL *)url placeholderImage:(nullable UIImage *)placeholder options:(SDWebImageOptions)options context:(nullable SDWebImageContext *)context {
[self sd_setImageWithURL:url placeholderImage:placeholder options:options context:context progress:nil completed:nil];
}
- (void)sd_setImageWithURL:(nullable NSURL *)url completed:(nullable SDExternalCompletionBlock)completedBlock {
[self sd_setImageWithURL:url placeholderImage:nil options:0 progress:nil completed:completedBlock];
}
- (void)sd_setImageWithURL:(nullable NSURL *)url placeholderImage:(nullable UIImage *)placeholder completed:(nullable SDExternalCompletionBlock)completedBlock {
[self sd_setImageWithURL:url placeholderImage:placeholder options:0 progress:nil completed:completedBlock];
}
- (void)sd_setImageWithURL:(nullable NSURL *)url placeholderImage:(nullable UIImage *)placeholder options:(SDWebImageOptions)options completed:(nullable SDExternalCompletionBlock)completedBlock {
[self sd_setImageWithURL:url placeholderImage:placeholder options:options progress:nil completed:completedBlock];
}
- (void)sd_setImageWithURL:(nullable NSURL *)url placeholderImage:(nullable UIImage *)placeholder options:(SDWebImageOptions)options progress:(nullable SDImageLoaderProgressBlock)progressBlock completed:(nullable SDExternalCompletionBlock)completedBlock {
[self sd_setImageWithURL:url placeholderImage:placeholder options:options context:nil progress:progressBlock completed:completedBlock];
}
- 这些最终都是调用一个全能方法。全能方法除了必需的的图片地址,还提供了占位图、可选项、加载进度和完成回调。
- (void)sd_setImageWithURL:(nullable NSURL *)url
placeholderImage:(nullable UIImage *)placeholder
options:(SDWebImageOptions)options
progress:(nullable SDWebImageDownloaderProgressBlock)progressBlock
completed:(nullable SDExternalCompletionBlock)completedBlock;
- 全能方法的实现:
- (void)sd_setImageWithURL:(nullable NSURL *)url
placeholderImage:(nullable UIImage *)placeholder
options:(SDWebImageOptions)options
progress:(nullable SDWebImageDownloaderProgressBlock)progressBlock
completed:(nullable SDExternalCompletionBlock)completedBlock {
[self sd_setImageWithURL:url
placeholderImage:placeholder
options:options
context:nil
progress:progressBlock
completed:completedBlock];
}
- 可以发现,全能方法并没有什么实际的实现,还是对另一个方法的封装,这个方法在另一个分类
UIView+WebCache
中,看看实现:
- (void)sd_setImageWithURL:(nullable NSURL *)url
placeholderImage:(nullable UIImage *)placeholder
options:(SDWebImageOptions)options
context:(nullable SDWebImageContext *)context
progress:(nullable SDImageLoaderProgressBlock)progressBlock
completed:(nullable SDExternalCompletionBlock)completedBlock {
[self sd_internalSetImageWithURL:url
placeholderImage:placeholder
options:options
context:context
setImageBlock:nil
progress:progressBlock
completed:^(UIImage * _Nullable image, NSData * _Nullable data, NSError * _Nullable error, SDImageCacheType cacheType, BOOL finished, NSURL * _Nullable imageURL) {
if (completedBlock) {
completedBlock(image, error, cacheType, imageURL);
}
}];
}
- 这个方法也是调用了一个全能方法,下面是其核心实现:
- (void)sd_internalSetImageWithURL:(nullable NSURL *)url
placeholderImage:(nullable UIImage *)placeholder
options:(SDWebImageOptions)options
context:(nullable SDWebImageContext *)context
setImageBlock:(nullable SDSetImageBlock)setImageBlock
progress:(nullable SDImageLoaderProgressBlock)progressBlock
completed:(nullable SDInternalCompletionBlock)completedBlock {
if (context) {
// copy to avoid mutable object
// 复制以避免可变对象
context = [context copy];
} else {
context = [NSDictionary dictionary];
}
// 生成一个有效的操作密钥
NSString *validOperationKey = context[SDWebImageContextSetImageOperationKey];
// 如果传入了参数就用传入的,否则就用当前类的类名
if (!validOperationKey) {
// pass through the operation key to downstream, which can used for tracing operation or image view class
// 通过操作键传递到下游,可用于跟踪操作或图像视图类
validOperationKey = NSStringFromClass([self class]);
SDWebImageMutableContext *mutableContext = [context mutableCopy];
mutableContext[SDWebImageContextSetImageOperationKey] = validOperationKey;
context = [mutableContext copy];
}
self.sd_latestOperationKey = validOperationKey;
[self sd_cancelImageLoadOperationWithKey:validOperationKey];
self.sd_imageURL = url;
// 如果没有选择延迟加载占位图
if (!(options & SDWebImageDelayPlaceholder)) {
// 在主线程主队列中设置占位图
dispatch_main_async_safe(^{
// 作为图片下载完成之前的替代图片
[self sd_setImage:placeholder imageData:nil basedOnClassOrViaCustomSetImageBlock:setImageBlock cacheType:SDImageCacheTypeNone imageURL:url];
});
}
// 如果传入了图片链接
if (url) {
// reset the progress
// 重置进度
NSProgress *imageProgress = objc_getAssociatedObject(self, @selector(sd_imageProgress));
// 获取图像加载进度
if (imageProgress) {
// 初始化图片加载进度
imageProgress.totalUnitCount = 0;
imageProgress.completedUnitCount = 0;
}
#if SD_UIKIT || SD_MAC
// check and start image indicator
// 是否显示进度条(小菊花)
[self sd_startImageIndicator];
id<SDWebImageIndicator> imageIndicator = self.sd_imageIndicator;
#endif
// 生成图片管理者对象,如果context中有就用context中的
SDWebImageManager *manager;
SDWebImageManager *manager = context[SDWebImageContextCustomManager];
// 否则就直接生成SDWebImageManager单例对象
if (!manager) {
manager = [SDWebImageManager sharedManager];
} else {
// remove this manager to avoid retain cycle (manger -> loader -> operation -> context -> manager)
// 删除此管理器以避免保留周期(管理器 -> 加载程序 -> 操作 -> 上下文 -> 管理器)
SDWebImageMutableContext *mutableContext = [context mutableCopy];
mutableContext[SDWebImageContextCustomManager] = nil;
context = [mutableContext copy];
}
// 生成一个代码块用来在下载图片的方法中监听进度并进行回调
SDImageLoaderProgressBlock combinedProgressBlock = ^(NSInteger receivedSize, NSInteger expectedSize, NSURL * _Nullable targetURL) {
if (imageProgress) {
imageProgress.totalUnitCount = expectedSize;
imageProgress.completedUnitCount = receivedSize;
}
#if SD_UIKIT || SD_MAC
if ([imageIndicator respondsToSelector:@selector(updateIndicatorProgress:)]) {
double progress = 0;
if (expectedSize != 0) {
progress = (double)receivedSize / expectedSize;
}
progress = MAX(MIN(progress, 1), 0); // 0.0 - 1.0
dispatch_async(dispatch_get_main_queue(), ^{
[imageIndicator updateIndicatorProgress:progress];
});
}
#endif
if (progressBlock) {
progressBlock(receivedSize, expectedSize, targetURL);
}
};
@weakify(self);
// 生成图片操作对象,并开始下载图片
id <SDWebImageOperation> operation = [manager loadImageWithURL:url options:options context:context progress:combinedProgressBlock completed:^(UIImage *image, NSData *data, NSError *error, SDImageCacheType cacheType, BOOL finished, NSURL *imageURL) {
@strongify(self);
if (!self) { return; }
// if the progress not been updated, mark it to complete state
// 如果已经完成并且没有错误,并且进度没有更新,就将进度状态设为未知
if (imageProgress && finished && !error && imageProgress.totalUnitCount == 0 && imageProgress.completedUnitCount == 0) {
imageProgress.totalUnitCount = SDWebImageProgressUnitCountUnknown;
imageProgress.completedUnitCount = SDWebImageProgressUnitCountUnknown;
}
#if SD_UIKIT || SD_MAC
// check and stop image indicator
// 检查和停止图像指示器(如果有加载小菊花的话就移除掉)
if (finished) {
[self sd_stopImageIndicator];
}
#endif
// 是否应该回调完成block: 如果已经完成或者设置了在设置图片前处理
BOOL shouldCallCompletedBlock = finished || (options & SDWebImageAvoidAutoSetImage);
// 是否应该不设置图片: 如果有图片但设置了在设置图片前处理,或者没有图片并且没有设置延迟加载占位图
BOOL shouldNotSetImage = ((image && (options & SDWebImageAvoidAutoSetImage)) ||
(!image && !(options & SDWebImageDelayPlaceholder)));
// 生成完成回调代码块
SDWebImageNoParamsBlock callCompletedBlockClojure = ^{
// 如果没有生成强引用的self就终止执行
if (!self) { return; }
// 如果需要设置图片就直接刷新视图
if (!shouldNotSetImage) {
[self sd_setNeedsLayout];
}
// 如果传入了回调block并且应该进行回调,就直接回调
if (completedBlock && shouldCallCompletedBlock) {
completedBlock(image, data, error, cacheType, finished, url);
}
};
// 如果不需要设置图片就在主线程主队列中调用上面生成的完成回调代码块,并且不再向下执行
if (shouldNotSetImage) {
dispatch_main_async_safe(callCompletedBlockClojure);
return;
}
// 生成变量保存数据
UIImage *targetImage = nil;
NSData *targetData = nil;
if (image) {
// 如果图片下载成功就用变量保存图片
targetImage = image;
targetData = data;
} else if (options & SDWebImageDelayPlaceholder) {
// 如果图片下载失败并且设置了延迟加载占位图,就保存占位图
targetImage = placeholder;
targetData = nil;
}
#if SD_UIKIT || SD_MAC
SDWebImageTransition *transition = nil;
BOOL shouldUseTransition = NO;
if (options & SDWebImageForceTransition) {
// Always
shouldUseTransition = YES;
} else if (cacheType == SDImageCacheTypeNone) {
// From network
shouldUseTransition = YES;
} else {
// From disk (and, user don't use sync query)
if (cacheType == SDImageCacheTypeMemory) {
shouldUseTransition = NO;
} else if (cacheType == SDImageCacheTypeDisk) {
// SDWebImageQueryMemoryDataSync:
// 默认情况下,当您仅指定“SDWebImageQueryMemoryData”时,我们会异步查询内存映像数据。
// 将此掩码也组合在一起,以同步查询内存图像数据
// 不建议同步查询数据,除非您要确保在同一 runloop 中加载映像以避免在单元重用期间闪烁。
// SDWebImageQueryDiskDataSync:
// 默认情况下,当内存缓存未命中时,我们会异步查询磁盘缓存。此掩码可以强制同步查询磁盘缓存(当内存缓存未命中时)。
// 这两个选项打开则NO。
if (options & SDWebImageQueryMemoryDataSync || options & SDWebImageQueryDiskDataSync) {
shouldUseTransition = NO;
} else {
shouldUseTransition = YES;
}
} else {
// Not valid cache type, fallback
shouldUseTransition = NO;
}
}
// 检查一下是否应该转换图片:如果下载完成,并且设置了图片强制转换或者图片缓存类型是不缓存直接从网络加载,就进行强制转换
if (finished && shouldUseTransition) {
transition = self.sd_imageTransition;
}
#endif
dispatch_main_async_safe(^{
#if SD_UIKIT || SD_MAC
// 在主线程主队列中设置图片
[self sd_setImage:targetImage imageData:targetData basedOnClassOrViaCustomSetImageBlock:setImageBlock transition:transition cacheType:cacheType imageURL:imageURL];
#else
// 如果用户没有设置调度组,就直接在主线程主队列中设置图片和调用完成回调代码块
[self sd_setImage:targetImage imageData:targetData basedOnClassOrViaCustomSetImageBlock:setImageBlock cacheType:cacheType imageURL:imageURL];
#endif
callCompletedBlockClojure();
});
}];
// 根据密钥保存下载图片的操作
[self sd_setImageLoadOperation:operation forKey:validOperationKey];
} else {
#if SD_UIKIT || SD_MAC
// 如果没传入图片链接,就在主线程主队列移除加载小菊花
[self sd_stopImageIndicator];
#endif
dispatch_main_async_safe(^{
// 如果传入了完成回调block就回调错误信息
if (completedBlock) {
NSError *error = [NSError errorWithDomain:SDWebImageErrorDomain code:SDWebImageErrorInvalidURL userInfo:@{NSLocalizedDescriptionKey : @"Image url is nil"}];
completedBlock(nil, nil, error, SDImageCacheTypeNone, YES, url);
}
});
}
}
整个过程看起来繁琐复杂。下面,我们讨论一下这个方法中的核心步骤。
取消当前正在进行的异步下载
取消当前正在进行的异步下载,确保每个UIImageView
对象中永远只存在一个operation
,当前只允许一个图片网络请求,该operation
负责从缓存中获取image或者是重新下载image。
// 生成一个有效的操作密钥
NSString *validOperationKey = context[SDWebImageContextSetImageOperationKey];
// 如果传入了参数就用传入的,否则就用当前类的类名
if (!validOperationKey) {
validOperationKey = NSStringFromClass([self class]);
SDWebImageMutableContext *mutableContext = [context mutableCopy];
mutableContext[SDWebImageContextSetImageOperationKey] = validOperationKey;
context = [mutableContext copy];
}
// 取消先前下载的任务
[self sd_cancelImageLoadOperationWithKey:validOperationKey];
... // 下载图片操作
// 将生成的加载操作赋值给UIView的自定义属性
[self sd_setImageLoadOperation:operation forKey:validOperationKey];
上述方法定义在UIView+WebCacheOperation
类中:
- (void)sd_cancelImageLoadOperationWithKey:(nullable NSString *)key {
if (key) {
// Cancel in progress downloader from queue
// 从队列中取消正在进行的下载程序
// 获取添加在UIView的自定义属性
SDOperationsDictionary *operationDictionary = [self sd_operationDictionary];
id<SDWebImageOperation> operation;
@synchronized (self) {
operation = [operationDictionary objectForKey:key];
}
if (operation) {
// 实现了SDWebImageOperation的协议
if ([operation conformsToProtocol:@protocol(SDWebImageOperation)]) {
[operation cancel];
}
@synchronized (self) {
[operationDictionary removeObjectForKey:key];
}
}
}
}
- (void)sd_setImageLoadOperation:(nullable id<SDWebImageOperation>)operation forKey:(nullable NSString *)key {
if (key) {
// 如果之前已经有过该图片的下载操作,则取消之前的图片下载操作
[self sd_cancelImageLoadOperationWithKey:key];
if (operation) {
SDOperationsDictionary *operationDictionary = [self sd_operationDictionary];
@synchronized (self) {
[operationDictionary setObject:operation forKey:key];
}
}
}
}
实际上,所有的操作都是由一个实际上,所有的操作都是由一个operationDictionary
字典维护的,执行新的操作之前,cancel所有的operation
。
占位图策略
作为图片下载完成之前的替代图片。dispatch_main_async_safe
是一个宏,保证在主线程安全执行。
if (!(options & SDWebImageDelayPlaceholder)) {
// 在主线程主队列中设置占位图
dispatch_main_async_safe(^{
// 设置占位图
[self sd_setImage:placeholder imageData:nil basedOnClassOrViaCustomSetImageBlock:setImageBlock];
});
}
判断url是否合法
如果url合法,则进行图片下载操作,否则直接block回调失败。
if (url) {
// 下载图片操作
} else {
dispatch_main_async_safe(^{
#if SD_UIKIT
[self sd_removeActivityIndicator];
#endif
if (completedBlock) {
NSError *error = [NSError errorWithDomain:SDWebImageErrorDomain code:-1 userInfo:@{NSLocalizedDescriptionKey : @"Trying to load a nil url"}];
completedBlock(nil, error, SDImageCacheTypeNone, url);
}
});
}
下载图片操作
下载图片的操作是由SDWebImageManager
完成的,它是一个单例。
- (id <SDWebImageOperation>)loadImageWithURL:(nullable NSURL *)url
options:(SDWebImageOptions)options
progress:(nullable SDWebImageDownloaderProgressBlock)progressBlock
completed:(nullable SDInternalCompletionBlock)completedBlock;
下载完成之后刷新UIImageView
的图片。
// 根据枚举类型,判断是否需要设置图片
shouldCallCompletedBlock = finished || (options & SDWebImageAvoidAutoSetImage);
BOOL shouldNotSetImage = ((image && (options & SDWebImageAvoidAutoSetImage)) ||
(!image && !(options & SDWebImageDelayPlaceholder)));
SDWebImageNoParamsBlock callCompletedBlockClojure = ^{
if (!sself) { return; }
if (!shouldNotSetImage) {
// 设置图片
[sself sd_setNeedsLayout];
}
if (completedBlock && shouldCallCompletedBlock) {
completedBlock(image, error, cacheType, url);
}
};
// 不要自动设置图片,则调用block传入image对象
if (shouldNotSetImage) {
dispatch_main_async_safe(callCompletedBlockClojure);
return;
}
// 设置图片操作
dispatch_main_async_safe(^{
#if SD_UIKIT || SD_MAC
[sself sd_setImage:targetImage imageData:targetData basedOnClassOrViaCustomSetImageBlock:setImageBlock transition:transition cacheType:cacheType imageURL:imageURL];
#else
[sself sd_setImage:targetImage imageData:targetData basedOnClassOrViaCustomSetImageBlock:setImageBlock];
#endif
callCompletedBlockClojure();
});
最后,把返回的 operation
添加到operationDictionary
中,方便后续的cancel。
// 将生成的加载操作赋值给UIView的自定义属性
[self sd_setImageLoadOperation:operation forKey:validOperationKey];