前言
这是本系列的第 3 篇,在前一篇中,我们了解了 SDWebImage 执行的基本流程,本篇就来介绍第一个核心类 SDWebImageMananger
。
正文
SDWebImageMananger.h 文件基本可以分为 3 各部分:
①定义了一个枚举 SDWebImageOptions
,列举了可能会用到的一些场景。
typedef NS_OPTIONS(NSUInteger, SDWebImageOptions) {
// 重试已经失败的 url
SDWebImageRetryFailed = 1 << 0,
// 低优先级,比如,在有 UI 交互的情况下,会延迟下载操作
SDWebImageLowPriority = 1 << 1,
// 下载完成后,仅做内存缓存,不做磁盘缓存
SDWebImageCacheMemoryOnly = 1 << 2,
// 下载过程中逐步加载图片,而不是完全下载完之后才展示
SDWebImageProgressiveDownload = 1 << 3,
// 刷新缓存
SDWebImageRefreshCached = 1 << 4,
// 当 App 进入后台时,继续下载任务,如果后台任务超时,操作将被自动取消
SDWebImageContinueInBackground = 1 << 5,
// 允许处理 Cookie
SDWebImageHandleCookies = 1 << 6,
// 允许不受信任的 SSL 证书,生产环境慎用
SDWebImageAllowInvalidSSLCertificates = 1 << 7,
// 高优先级,即会把相应的图片放到最前边加载,而不是按照加入队列时的顺序执行
SDWebImageHighPriority = 1 << 8,
// 延迟 placeholder 的加载,即在下载完成时才加载
SDWebImageDelayPlaceholder = 1 << 9,
// 对动图也执行 transform 操作
SDWebImageTransformAnimatedImage = 1 << 10,
// 图片下载完成后,不直接自动给 imageView 赋值,给用户调整图片的机会
SDWebImageAvoidAutoSetImage = 1 << 11,
// 依据设备内存缩放图片,如果设置了 `SDWebImageProgressiveDownload` ,此设置无效
SDWebImageScaleDownLargeImages = 1 << 12,
// 在有内存缓存的情况下,依然需要查询磁盘缓存,建议与 SDWebImageQueryDiskSync 配合使用
SDWebImageQueryDataWhenInMemory = 1 << 13,
// 同步查询磁盘缓存
SDWebImageQueryDiskSync = 1 << 14,
// 仅加载缓存图片
SDWebImageFromCacheOnly = 1 << 15,
// 对内存和磁盘中的 image 也执行 transition 的操作
SDWebImageForceTransition = 1 << 16
};
②定义了一个协议 SDWebImageManagerDelegate
,这里提供了以下 3 个协议方法:
// 缓存中没有指定图片时,是否需要下载
- (BOOL)imageManager:(nonnull SDWebImageManager *)imageManager shouldDownloadImageForURL:(nullable NSURL *)imageURL;
// 是否需要将制定 URL 标记为失败的 URL
- (BOOL)imageManager:(nonnull SDWebImageManager *)imageManager shouldBlockFailedURL:(nonnull NSURL *)imageURL withError:(nonnull NSError *)error;
// 允许在刚刚下载到 image 并且未做缓存之前,对图片执行 transform,返回处理后的 image
- (nullable UIImage *)imageManager:(nonnull SDWebImageManager *)imageManager transformDownloadedImage:(nullable UIImage *)image withURL:(nullable NSURL *)imageURL;
③SDWebImageManager 的头文件,有几个重要属性,他们的作用见下边的注释。
// 代理对象
@property (weak, nonatomic, nullable) id delegate;
// 处理缓存的对象
@property (strong, nonatomic, readonly, nullable) SDImageCache *imageCache;
// 处理下载工作的对象
@property (strong, nonatomic, readonly, nullable) SDWebImageDownloader *imageDownloader;
// 一个用户定义的 block,用于生成 cacheKey
@property (nonatomic, copy, nullable) SDWebImageCacheKeyFilterBlock cacheKeyFilter;
// 一个用户定义的 block,用于序列化下载到的数据
@property (nonatomic, copy, nullable) SDWebImageCacheSerializerBlock cacheSerializer;
下面是 2 个常用的创建方法:+ (nonnull instancetype)sharedManager;
和 - (nonnull instancetype)initWithCache:(nonnull SDImageCache *)cache downloader:(nonnull SDWebImageDownloader *)downloader;
,其实最终都是调用了后者 。
// 单例
+ (nonnull instancetype)sharedManager {
static dispatch_once_t once;
static id instance;
dispatch_once(&once, ^{
instance = [self new];
});
return instance;
}
// 初始化,创建处理缓存和下载任务的对象 cache 和 downloader
- (nonnull instancetype)init {
SDImageCache *cache = [SDImageCache sharedImageCache];
SDWebImageDownloader *downloader = [SDWebImageDownloader sharedDownloader];
return [self initWithCache:cache downloader:downloader];
}
// 核心的初始化方法,为各属性赋初值
- (nonnull instancetype)initWithCache:(nonnull SDImageCache *)cache downloader:(nonnull SDWebImageDownloader *)downloader {
if ((self = [super init])) {
// 处理缓存和下载任务的对象
_imageCache = cache;
_imageDownloader = downloader;
// 用于存储请求失败的 URL 的集合及操作时用的锁 (信号量)
_failedURLs = [NSMutableSet new];
_failedURLsLock = dispatch_semaphore_create(1);
// 存储运行中 operation 的集合,通过判断他的 count 是否为 0,判断操作是否在进行中:BOOL isRunning = (self.runningOperations.count > 0);
_runningOperations = [NSMutableSet new];
// 操作时用的锁 (信号量)
_runningOperationsLock = dispatch_semaphore_create(1);
}
return self;
}
另外几个方法,就不单独介绍了,用到的时候再继续讨论。此处,我们只看一个核心方法 - (nullable id
,下面我们来一步步讨论这个方法的具体实现。
1.校验参数
依次做如下处理:如果传入的 completedBlock 为空,就直接报错;如果传入的参数是 NSString * 类型的,需要将其转换成 NSURL;最后,如果 url 还不是 NSURL 类型,那就只能将其置为 nil,以免造成后边 Crash。
NSAssert(completedBlock != nil, @"If you mean to prefetch the image, use -[SDWebImagePrefetcher prefetchURLs] instead");
if ([url isKindOfClass:NSString.class]) {
url = [NSURL URLWithString:(NSString *)url];
}
if (![url isKindOfClass:NSURL.class]) {
url = nil;
}
2.生成总的 operation
他是 SDWebImageCombinedOperation
实例对象,也是当前方法要返回的结果,并将当前类赋值给 operation 的一个 weak 属性(避免循环引用)。
SDWebImageCombinedOperation *operation = [SDWebImageCombinedOperation new];
operation.manager = self; // 肯定是 weak 属性
SDWebImageCombinedOperation
的声明与实现文件均在当前类 SDWebImageManager
的实现文件里边,简单看一下他的 .h/.m 文件吧。
// SDWebImageCombinedOperation.h
@interface SDWebImageCombinedOperation : NSObject
// 标识是否已取消
@property (assign, nonatomic, getter = isCanceled) BOOL cancelled;
// downloadToken 这是一个继承自 NSObject 的类,他有一个继承自 NSOperation 的属性,也就是真正执行下载操作时的 operation,cancel 时会用到
@property (strong, nonatomic, nullable) SDWebImageDownloadToken *downloadToken;
// 查询缓存时的 operation,用于标识当前 operation 是否已经被取消。其实查询缓存时,首先查看 operation.isCanceled,如果没被取消了,就会再去查询了。cancel 时会将其 isCanceled 属性置为 YES。
@property (strong, nonatomic, nullable) NSOperation *cacheOperation;
// manager
@property (weak, nonatomic, nullable) SDWebImageManager *manager;
@end
// SDWebImageCombinedOperation.m
#pragma mark - 代理方法实现
@implementation SDWebImageCombinedOperation
- (void)cancel {
@synchronized(self) {
self.cancelled = YES;
// 取消查询缓存的 Operation,此时 isCanceled 会被置为 YES。
if (self.cacheOperation) {
[self.cacheOperation cancel];
self.cacheOperation = nil;
}
// 取消下载操作
if (self.downloadToken) {
[self.manager.imageDownloader cancel:self.downloadToken];
}
// 将当前 operation 从 manager 中运行着的 operation 数组中移除。
[self.manager safelyRemoveOperationFromRunning:self];
}
}
@end
// SDWebImageOperation 协议的定义
@protocol SDWebImageOperation
- (void)cancel;
@end
可以看到,SDWebImageCombinedOperation
这个类的主要作用就是 cancel 操作,包括 cancel 查询缓存 和 cancel 下载数据。
3.再次检测一下 url
如果是曾经失败的 url,而且不允许重试,或者 url 为空时,执行 completionBlock,并返回当前 operation。
// self.failedURLs 是一个保存曾经失败过的 URL 的数组,用于检测当前 URL 是不是曾经请求失败过的URL.另外,搜索一个个元素的时候,NSSet 比 NSArray 查询更快。
BOOL isFailedUrl = NO;
if (url) {
LOCK(self.failedURLsLock);
isFailedUrl = [self.failedURLs containsObject:url];
UNLOCK(self.failedURLsLock);
}
// 若出现以下两种情况就不再往下走了,直接执行 CompletionBlock:① URL 是空的;② 此 URL 是曾经请求失败的 URL,并且规定不允许重新请求曾经失败的 URL。
if (url.absoluteString.length == 0
|| (!(options & SDWebImageRetryFailed) && isFailedUrl))
{
[self callCompletionBlockForOperation:operation
completion:completedBlock
error:[NSError errorWithDomain:NSURLErrorDomain code:NSURLErrorFileDoesNotExist userInfo:nil]
url:url];
return operation;
}
4.保存 operation
至 self.runningOperations
后者是一个数组,这里使用了信号量来确保线程安装。
LOCK(self.runningOperationsLock);
[self.runningOperations addObject:operation];
UNLOCK(self.runningOperationsLock);
5.查询缓存。
NSString *key = [self cacheKeyForURL:url];
SDImageCacheOptions cacheOptions = 0;
if (options & SDWebImageQueryDataWhenInMemory) cacheOptions |= SDImageCacheQueryDataWhenInMemory;
if (options & SDWebImageQueryDiskSync) cacheOptions |= SDImageCacheQueryDiskSync;
if (options & SDWebImageScaleDownLargeImages) cacheOptions |= SDImageCacheScaleDownLargeImages;
__weak SDWebImageCombinedOperation *weakOperation = operation;
operation.cacheOperation = [self.imageCache queryCacheOperationForKey:key
options:cacheOptions
done:^(UIImage *cachedImage, NSData *cachedData, SDImageCacheType cacheType)
{
// 查询完成后的操作在这里,可能查到了,也可能没查到...
}
这里先准备了 2 个参数,查询的依据 key 和一些条件 cacheOptions。key 的获取是通过一个私有方法 (如下),如果自定义了 key
的生成规则 self.cacheKeyFilter,就用自定义的,如果没有,就直接取 url.absoluteString。cacheOptions
是一个用 NS_OPTIONS 定义的枚举类型 (前边已介绍过),可组合多种情况,在这里综合了 2 个查询的要求和 1 个缩放图片的要求。
- (nullable NSString *)cacheKeyForURL:(nullable NSURL *)url {
if (!url) {
return @"";
}
if (self.cacheKeyFilter) {
return self.cacheKeyFilter(url);
} else {
return url.absoluteString;
}
}
6.查询的具体过程
详情将会在 SDImageCache 中介绍,下面讨论一下查询缓存结束后的操作。
7.移除当前 operation
从 self.runningOperations 这个数组中移除当前 operation。
if (!strongOperation || strongOperation.isCancelled) {
[self safelyRemoveOperationFromRunning:strongOperation];
return;
}
8.判断是否需要下载
当同时满足 3 个要求时,就需要下载新数据了:
①没要求只能从缓存获取数据,即当缓存找不到时,可以去下载;
②找不到缓存 或 要求必须更新缓存;
③当 self.delegate 没有遵守协议, 或者 协议方法返回 YES。
BOOL shouldDownload = (!(options & SDWebImageFromCacheOnly))
&& (!cachedImage || options & SDWebImageRefreshCached)
&& (![self.delegate respondsToSelector:@selector(imageManager:shouldDownloadImageForURL:)] || [self.delegate imageManager:self shouldDownloadImageForURL:url]);
9.若需要下载
- 首先依然要做一个判断,即 如果有缓存数据并且要求刷新缓存数据时,需要先调用一次 CompletionBlock,将缓存数据返回去,然后再开始下载新数据,代码如下:
if (cachedImage && options & SDWebImageRefreshCached) {
[self callCompletionBlockForOperation:strongOperation completion:completedBlock image:cachedImage data:cachedData error:nil cacheType:cacheType finished:YES url:url];
}
然后,准备下载数据时所需的一些基本选项,可以参考篇头介绍的枚举 SDWebImageOptions
。
- 开始下载,调用了 SDWebImageDownloader 的下载方法,留待 SDWebImageDownloader 介绍,这里只讨论下载完成之后的操作。
__weak typeof(strongOperation) weakSubOperation = strongOperation;
strongOperation.downloadToken = [self.imageDownloader downloadImageWithURL:url
options:downloaderOptions
progress:progressBlock
completed:^(UIImage *downloadedImage, NSData *downloadedData, NSError *error, BOOL finished)
{
// 下载完成后的操作...
}
下载完成后,可以分这么几种情况:
a.当前 operation 已经被取消,这种情况下不作任何操作,包括回调。
b.下载出错,先将失败的 error 信息返回,然后决定是否需要将当前 URL 存入失败的 URL 数组。
c.下载成功,此时要做的工作还有许多:
①如果设置了失败重发,则将当前 URL 从失败的 URL 数组中移除。
if ((options & SDWebImageRetryFailed)) {
LOCK(self.failedURLsLock);
[self.failedURLs removeObject:url];
UNLOCK(self.failedURLsLock);
}
②对于自定义的 manager,需要执行另外一套缩放标准。
if (self != [SDWebImageManager sharedManager]
&& self.cacheKeyFilter
&& downloadedImage)
{
downloadedImage = [self scaledImageForKey:key image:downloadedImage];
}
③若需要更新缓存,但是未下载到图片,且缓存中本来有值的情况下,什么也不做,因为下载之前早已经缓存数据返回了。
if (options & SDWebImageRefreshCached && cachedImage && !downloadedImage) {
// 需要更新缓存,但是未下载到图片,且缓存中本来有值的情况下,什么也不做,因为下载之前早已经缓存数据返回了
}
④如果下载到了图片,并且要求 transform 图片的情况下,异步执行 transform 和缓存图片的工作,然后回到主线程执行 completionBlock。
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0), ^{
UIImage *transformedImage = [self.delegate imageManager:self
transformDownloadedImage:downloadedImage
withURL:url];
if (transformedImage && finished) {
BOOL imageWasTransformed = ![transformedImage isEqual:downloadedImage];
NSData *cacheData;
// pass nil if the image was transformed, so we can recalculate the data from the image
if (self.cacheSerializer) {
cacheData = self.cacheSerializer(transformedImage, (imageWasTransformed ? nil : downloadedData), url);
} else {
cacheData = (imageWasTransformed ? nil : downloadedData);
}
// *** 存盘:注意是存的 imageData
[self.imageCache storeImage:transformedImage
imageData:cacheData
forKey:key
toDisk:cacheOnDisk
completion:nil];
}
[self callCompletionBlockForOperation:strongSubOperation
completion:completedBlock
image:transformedImage
data:downloadedData
error:nil
cacheType:SDImageCacheTypeNone
finished:finished
url:url];
});
⑤如果下载到了图片,并且下载完成的话,则存盘并执行 completionBlock。存盘调用了 SDImageCache
的方法,随后介绍。
最后将当前 operation 移除。
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0), ^{
UIImage *transformedImage = [self.delegate imageManager:self
transformDownloadedImage:downloadedImage
withURL:url];
if (transformedImage && finished) {
BOOL imageWasTransformed = ![transformedImage isEqual:downloadedImage];
NSData *cacheData;
// pass nil if the image was transformed, so we can recalculate the data from the image
if (self.cacheSerializer) {
cacheData = self.cacheSerializer(transformedImage, (imageWasTransformed ? nil : downloadedData), url);
} else {
cacheData = (imageWasTransformed ? nil : downloadedData);
}
// *** 存盘:注意是存的 imageData
[self.imageCache storeImage:transformedImage
imageData:cacheData
forKey:key
toDisk:cacheOnDisk
completion:nil];
}
[self callCompletionBlockForOperation:strongSubOperation
completion:completedBlock
image:transformedImage
data:downloadedData
error:nil
cacheType:SDImageCacheTypeNone
finished:finished
url:url];
});
// ...
if (finished) {
[self safelyRemoveOperationFromRunning:strongSubOperation];
}
10.若不需要下载,并且有缓存
此时,执行 completionBlock 将缓存数据返回,然后移除当前 operation。
[self callCompletionBlockForOperation:strongOperation
completion:completedBlock
image:cachedImage
data:cachedData
error:nil
cacheType:cacheType
finished:YES
url:url];
[self safelyRemoveOperationFromRunning:strongOperation];
11.其它,即没有缓存,且不需要下载
和上边的操作类似,只不过传回的 image 和 data 均为 nil。
[self callCompletionBlockForOperation:strongOperation
completion:completedBlock
image:nil
data:nil
error:nil
cacheType:SDImageCacheTypeNone
finished:YES
url:url];
[self safelyRemoveOperationFromRunning:strongOperation];
最后将 operation 返回。
小结
以上就是 SDWebImageManager 这个类的主要功能,其中关于缓存和下载的内容,详见后边几篇的讨论。