前言
在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