SDWebImage主线梳理(一)
本文只梳理SDWebImage主线相关的内容,所谓的主线就是从UIImageView
调用sd_setImageWithURL:
方法开始,梳理这一条主线上的相关内容。
本文使用的版本是 pod 'SDWebImage', '~> 5.3.1'
。
先看一下 SDWebImage 在 GitHub 上给出的时序图,SDWebImage的整体调用顺序就如下图所示:
![](https://img.haomeiwen.com/i2177502/32bfc4fedffa8707.png)
由于SDWebImage架构体系复杂,因此会由浅入深分成几部分对主线内容进行解析。文章篇幅太长受限,不得不分割成两篇。第二篇传送门:SDWebImage主线梳理(二)
- 第一部分:基础源码的解析(主线正向流程)
- 第二部分:主线涉及的重要block的时间线梳理(主线逆向流程)
- 第三部分:本人在阅读源码时遇到的问题
- 第四部分:SDWebImage小知识点(纯属个人喜好)
- 另外,解码部分和缓存部分将会分别单独各写一篇文章进行解析
为了方便表述和定位,重点解析的方法都会先在源码的注释中标注序号,按照序号去找对应的解析即可。
第一部分:基础源码解析(主线正向流程)
主线正向流程(我自己起的名)指的是从一个UIImageView
调用sd_setImageWithURL:
命令开始,一直到SD开启了下载任务(网络请求尚未回调)为止,这一段的主要流程。
1-1
废话不多说,直接开始
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
[self.imageView sd_setImageWithURL:YourImageURL];
}
imageView调用 sd_setImageWithURL:
方法,开启SDWebImage之旅。YourImageURL
只要是个网络图片URL即可,随意指定。
1-2
来到-[UIImageView(WebCache) sd_setImageWithURL:]
- (void)sd_setImageWithURL:(nullable NSURL *)url {
[self sd_setImageWithURL:url placeholderImage:nil options:0 progress:nil completed:nil];
}
连锁调用,继续
1-3
来到-[UIImageView(WebCache) sd_setImageWithURL:placeholderImage:options:progress:completed:]
- (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];
}
继续
1-4 触发主线方法
来到-[UIImageView(WebCache) sd_setImageWithURL:placeholderImage:options:context:progress:completed:]
- (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);
}
}];
}
虽然还是连锁调用,但是有两点要注意:
- 这里实现了
sd_internalSetImageWithURL:...
方法的completedBlock, 这个block的类型是SDInternalCompletionBlock
,为了方便第二部分追踪block的调用顺序,我们给它起个名就叫 InternalBlock1。 -
InternalBlock1 的实现里调用了
-[UIImageView(WebCache) sd_setImageWithURL...]
方法的completedBlock,这个block是要回调给开发者的,所以不必深究。
1-5 主线方法
-[UIView(WebCache) sd_internalSetImageWithURL:placeholderImage:options:context:setImageBlock:progress:completed:]
约定一下block的简称:
- 方法中定义并实现了
combinedProgressBlock
,SDImageLoaderProgressBlock
类型。这个就是进度回调的block,以下简称 ProgressBlock。 - 方法中还实现了
loadImageWithURL:options:context:progress:completed:
方法的completedBlock,SDInternalCompletionBlock
类型。以下简称 InternalBlock2。
由于这个方法源码量稍有些多,所以最直接的解析内容先写在源码的注释中。细化的内容在后面按照编号顺序进行解析。
- (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 {
// 这个context是个nil,是在连锁调用中补上的一个参数
context = [context copy]; // copy to avoid mutable object
// context是nil肯定取不出值来,此处validOperationKey仍然是nil
NSString *validOperationKey = context[SDWebImageContextSetImageOperationKey];
if (!validOperationKey) {
// 相当于 validOperationKey = @"UIImageView"
validOperationKey = NSStringFromClass([self class]);
}
// sd_latestOperationKey是一个关联属性,关联属性的 key = @selector(sd_latestOperationKey); 详见 1-5-1
self.sd_latestOperationKey = validOperationKey;
// 取消所有队列中的下载任务; 详见 1-5-2
[self sd_cancelImageLoadOperationWithKey:validOperationKey];
// sd_imageURL也是一个关联属性,key = @selector(sd_imageURL); 详见 1-5-3
self.sd_imageURL = url;
// 设置占位图的枚举值; 详见1-5-4
if (!(options & SDWebImageDelayPlaceholder)) {
// dispatch_main_async_safe() 函数是SDWebImage自定义的,保证代码在主线程执行; 详见1-5-5
dispatch_main_async_safe(^{
// 为UIImageView设置图片(此处是设置占位图)
[self sd_setImage:placeholder imageData:nil basedOnClassOrViaCustomSetImageBlock:setImageBlock cacheType:SDImageCacheTypeNone imageURL:url];
});
}
// 一般情况url都是我们传的图片地址,不会空,所以主要看if分支,忽略else分支
if (url) {
// reset the progress
// 下载进度归零(或称重置、亦或复位)
NSProgress *imageProgress = objc_getAssociatedObject(self, @selector(sd_imageProgress));
if (imageProgress) {
imageProgress.totalUnitCount = 0;
imageProgress.completedUnitCount = 0;
}
// SD_UIKIT 是 SDWebImage 自定义的宏,如果当前系统是iOS或tvOS则为真
#if SD_UIKIT || SD_MAC
// check and start image indicator
// 大概是菊花转圈圈之类的,这个东西不多逼逼
[self sd_startImageIndicator];
id<SDWebImageIndicator> imageIndicator = self.sd_imageIndicator;
#endif
// 和前面的validOperationKey一样,取出来都是nil
SDWebImageManager *manager = context[SDWebImageContextCustomManager];
if (!manager) {
// 获取SDWebImageManager单例对象; 详见1-5-6
manager = [SDWebImageManager sharedManager];
}
// combinedProgressBlock 先保存一段代码暂不执行
SDImageLoaderProgressBlock combinedProgressBlock = ^(NSInteger receivedSize, NSInteger expectedSize, NSURL * _Nullable targetURL) {
if (imageProgress) {
// 首先是修改imageProgress的总数和已完成数
imageProgress.totalUnitCount = expectedSize;
imageProgress.completedUnitCount = receivedSize;
}
#if SD_UIKIT || SD_MAC
if ([imageIndicator respondsToSelector:@selector(updateIndicatorProgress:)]) {
// 利用 receivedSize 和 expectedSize 计算当前进度progress(0-1之间)
double progress = 0;
if (expectedSize != 0) {
progress = (double)receivedSize / expectedSize;
}
// MAX(MIN(progress, 1), 0) 这个写法很有意思,保证无论你输入什么值最后范围都在0-1之间
progress = MAX(MIN(progress, 1), 0); // 0.0 - 1.0
dispatch_async(dispatch_get_main_queue(), ^{
// 更新 imageIndicator 的状态
[imageIndicator updateIndicatorProgress:progress];
});
}
#endif
// 如果开发者需要(实现了block),把进度回调给开发者
if (progressBlock) {
progressBlock(receivedSize, expectedSize, targetURL);
}
};
@weakify(self);
// loadImageWithURL: 方法是主线方法,详见 1-5-7
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
// 如果finished为真,结束indicator
if (finished) {
[self sd_stopImageIndicator];
}
#endif
// 如果finished为真或者选项为SDWebImageAvoidAutoSetImage(避免自动设置图片),那么应该调用CompletedBlock
BOOL shouldCallCompletedBlock = finished || (options & SDWebImageAvoidAutoSetImage);
// 跟下面那段英文说的一个意思:有图(image)但是选项是SDWebImageAvoidAutoSetImage,
// 或者没图但是选项没选SDWebImageDelayPlaceholder(图片网络请求完成后再设置占位图),这两种情况都不该设置图片(主图和占位图都不设置)
BOOL shouldNotSetImage = ((image && (options & SDWebImageAvoidAutoSetImage)) ||
(!image && !(options & SDWebImageDelayPlaceholder)));
SDWebImageNoParamsBlock callCompletedBlockClojure = ^{
if (!self) { return; }
if (!shouldNotSetImage) {
[self sd_setNeedsLayout];
}
// 这个 completedBlock 就是前面 1-4 提到的 InternalBlock1
if (completedBlock && shouldCallCompletedBlock) {
completedBlock(image, data, error, cacheType, finished, url);
}
};
// case 1a: we got an image, but the SDWebImageAvoidAutoSetImage flag is set
// OR
// case 1b: we got no image and the SDWebImageDelayPlaceholder is not set
if (shouldNotSetImage) {
// 这里调用 callCompletedBlockClojure block,将会间接触发 InternalBlock1
dispatch_main_async_safe(callCompletedBlockClojure);
return;
}
UIImage *targetImage = nil;
NSData *targetData = nil;
if (image) {
// case 2a: we got an image and the SDWebImageAvoidAutoSetImage is not set
// 有图并且没选 SDWebImageAvoidAutoSetImage
targetImage = image;
targetData = data;
} else if (options & SDWebImageDelayPlaceholder) {
// case 2b: we got no image and the SDWebImageDelayPlaceholder flag is set
// 没图并且选了SDWebImageDelayPlaceholder,意味着在图片网络请求完成后再设置占位图,也就是现在这个回调中的这个时机
targetImage = placeholder;
targetData = nil;
}
#if SD_UIKIT || SD_MAC
// check whether we should use the image transition
// 关于形变的内容不展开解析
SDWebImageTransition *transition = nil;
if (finished && (options & SDWebImageForceTransition || cacheType == SDImageCacheTypeNone)) {
transition = self.sd_imageTransition;
}
#endif
dispatch_main_async_safe(^{
#if SD_UIKIT || SD_MAC
// 给imageView设置图片; 详见1-5-8
[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 block,将会间接触发 InternalBlock1
callCompletedBlockClojure();
});
}];
// 取消并删除旧的operation,保存新的operation(本方法中loadImageWithURL:方法返回的); 详见 1-5-9
[self sd_setImageLoadOperation:operation forKey:validOperationKey];
} else {
#if SD_UIKIT || SD_MAC
[self sd_stopImageIndicator];
#endif
dispatch_main_async_safe(^{
if (completedBlock) {
// 回调错误信息
NSError *error = [NSError errorWithDomain:SDWebImageErrorDomain code:SDWebImageErrorInvalidURL userInfo:@{NSLocalizedDescriptionKey : @"Image url is nil"}];
completedBlock(nil, nil, error, SDImageCacheTypeNone, YES, url);
}
});
}
}
1-5-1
sd_latestOperationKey
是分类中定义的一个关联属性,没有声明,只有GETTER和SETTER。
- (nullable NSString *)sd_latestOperationKey {
return objc_getAssociatedObject(self, @selector(sd_latestOperationKey));
}
- (void)setSd_latestOperationKey:(NSString * _Nullable)sd_latestOperationKey {
objc_setAssociatedObject(self, @selector(sd_latestOperationKey), sd_latestOperationKey, OBJC_ASSOCIATION_COPY_NONATOMIC);
}
1-5-2
-[UIView(WebCacheOperation) sd_cancelImageLoadOperationWithKey:]
取消所有队列中的下载任务
- (void)sd_cancelImageLoadOperationWithKey:(nullable NSString *)key {
if (key) {
// Cancel in progress downloader from queue
// 从sd_operationDictionary中获取操作字典; sd_operationDictionary是一个关联属性,详见1-5-2-1
SDOperationsDictionary *operationDictionary = [self sd_operationDictionary];
// 定义一个遵守SDWebImageOperation协议的指针
id<SDWebImageOperation> operation;
// 读操作增加线程锁
@synchronized (self) {
operation = [operationDictionary objectForKey:key];
}
if (operation) {
if ([operation conformsToProtocol:@protocol(SDWebImageOperation)]) {
// 取消操作
[operation cancel];
}
@synchronized (self) {
// 从操作字典中移除
[operationDictionary removeObjectForKey:key];
}
}
}
}
1-5-2-1
sd_operationDictionary
是分类中定义的一个关联属性,没有声明和SETTER,只有GETTER。
- (SDOperationsDictionary *)sd_operationDictionary {
// 全程加锁
@synchronized(self) {
// 获取操作字典,SDOperationsDictionary类型
SDOperationsDictionary *operations = objc_getAssociatedObject(self, &loadOperationKey);
// 之前保存过就直接返回
if (operations) {
return operations;
}
// 之前没保存过就新创建一个 NSMapTable,关于 NSMapTable 详见1-5-2-1-1
operations = [[NSMapTable alloc] initWithKeyOptions:NSPointerFunctionsStrongMemory valueOptions:NSPointerFunctionsWeakMemory capacity:0];
// 保存到关联属性
objc_setAssociatedObject(self, &loadOperationKey, operations, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
return operations;
}
}
值得一提的是这个关联属性是SDOperationsDictionary
类型。SDOperationsDictionary
类型是SD自定义(typedef)的类型。表面上看是一个字典(NSDictionary
),其实是NSMapTable
类型
// key is strong, value is weak because operation instance is retained by SDWebImageManager's runningOperations property
// we should use lock to keep thread-safe because these method may not be acessed from main queue
typedef NSMapTable<NSString *, id<SDWebImageOperation>> SDOperationsDictionary;
1-5-2-1-1
关于 NSMapTable
- 首先 NSMapTable 也是一种映射表。
- 相对 NSSet 和 NSDictionary 而言,NSMapTable 可以自由定义是否强引用key、value。而NSSet和NSDictionary默认是强引用key、value。
- NSMapTable 的 key、value更加自由,可以是对象到对象,也可以是C指针到对象等等
Q1: 为什么使用 NSMapTable 存储 operation<SDWebImageOperation> 实例 ?
A1: 在此例中,NSMapTable 强引用 key,弱引用 value。因为operation实例被SDWebImageManager的runningOperations属性持有了,我们应该使用lock来保持线程安全,这些方法可能不会从主队列访问。
1-5-3
sd_imageURL
虽然在分类的头文件中有声明,但是GETTER和SETTER都是使用关联属性进行的管理。说句题外话,由于 sd_imageURL
同时实现了GETTER和SETTER,系统就不会自动生成成员变量了。
/**
* Get the current image URL.
*
* @note Note that because of the limitations of categories this property can get out of sync if you use setImage: directly.
*/
@property (nonatomic, strong, readonly, nullable) NSURL *sd_imageURL;
- (nullable NSURL *)sd_imageURL {
return objc_getAssociatedObject(self, @selector(sd_imageURL));
}
- (void)setSd_imageURL:(NSURL * _Nullable)sd_imageURL {
objc_setAssociatedObject(self, @selector(sd_imageURL), sd_imageURL, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}
1-5-4
SDWebImageDelayPlaceholder
是 SDWebImageOptions 类型的枚举值
选项用途:图片网络请求或者从缓存加载完成后再设置占位图。实际上就是把占位图作为最后的希望,如果缓存查找失败并且网络下载也失败,那么会把占位图派上场,打替补。
typedef NS_OPTIONS(NSUInteger, SDWebImageOptions) {
...
/**
* By default, placeholder images are loaded while the image is loading. This flag will delay the loading
* of the placeholder image until after the image has finished loading.
* 默认情况下,占位图在图片加载(网络请求或从缓存加载)过程中就被加载(展示到imageView)了。这个选项将会延迟占
位图加载(展示到imageView),直到图片加载(网络请求)完成后。
*/
SDWebImageDelayPlaceholder = 1 << 8,
...
}
1-5-5
dispatch_main_async_safe()
函数是SDWebImage自定义的宏,保证代码在主线程执行。
- 如果当前队列是主队列(在主线程中),则直接回调block,也就是顺序执行代码,相当于
dispatch_main_async_safe()
什么都没做。 - 如果当前队列不是主队列(有可能是在子线程),则异步拉回主队列回调block,保证代码在主队列(主线程)执行。
#ifndef dispatch_main_async_safe
#define dispatch_main_async_safe(block)\
NSLog(@"%s", dispatch_queue_get_label(DISPATCH_CURRENT_QUEUE_LABEL));\
if (dispatch_queue_get_label(DISPATCH_CURRENT_QUEUE_LABEL) == dispatch_queue_get_label(dispatch_get_main_queue())) {\
NSLog(@"%s", dispatch_queue_get_label(DISPATCH_CURRENT_QUEUE_LABEL));\
block();\
} else {\
dispatch_async(dispatch_get_main_queue(), block);\
}
#endif
1-5-6 大量代码来袭
+[SDWebImageManager sharedManager]
获取 SDWebImageManager 单例对象
+ (nonnull instancetype)sharedManager {
static dispatch_once_t once;
static id instance;
dispatch_once(&once, ^{
instance = [self new]; // 相当于调用 init
});
return instance;
}
-[SDWebImageManager init]
名义上的 init,其实内部并没有初始化,还需要调用下一方法进行初始化。
- (nonnull instancetype)init {
// 取出默认图片缓存,默认为nil; defaultImageCache 是 SDWebImageManager 的类属性,详见1-5-6-1
id<SDImageCache> cache = [[self class] defaultImageCache];
if (!cache) {
// 没有 defaultImageCache 则使用 sharedImageCache; sharedImageCache 详见1-5-6-2
cache = [SDImageCache sharedImageCache];
}
// 取出默认图片加载器,默认为nil; defaultImageLoader 是 SDWebImageManager 的类属性,详见1-5-6-3
id<SDImageLoader> loader = [[self class] defaultImageLoader];
if (!loader) {
// 没有 defaultImageLoader 则使用 sharedDownloader; sharedDownloader 详见1-5-6-4
loader = [SDWebImageDownloader sharedDownloader];
}
// -[SDWebImageManager initWithCache:loader:]
return [self initWithCache:cache loader:loader];
}
-[SDWebImageManager initWithCache:loader:]
没什么可说的,就是各种保存。
- (nonnull instancetype)initWithCache:(nonnull id<SDImageCache>)cache loader:(nonnull id<SDImageLoader>)loader {
if ((self = [super init])) {
_imageCache = cache;
_imageLoader = loader;
_failedURLs = [NSMutableSet new];
_failedURLsLock = dispatch_semaphore_create(1);
_runningOperations = [NSMutableSet new];
_runningOperationsLock = dispatch_semaphore_create(1);
}
return self;
}
1-5-6-1
SDWebImageManager 的类属性 defaultImageCache,默认为nil,也就是默认使用 sharedImageCache 作为 image cache。
/**
The default image cache when the manager which is created with no arguments. Such as shared manager or init.
Defaults to nil. Means using `SDImageCache.sharedImageCache`
*/
@property (nonatomic, class, nullable) id<SDImageCache> defaultImageCache;
无论SETTER还是GETTER都是在操作全局静态变量 _defaultImageCache
,由于 defaultImageCache
是类属性,所以GETTER和SETTER都需要自己实现。
static id<SDImageCache> _defaultImageCache;
...
+ (id<SDImageCache>)defaultImageCache {
return _defaultImageCache;
}
+ (void)setDefaultImageCache:(id<SDImageCache>)defaultImageCache {
if (defaultImageCache && ![defaultImageCache conformsToProtocol:@protocol(SDImageCache)]) {
return;
}
_defaultImageCache = defaultImageCache;
}
1-5-6-2
+[SDImageCache sharedImageCache]
获取 SDImageCache 单例对象
+ (nonnull instancetype)sharedImageCache {
static dispatch_once_t once;
static id instance;
dispatch_once(&once, ^{
instance = [self new]; // 相当于调用 init
});
return instance;
}
-[SDImageCache init]
传入参数 namespace = @"default"
- (instancetype)init {
return [self initWithNamespace:@"default"];
}
-[SDImageCache initWithNamespace:]
- (nonnull instancetype)initWithNamespace:(nonnull NSString *)ns {
return [self initWithNamespace:ns diskCacheDirectory:nil];
}
-[SDImageCache initWithNamespace:diskCacheDirectory:]
- (nonnull instancetype)initWithNamespace:(nonnull NSString *)ns
diskCacheDirectory:(nullable NSString *)directory {
return [self initWithNamespace:ns diskCacheDirectory:directory config:SDImageCacheConfig.defaultCacheConfig];
}
-[SDImageCache initWithNamespace:diskCacheDirectory:config:]
- (nonnull instancetype)initWithNamespace:(nonnull NSString *)ns
diskCacheDirectory:(nullable NSString *)directory
config:(nullable SDImageCacheConfig *)config {
if ((self = [super init])) {
NSAssert(ns, @"Cache namespace should not be nil");
// Create IO serial queue
// 创建一个串行队列
_ioQueue = dispatch_queue_create("com.hackemist.SDImageCache", DISPATCH_QUEUE_SERIAL);
// 如果入参config为空,则取 SDImageCacheConfig 单例对象; 详见1-5-6-2-1
if (!config) {
config = SDImageCacheConfig.defaultCacheConfig;
}
_config = [config copy];
// Init the memory cache
NSAssert([config.memoryCacheClass conformsToProtocol:@protocol(SDMemoryCache)], @"Custom memory cache class must conform to `SDMemoryCache` protocol");
// 初始化一个内存缓存实例; 缓存部分会在另外一篇文章详细解析。
_memoryCache = [[config.memoryCacheClass alloc] initWithConfig:_config];
// Init the disk cache
// 默认 directory 为 nil
if (directory != nil) {
// directory如果不为空,拼接一个缓存文件路径
_diskCachePath = [directory stringByAppendingPathComponent:ns];
} else {
// 拼接一个缓存路径,userCacheDirectory 方法仅仅是获取系统的缓存文件夹路径;
// 这个 path 与 migrateDiskCacheDirectory 方法中的新路径规则一致
NSString *path = [[[self userCacheDirectory] stringByAppendingPathComponent:@"com.hackemist.SDImageCache"] stringByAppendingPathComponent:ns];
_diskCachePath = path;
}
NSAssert([config.diskCacheClass conformsToProtocol:@protocol(SDDiskCache)], @"Custom disk cache class must conform to `SDDiskCache` protocol");
// 初始化一个磁盘缓存实例; 缓存部分会在另外一篇文章详细解析。
_diskCache = [[config.diskCacheClass alloc] initWithCachePath:_diskCachePath config:_config];
// Check and migrate disk cache directory if need
// 把旧文件夹的缓存移动到新文件夹路径下; 详见1-5-6-2-2
[self migrateDiskCacheDirectory];
#if SD_UIKIT
// Subscribe to app events
[[NSNotificationCenter defaultCenter] addObserver:self
selector:@selector(applicationWillTerminate:)
name:UIApplicationWillTerminateNotification
object:nil];
[[NSNotificationCenter defaultCenter] addObserver:self
selector:@selector(applicationDidEnterBackground:)
name:UIApplicationDidEnterBackgroundNotification
object:nil];
#endif
// 这个分支不用看
#if SD_MAC
[[NSNotificationCenter defaultCenter] addObserver:self
selector:@selector(applicationWillTerminate:)
name:NSApplicationWillTerminateNotification
object:nil];
#endif
}
return self;
}
1-5-6-2-1
+[SDImageCacheConfig defaultCacheConfig]
获取 SDImageCacheConfig 单例对象
+ (SDImageCacheConfig *)defaultCacheConfig {
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
_defaultCacheConfig = [SDImageCacheConfig new]; // 调用 init
});
return _defaultCacheConfig;
}
-[SDImageCacheConfig init]
- (instancetype)init {
if (self = [super init]) {
_shouldDisableiCloud = YES; // 禁用iCloud
_shouldCacheImagesInMemory = YES; // 在内存中缓存图片
_shouldUseWeakMemoryCache = YES; // 使用弱内存缓存
_shouldRemoveExpiredDataWhenEnterBackground = YES; // App进入后台清除过期数据
_diskCacheReadingOptions = 0;
_diskCacheWritingOptions = NSDataWritingAtomic;
_maxDiskAge = kDefaultCacheMaxDiskAge; // 磁盘缓存最大过期时间,默认为7天
_maxDiskSize = 0; // 磁盘缓存可占用的最大空间大小,0 即为不限制
_diskCacheExpireType = SDImageCacheConfigExpireTypeModificationDate; // 过期时间类型
_memoryCacheClass = [SDMemoryCache class];
_diskCacheClass = [SDDiskCache class];
}
return self;
}
1-5-6-2-2
-[SDImageCache migrateDiskCacheDirectory]
把旧文件夹的缓存移动到新文件夹路径下
- (void)migrateDiskCacheDirectory {
if ([self.diskCache isKindOfClass:[SDDiskCache class]]) {
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
// 使用模拟器时新路径长这样:~/Library/Caches/com.hackemist.SDImageCache/default/
NSString *newDefaultPath = [[[self userCacheDirectory] stringByAppendingPathComponent:@"com.hackemist.SDImageCache"] stringByAppendingPathComponent:@"default"];
// 使用模拟器时旧路径长这样:~/Library/Caches/default/com.hackemist.SDWebImageCache.default/
NSString *oldDefaultPath = [[[self userCacheDirectory] stringByAppendingPathComponent:@"default"] stringByAppendingPathComponent:@"com.hackemist.SDWebImageCache.default"];
dispatch_async(self.ioQueue, ^{
[((SDDiskCache *)self.diskCache) moveCacheDirectoryFromPath:oldDefaultPath toPath:newDefaultPath];
});
});
}
}
Q: 为什么要移动?
A: 没有在官方的文档中找到解释。个人猜测,应该是为了兼容旧版本的SD的缓存路径。如果哪位大牛知道请不吝留言赐教!
1-5-6-3
SDWebImageManager 的类属性 defaultImageLoader,默认为nil,也就是默认使用 sharedDownloader 作为 image loader。
/**
The default image loader for manager which is created with no arguments. Such as shared manager or init.
Defaults to nil. Means using `SDWebImageDownloader.sharedDownloader`
*/
@property (nonatomic, class, nullable) id<SDImageLoader> defaultImageLoader;
无论SETTER还是GETTER都是在操作全局静态变量 _defaultImageLoader
,由于 defaultImageLoader
是类属性,所以GETTER和SETTER都需要自己实现。
static id<SDImageLoader> _defaultImageLoader;
...
+ (id<SDImageLoader>)defaultImageLoader {
return _defaultImageLoader;
}
+ (void)setDefaultImageLoader:(id<SDImageLoader>)defaultImageLoader {
if (defaultImageLoader && ![defaultImageLoader conformsToProtocol:@protocol(SDImageLoader)]) {
return;
}
_defaultImageLoader = defaultImageLoader;
}
1-5-6-4
+[SDWebImageDownloader sharedDownloader]
获取 SDWebImageDownloader 单例对象
+ (nonnull instancetype)sharedDownloader {
static dispatch_once_t once;
static id instance;
dispatch_once(&once, ^{
instance = [self new]; // 调用init
});
return instance;
}
-[SDWebImageDownloader init]
- (nonnull instancetype)init {
return [self initWithConfig:SDWebImageDownloaderConfig.defaultDownloaderConfig];
}
-[SDWebImageDownloader initWithConfig:]
- (instancetype)initWithConfig:(SDWebImageDownloaderConfig *)config {
self = [super init];
if (self) {
// 入参 config 在 -[SDWebImageDownloader init] 方法中就被赋值 SDWebImageDownloaderConfig.defaultDownloaderConfig
if (!config) {
config = SDWebImageDownloaderConfig.defaultDownloaderConfig;
}
_config = [config copy];
[_config addObserver:self forKeyPath:NSStringFromSelector(@selector(maxConcurrentDownloads)) options:0 context:SDWebImageDownloaderContext];
// 创建一个 NSOperationQueue
_downloadQueue = [NSOperationQueue new];
// 最大并发数,默认为 6
_downloadQueue.maxConcurrentOperationCount = _config.maxConcurrentDownloads;
_downloadQueue.name = @"com.hackemist.SDWebImageDownloader";
_URLOperations = [NSMutableDictionary new];
NSMutableDictionary<NSString *, NSString *> *headerDictionary = [NSMutableDictionary dictionary];
NSString *userAgent = nil;
#if SD_UIKIT
// User-Agent Header; see http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.43
// 按照标准配置 User-Agent
userAgent = [NSString stringWithFormat:@"%@/%@ (%@; iOS %@; Scale/%0.2f)", [[NSBundle mainBundle] infoDictionary][(__bridge NSString *)kCFBundleExecutableKey] ?: [[NSBundle mainBundle] infoDictionary][(__bridge NSString *)kCFBundleIdentifierKey], [[NSBundle mainBundle] infoDictionary][@"CFBundleShortVersionString"] ?: [[NSBundle mainBundle] infoDictionary][(__bridge NSString *)kCFBundleVersionKey], [[UIDevice currentDevice] model], [[UIDevice currentDevice] systemVersion], [[UIScreen mainScreen] scale]];
#elif SD_WATCH
// User-Agent Header; see http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.43
userAgent = [NSString stringWithFormat:@"%@/%@ (%@; watchOS %@; Scale/%0.2f)", [[NSBundle mainBundle] infoDictionary][(__bridge NSString *)kCFBundleExecutableKey] ?: [[NSBundle mainBundle] infoDictionary][(__bridge NSString *)kCFBundleIdentifierKey], [[NSBundle mainBundle] infoDictionary][@"CFBundleShortVersionString"] ?: [[NSBundle mainBundle] infoDictionary][(__bridge NSString *)kCFBundleVersionKey], [[WKInterfaceDevice currentDevice] model], [[WKInterfaceDevice currentDevice] systemVersion], [[WKInterfaceDevice currentDevice] screenScale]];
#elif SD_MAC
userAgent = [NSString stringWithFormat:@"%@/%@ (Mac OS X %@)", [[NSBundle mainBundle] infoDictionary][(__bridge NSString *)kCFBundleExecutableKey] ?: [[NSBundle mainBundle] infoDictionary][(__bridge NSString *)kCFBundleIdentifierKey], [[NSBundle mainBundle] infoDictionary][@"CFBundleShortVersionString"] ?: [[NSBundle mainBundle] infoDictionary][(__bridge NSString *)kCFBundleVersionKey], [[NSProcessInfo processInfo] operatingSystemVersionString]];
#endif
if (userAgent) {
// 如果userAgent不能被转换成ASCII编码格式,处理userAgent为可变字符串。
if (![userAgent canBeConvertedToEncoding:NSASCIIStringEncoding]) {
NSMutableString *mutableUserAgent = [userAgent mutableCopy];
if (CFStringTransform((__bridge CFMutableStringRef)(mutableUserAgent), NULL, (__bridge CFStringRef)@"Any-Latin; Latin-ASCII; [:^ASCII:] Remove", false)) {
userAgent = mutableUserAgent;
}
}
headerDictionary[@"User-Agent"] = userAgent;
}
headerDictionary[@"Accept"] = @"image/*,*/*;q=0.8";
// headerDictionary 中现在有两个字段:"User-Agent" 以及 "Accept"
_HTTPHeaders = headerDictionary;
_HTTPHeadersLock = dispatch_semaphore_create(1);
_operationsLock = dispatch_semaphore_create(1);
NSURLSessionConfiguration *sessionConfiguration = _config.sessionConfiguration;
if (!sessionConfiguration) {
sessionConfiguration = [NSURLSessionConfiguration defaultSessionConfiguration];
}
/**
* Create the session for this task
* We send nil as delegate queue so that the session creates a serial operation queue for performing all delegate
* method calls and completion handler calls.
*/
// 创建 NSURLSession ,代理是 SDWebImageDownloader 单例对象(自己)
_session = [NSURLSession sessionWithConfiguration:sessionConfiguration
delegate:self
delegateQueue:nil];
}
return self;
}
1-5-7 主线方法-加载图片
-[SDWebImageManager loadImageWithURL:options:context:progress:completed:]
- (SDWebImageCombinedOperation *)loadImageWithURL:(nullable NSURL *)url
options:(SDWebImageOptions)options
context:(nullable SDWebImageContext *)context
progress:(nullable SDImageLoaderProgressBlock)progressBlock
completed:(nonnull SDInternalCompletionBlock)completedBlock {
// Invoking this method without a completedBlock is pointless
// 断言:必须传入 completedBlock(必须实现了此block),调用此方法不传completedBlock是无意义的
NSAssert(completedBlock != nil, @"If you mean to prefetch the image, use -[SDWebImagePrefetcher prefetchURLs] instead");
// Very common mistake is to send the URL using NSString object instead of NSURL. For some strange reason, Xcode won't
// throw any warning for this type mismatch. Here we failsafe this error by allowing URLs to be passed as NSString.
// 处理入参 url,如果传入的是 NSString 则转换成 NSURL
if ([url isKindOfClass:NSString.class]) {
url = [NSURL URLWithString:(NSString *)url];
}
// Prevents app crashing on argument type error like sending NSNull instead of NSURL
// 再次处理入参 url,如果传入的 url 不是 NSURL 则将 url 置为空
if (![url isKindOfClass:NSURL.class]) {
url = nil;
}
// 创建 SDWebImageCombinedOperation 实例
SDWebImageCombinedOperation *operation = [SDWebImageCombinedOperation new];
// manager 是 operation 的私有属性,所有权修饰符为 weak,弱引用
operation.manager = self;
BOOL isFailedUrl = NO;
if (url) {
// 使用信号量加锁
SD_LOCK(self.failedURLsLock);
// 如果 failedURLs(NSSet) 包含此 url,则临时变量 isFailedUrl 将被赋值 YES
isFailedUrl = [self.failedURLs containsObject:url];
SD_UNLOCK(self.failedURLsLock);
}
// 如果url字符长度为零,或者options不是失败重试(SDWebImageRetryFailed)并且这个url失败过
// SDWebImageRetryFailed 是一个枚举值,详见1-5-7-1
if (url.absoluteString.length == 0 || (!(options & SDWebImageRetryFailed) && isFailedUrl)) {
// 触发 InternalBlock2 回调错误信息,详见1-5-7-2
[self callCompletionBlockForOperation:operation completion:completedBlock error:[NSError errorWithDomain:SDWebImageErrorDomain code:SDWebImageErrorInvalidURL userInfo:@{NSLocalizedDescriptionKey : @"Image url is nil"}] url:url];
return operation;
}
// 使用信号量加锁
SD_LOCK(self.runningOperationsLock);
// SDWebImageManager 的属性 runningOperations(NSMutableSet) 持有 operation,对比 1-5-2-1-1
[self.runningOperations addObject:operation];
SD_UNLOCK(self.runningOperationsLock);
// Preprocess the options and context arg to decide the final the result for manager
// 对options和context参数进行预处理,以决定最后的结果; 详见1-5-7-3
SDWebImageOptionsResult *result = [self processedResultForURL:url options:options context:context];
// Start the entry to load image from cache
// 主线方法! 处理缓存相关业务,此处传递给 callCacheProcessForOperation 方法的 completedBlock 是 InternalBlock2,详见1-5-7-4
[self callCacheProcessForOperation:operation url:url options:result.options context:result.context progress:progressBlock completed:completedBlock];
return operation;
}
1-5-7-1
SDWebImageRetryFailed
与 1-5-4 中的 SDWebImageDelayPlaceholder
是同一类型枚举值,都定义在 SDWebImageOptions 中。
默认情况下,当一个URL下载失败后,这个URL将被列入黑名单不再重试。这个选项可以禁用这个黑名单。
/// WebCache options
typedef NS_OPTIONS(NSUInteger, SDWebImageOptions) {
/**
* By default, when a URL fail to be downloaded, the URL is blacklisted so the library won't keep trying.
* This flag disable this blacklisting.
*/
SDWebImageRetryFailed = 1 << 0,
...
}
1-5-7-2
-[SDWebImageManager callCompletionBlockForOperation:completion:error:url:]
连锁调用
- (void)callCompletionBlockForOperation:(nullable SDWebImageCombinedOperation*)operation
completion:(nullable SDInternalCompletionBlock)completionBlock
error:(nullable NSError *)error
url:(nullable NSURL *)url {
[self callCompletionBlockForOperation:operation completion:completionBlock image:nil data:nil error:error cacheType:SDImageCacheTypeNone finished:YES url:url];
}
-[SDWebImageManager callCompletionBlockForOperation:completion:image:data:error:cacheType:finished:url:]
方法的入参 completionBlock 是 SDInternalCompletionBlock 类型,也就是我们前面简称 InternalBlock2 的block。
- (void)callCompletionBlockForOperation:(nullable SDWebImageCombinedOperation*)operation
completion:(nullable SDInternalCompletionBlock)completionBlock
image:(nullable UIImage *)image
data:(nullable NSData *)data
error:(nullable NSError *)error
cacheType:(SDImageCacheType)cacheType
finished:(BOOL)finished
url:(nullable NSURL *)url {
// dispatch_main_async_safe() 函数在前面 1-5-5 介绍过
dispatch_main_async_safe(^{
if (completionBlock) {
// 触发 completionBlock,也就是 InternalBlock2
completionBlock(image, data, error, cacheType, finished, url);
}
});
}
1-5-7-3
-[SDWebImageManager processedResultForURL:options:context:]
代码虽然多,但是逻辑很简单,总结如下:
- 初始化 mutableContext (就是个可变字典)
- 如果 mutableContext 中没有 transformer、cacheKeyFilter、cacheSerializer,mutableContext 会把 SDWebImageManager (即self) 的拿过来保存一份
- 将 context 追加到 mutableContext 中,再将新的 mutableContext 的内容 copy 一份赋值给入参 context
- 初始化一个 SDWebImageOptionsResult 实例并返回
- (SDWebImageOptionsResult *)processedResultForURL:(NSURL *)url options:(SDWebImageOptions)options context:(SDWebImageContext *)context {
SDWebImageOptionsResult *result;
SDWebImageMutableContext *mutableContext = [SDWebImageMutableContext dictionary];
// Image Transformer from manager
if (!context[SDWebImageContextImageTransformer]) {
id<SDImageTransformer> transformer = self.transformer;
[mutableContext setValue:transformer forKey:SDWebImageContextImageTransformer];
}
// Cache key filter from manager
if (!context[SDWebImageContextCacheKeyFilter]) {
id<SDWebImageCacheKeyFilter> cacheKeyFilter = self.cacheKeyFilter;
[mutableContext setValue:cacheKeyFilter forKey:SDWebImageContextCacheKeyFilter];
}
// Cache serializer from manager
if (!context[SDWebImageContextCacheSerializer]) {
id<SDWebImageCacheSerializer> cacheSerializer = self.cacheSerializer;
[mutableContext setValue:cacheSerializer forKey:SDWebImageContextCacheSerializer];
}
if (mutableContext.count > 0) {
if (context) {
[mutableContext addEntriesFromDictionary:context];
}
context = [mutableContext copy];
}
// Apply options processor
// 默认为空
if (self.optionsProcessor) {
result = [self.optionsProcessor processedResultForURL:url options:options context:context];
}
// 初始化一个 SDWebImageOptionsResult 实例
if (!result) {
// Use default options result
// 调用 initWithOptions:context: 方法
result = [[SDWebImageOptionsResult alloc] initWithOptions:options context:context];
}
return result;
}
-[SDWebImageOptionsResult initWithOptions:context:]
保存 options 和 context,初始化一个 SDWebImageOptionsResult 实例
- (instancetype)initWithOptions:(SDWebImageOptions)options context:(SDWebImageContext *)context {
self = [super init];
if (self) {
self.options = options;
self.context = context;
}
return self;
}
1-5-7-4 主线方法
-[SDWebImageManager callCacheProcessForOperation:url:options:context:progress:completed:]
-
约定一下block简称:
- 这里实现了
queryImageForKey:options:context:completion:
方法的completionBlock,SDImageCacheQueryCompletionBlock
类型。为了方便,以下简称此block为 doneBlock
- 这里实现了
-
主要是在处理缓存和下载相关业务
- 首先去查找缓存,在 1-5-7-4-3-1 会有查找结果,无论是否查找到缓存,结束后都会调用 doneBlock 回到这里。
- 1-5-7-4-3-1 调用 doneBlock 回传image和data,在 doneBlock 的实现代码中调用 callDownloadProcessForOperation 方法进入下载业务模块(1-5-7-4-5)。
// Query cache process
- (void)callCacheProcessForOperation:(nonnull SDWebImageCombinedOperation *)operation
url:(nonnull NSURL *)url
options:(SDWebImageOptions)options
context:(nullable SDWebImageContext *)context
progress:(nullable SDImageLoaderProgressBlock)progressBlock
completed:(nullable SDInternalCompletionBlock)completedBlock {
// Check whether we should query cache
// shouldQueryCache 是否应该去查找缓存,如果没有选择 SDWebImageFromLoaderOnly,则为 YES
// SDWebImageFromLoaderOnly 是一个枚举值,详见1-5-7-4-1
BOOL shouldQueryCache = !SD_OPTIONS_CONTAINS(options, SDWebImageFromLoaderOnly);
if (shouldQueryCache) {
// 默认 context 中没有 cacheKeyFilter
id<SDWebImageCacheKeyFilter> cacheKeyFilter = context[SDWebImageContextCacheKeyFilter];
// cacheKeyForURL:cacheKeyFilter: 方法返回 URL 的字符串, 详见1-5-7-4-2
NSString *key = [self cacheKeyForURL:url cacheKeyFilter:cacheKeyFilter];
@weakify(operation);
// 主线方法:queryImageForKey: 查找缓存,缓存业务模块的入口, 详见 1-5-7-4-3
operation.cacheOperation = [self.imageCache queryImageForKey:key options:options context:context completion:^(UIImage * _Nullable cachedImage, NSData * _Nullable cachedData, SDImageCacheType cacheType) {
@strongify(operation);
// 如果 operation 不存在或者被取消,回调错误信息
if (!operation || operation.isCancelled) {
// Image combined operation cancelled by user
// callCompletionBlockForOperation: 只是单纯的调用 completion: block回调错误信息,详见1-5-7-4-4
[self callCompletionBlockForOperation:operation completion:completedBlock error:[NSError errorWithDomain:SDWebImageErrorDomain code:SDWebImageErrorCancelled userInfo:nil] url:url];
[self safelyRemoveOperationFromRunning:operation];
return;
}
// Continue download process
// 主线方法:下载业务模块的入口,需要等待查询缓存之后(等待 queryImageForKey 的回调)决定是否需要下载操作; 详见 1-5-7-4-5
[self callDownloadProcessForOperation:operation url:url options:options context:context cachedImage:cachedImage cachedData:cachedData cacheType:cacheType progress:progressBlock completed:completedBlock];
}];
} else {
// Continue download process
// 主线方法:下载业务模块的入口,不需要查询缓存时,直接下载; 如果你不选择 SDWebImageFromLoaderOnly 这个选项,不会走这个分支
[self callDownloadProcessForOperation:operation url:url options:options context:context cachedImage:nil cachedData:nil cacheType:SDImageCacheTypeNone progress:progressBlock completed:completedBlock];
}
}
1-5-7-4-1
SDWebImageFromLoaderOnly 是一个枚举值,对没错,仍然与 1-5-4 中的 SDWebImageDelayPlaceholder
是同一类型枚举值,都定义在 SDWebImageOptions 中。
SDWebImageFromLoaderOnly选项:默认情况下,SD在下载器下载图片完成之前会去查询缓存的图片来用。这个选项会阻止这个默认的操作,只从下载器加载图片。
/// WebCache options
typedef NS_OPTIONS(NSUInteger, SDWebImageOptions) {
...
/**
* By default, we query the cache before the image is load from the loader. This flag can prevent this to load from loader only.
*/
SDWebImageFromLoaderOnly = 1 << 16,
...
1-5-7-4-2
-[SDWebImageManager cacheKeyForURL:cacheKeyFilter:]
,从 NSURL 中提取 URL 字符串。
- (nullable NSString *)cacheKeyForURL:(nullable NSURL *)url cacheKeyFilter:(id<SDWebImageCacheKeyFilter>)cacheKeyFilter {
if (!url) {
return @"";
}
// 默认 cacheKeyFilter 为空
if (cacheKeyFilter) {
// 开发者可以自定义处理URL的规则,并返回处理之后的 URL 字符串;详见1-5-7-4-2-1
return [cacheKeyFilter cacheKeyForURL:url];
} else {
// 默认直接返回从 NSURL 中提取的 URL 字符串
return url.absoluteString;
}
}
1-5-7-4-2-1
如果开发者不想单纯的直接返回从 NSURL 中提取的 URL 字符串,可以通过 SDWebImageCacheKeyFilter 的初始化方法 -[SDWebImageCacheKeyFilter initWithBlock:]
或 +[SDWebImageCacheKeyFilter cacheKeyFilterWithBlock:]
传入block自定一个规则,也就是你想如何处理NSURL,但最后仍要返回一个 URL 字符串。
-[SDWebImageCacheKeyFilter cacheKeyForURL:]
- (NSString *)cacheKeyForURL:(NSURL *)url {
// 这个block来自初始化方法 -[SDWebImageCacheKeyFilter initWithBlock:] 或 +[SDWebImageCacheKeyFilter cacheKeyFilterWithBlock:]
if (!self.block) {
return nil;
}
return self.block(url);
}
// SDWebImageCacheKeyFilterBlock 类型的block传入NSURL,返回NSString
- (instancetype)initWithBlock:(SDWebImageCacheKeyFilterBlock)block {
self = [super init];
if (self) {
self.block = block;
}
return self;
}
+ (instancetype)cacheKeyFilterWithBlock:(SDWebImageCacheKeyFilterBlock)block {
SDWebImageCacheKeyFilter *cacheKeyFilter = [[SDWebImageCacheKeyFilter alloc] initWithBlock:block];
return cacheKeyFilter;
}
SDWebImageCacheKeyFilterBlock 类型的block传入NSURL,返回NSString
typedef NSString * _Nullable(^SDWebImageCacheKeyFilterBlock)(NSURL * _Nonnull url);
1-5-7-4-3 主线方法
-[SDImageCache queryImageForKey:options:context:completion:]
首先将选项枚举值对应转换,即 SDWebImageOptions 转换成 SDImageCacheOptions 对应的枚举值。然后调用 queryCacheOperationForKey: 方法。
- (id<SDWebImageOperation>)queryImageForKey:(NSString *)key options:(SDWebImageOptions)options context:(nullable SDWebImageContext *)context completion:(nullable SDImageCacheQueryCompletionBlock)completionBlock {
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;
// queryCacheOperationForKey:options:context:done: 方法详见 1-5-7-4-3-1
return [self queryCacheOperationForKey:key options:cacheOptions context:context done:completionBlock];
}
1-5-7-4-3-1 主线方法-查找缓存
-[SDImageCache queryCacheOperationForKey:options:context:done:]
- 简单来讲内存缓存(SDMemoryCache)保存的是 UIImage 对象;磁盘缓存(SDDiskCache)保存的是 NSData 二进制数据。
- 查找缓存分为两步:第1步 查找内存缓存;第2步 查找磁盘缓存;
- 不论最终是否查找到缓存,都要调用 doneBlock ,回到 1-5-7-4
- (nullable NSOperation *)queryCacheOperationForKey:(nullable NSString *)key options:(SDImageCacheOptions)options context:(nullable SDWebImageContext *)context done:(nullable SDImageCacheQueryCompletionBlock)doneBlock {
if (!key) {
if (doneBlock) {
doneBlock(nil, nil, SDImageCacheTypeNone);
}
return nil;
}
// 默认 context 中无 transformer,transformer 为空
id<SDImageTransformer> transformer = context[SDWebImageContextImageTransformer];
if (transformer) {
// grab the transformed disk image if transformer provided
NSString *transformerKey = [transformer transformerKey];
// 处理key,经过形变的 key 组装的内容是不同于普通图片名称(key)的
key = SDTransformedKeyForKey(key, transformerKey);
}
// First check the in-memory cache...
// 第1步 查找内存缓存,详见 1-5-7-4-3-1-1
UIImage *image = [self imageFromMemoryCacheForKey:key];
// 如果有内存缓存则走此 if 分支
if (image) {
// 如果options是SDImageCacheDecodeFirstFrameOnly(只取动图的第一帧),则走此 if 分支;枚举值详见 1-5-7-4-3-1-2
if (options & SDImageCacheDecodeFirstFrameOnly) {
// Ensure static image
// 获取 image 的类
Class animatedImageClass = image.class;
// 判断是不是动图;详见 1-5-7-4-3-1-3
if (image.sd_isAnimated || ([animatedImageClass isSubclassOfClass:[UIImage class]] && [animatedImageClass conformsToProtocol:@protocol(SDAnimatedImage)])) {
#if SD_MAC
image = [[NSImage alloc] initWithCGImage:image.CGImage scale:image.scale orientation:kCGImagePropertyOrientationUp];
#else
// 如果 image 是动图,那么 image.CGImage 就意味着获取其第一帧
image = [[UIImage alloc] initWithCGImage:image.CGImage scale:image.scale orientation:image.imageOrientation];
#endif
}
// 如果options是SDImageCacheMatchAnimatedImageClass(匹配图片的类),则走此 else if 分支;枚举值详见 1-5-7-4-3-1-2
} else if (options & SDImageCacheMatchAnimatedImageClass) {
// Check image class matching
Class animatedImageClass = image.class;
// 开发者设置的图片的类,期望的类
Class desiredImageClass = context[SDWebImageContextAnimatedImageClass];
// 如果开发者设置了图片期望的类,但是 image 的类与设置的类不一致则将image置空
if (desiredImageClass && ![animatedImageClass isSubclassOfClass:desiredImageClass]) {
image = nil;
}
}
}
// shouldQueryMemoryOnly:只查询内存缓存;判断只要options不是SDImageCacheQueryMemoryData,立即调用doneBlock将image回传。
// SDImageCacheQueryMemoryData:去磁盘查找图片二进制数据(image data),枚举值详见1-5-7-4-3-1-2
BOOL shouldQueryMemoryOnly = (image && !(options & SDImageCacheQueryMemoryData));
// 如果只查询内存缓存,则立即调用 doneBlock 将 image 回传
if (shouldQueryMemoryOnly) {
if (doneBlock) {
doneBlock(image, nil, SDImageCacheTypeMemory);
}
return nil;
}
// 第2步 查找磁盘缓存
// Second check the disk cache...
NSOperation *operation = [NSOperation new];
// Check whether we need to synchronously query disk
// 1. in-memory cache hit & memoryDataSync
// 2. in-memory cache miss & diskDataSync
// 判断我们是否需要同步的去查找磁盘缓存
// 1. 内存缓存命中并且选择了SDImageCacheQueryMemoryDataSync;枚举值详见1-5-7-4-3-1-2
// 2. 内存缓存未命中并且选择了SDImageCacheQueryDiskDataSync;枚举值详见1-5-7-4-3-1-2
BOOL shouldQueryDiskSync = ((image && options & SDImageCacheQueryMemoryDataSync) ||
(!image && options & SDImageCacheQueryDiskDataSync));
// 定义并实现 queryDiskBlock 来保存一段代码,别急,不出这个方法就会调用这个block
void(^queryDiskBlock)(void) = ^{
// 如果operation被取消,直接调用doneBlock回调各种空值
if (operation.isCancelled) {
if (doneBlock) {
doneBlock(nil, nil, SDImageCacheTypeNone);
}
return;
}
@autoreleasepool {
// 只是单纯的从磁盘取出图片二进制数据(NSData); 缓存部分会在另外一篇文章详细解析。
NSData *diskData = [self diskImageDataBySearchingAllPathsForKey:key];
UIImage *diskImage;
SDImageCacheType cacheType = SDImageCacheTypeNone;
if (image) { // 如果内存缓存命中。此 if 分支并不排除 image 和 diskData 都命中的情形。
// the image is from in-memory cache, but need image data
diskImage = image;
cacheType = SDImageCacheTypeMemory;
} else if (diskData) { // 如果内存缓存未命中并且磁盘缓存命中
cacheType = SDImageCacheTypeDisk;
// decode image data only if in-memory cache missed
// diskImageForKey:data:options:context: 将 data 解码并返回 UIImage;详见1-5-7-4-3-1-4
diskImage = [self diskImageForKey:key data:diskData options:options context:context];
// 如果解码成功,并且 config 中 shouldCacheImagesInMemory(默认为YES) 为 YES,则将解码后的图片保存一份到内存缓存;
if (diskImage && self.config.shouldCacheImagesInMemory) {
NSUInteger cost = diskImage.sd_memoryCost;
[self.memoryCache setObject:diskImage forKey:key cost:cost];
}
}
// 根据前面的判断来选择同步还是异步调用doneBlock,回传image和data
if (doneBlock) {
if (shouldQueryDiskSync) {
doneBlock(diskImage, diskData, cacheType);
} else {
dispatch_async(dispatch_get_main_queue(), ^{
doneBlock(diskImage, diskData, cacheType);
});
}
}
}
};
// Query in ioQueue to keep IO-safe
// 根据前面的判断来选择同步还是异步调用刚刚自己实现的 queryDiskBlock
if (shouldQueryDiskSync) {
dispatch_sync(self.ioQueue, queryDiskBlock);
} else {
dispatch_async(self.ioQueue, queryDiskBlock);
}
return operation;
}
1-5-7-4-3-1-1
查找内存缓存有两种可能:有或没有。这不是废话,而是要分开说明有无内存缓存都大概有什么场景。寡人目前能想到的就是下面这些场景,如有其它的场景欢迎留言补充:
无内存缓存:
- 第一次使用SD加载这个URL下的图片;这时候去查找是没有内存缓存的。
- 第一次启动App;由于刚刚启动,内存中也一定是没有缓存的。
- App运行中 && 内存警告 && 未使用弱内存缓存;SD删除了内存缓存,同时也没有弱内存缓存可用。
有内存缓存:
- App运行中,第二次使用SD加载这个URL下的图片;
- 启动App && 有磁盘缓存,下一次查询缓存就会有内存缓存。因为磁盘缓存被取出后会存到内存缓存一份,以备下次使用;
- 如果使用了弱内存缓存,系统发出内存警告时不会删除弱内存缓存而只删除内存缓存,此时弱内存缓存可以当做内存缓存使用;
-[SDImageCache imageFromMemoryCacheForKey:]
查询内存缓存
- (nullable UIImage *)imageFromMemoryCacheForKey:(nullable NSString *)key {
return [self.memoryCache objectForKey:key];
}
-[SDMemoryCache objectForKey:]
查询内存缓存
- (id)objectForKey:(id)key {
id obj = [super objectForKey:key];
if (!self.config.shouldUseWeakMemoryCache) {
return obj;
}
if (key && !obj) {
// Check weak cache
SD_LOCK(self.weakCacheLock);
obj = [self.weakCache objectForKey:key];
SD_UNLOCK(self.weakCacheLock);
if (obj) {
// Sync cache
NSUInteger cost = 0;
if ([obj isKindOfClass:[UIImage class]]) {
cost = [(UIImage *)obj sd_memoryCost];
}
[super setObject:obj forKey:key cost:cost];
}
}
return obj;
}
1-5-7-4-3-1-2 SDImageCacheOptions枚举值部分解析
-
SDImageCacheDecodeFirstFrameOnly
是一个SDImageCacheOptions
类型的枚举值。默认情况下SD会解码动图。这个选项会强制仅解码动图的第一帧并返回这个(第一帧)静态图。 -
SDImageCacheMatchAnimatedImageClass
选项会保证image的类是你提供的类。与SDImageCacheDecodeFirstFrameOnly
对立,因为SDImageCacheDecodeFirstFrameOnly
总是返回UIImage/NSImage。 -
SDImageCacheQueryMemoryData
选项会让SD去磁盘异步查找 image data;默认情况下只要是查到内存缓存中有图片(UIImage),就不再去磁盘查找图片的二进制数据了(image data)。 -
SDImageCacheQueryMemoryDataSync
:默认情况下当开发者指定了SDImageCacheQueryMemoryData
选项后, SD会异步查找 image data。结合这个掩码也可以同步查找 image data。 -
SDImageCacheQueryDiskDataSync
:默认情况下当内存缓存未命中,SD会异步查找磁盘缓存。这个掩码可以强制SD同步查找磁盘缓存(内存缓存未命中时)。
/// Image Cache Options
typedef NS_OPTIONS(NSUInteger, SDImageCacheOptions) {
/**
* By default, we do not query image data when the image is already cached in memory. This mask can force to query image data at the same time. However, this query is asynchronously unless you specify `SDImageCacheQueryMemoryDataSync`
*/
SDImageCacheQueryMemoryData = 1 << 0,
/**
* By default, when you only specify `SDImageCacheQueryMemoryData`, we query the memory image data asynchronously. Combined this mask as well to query the memory image data synchronously.
*/
SDImageCacheQueryMemoryDataSync = 1 << 1,
/**
* By default, when the memory cache miss, we query the disk cache asynchronously. This mask can force to query disk cache (when memory cache miss) synchronously.
@note These 3 query options can be combined together. For the full list about these masks combination, see wiki page.
*/
SDImageCacheQueryDiskDataSync = 1 << 2,
...
/*
* By default, we decode the animated image. This flag can force decode the first frame only and produece the static image.
*/
SDImageCacheDecodeFirstFrameOnly = 1 << 5,
...
/**
* By default, when you use `SDWebImageContextAnimatedImageClass` context option (like using `SDAnimatedImageView` which designed to use `SDAnimatedImage`), we may still use `UIImage` when the memory cache hit, or image decoder is not available, to behave as a fallback solution.
* Using this option, can ensure we always produce image with your provided class. If failed, a error with code `SDWebImageErrorBadImageData` will been used.
* Note this options is not compatible with `SDImageCacheDecodeFirstFrameOnly`, which always produce a UIImage/NSImage.
*/
SDImageCacheMatchAnimatedImageClass = 1 << 7,
...
}
1-5-7-4-3-1-3
SD判断一张图是不是动图有两种方式:
- 一是判断 UIImage 的 images 属性是否为nil,非动图这个属性默认为nil;
源码中的 image.sd_isAnimated
就是 SD 专门为 UIImage 自定的一个分类方法:
// UIImage+Metadata.m
- (BOOL)sd_isAnimated {
return (self.images != nil);
}
- 二是根据SD自己的SDAnimatedImage类,这个类继承自UIImage,并且遵循SDAnimatedImage协议。如果这个image对象继承于UIImage类,并且遵循SDAnimatedImage协议,那么这个image就是SDAnimatedImage类,也就是动图。
1-5-7-4-3-1-4
-[SDImageCache diskImageForKey:data:options:context:]
调用解码函数 SDImageCacheDecodeImageData()
,输入 NSData,输出 UIImage。
解码部分会在另外一篇文章详细解析。
- (nullable UIImage *)diskImageForKey:(nullable NSString *)key data:(nullable NSData *)data options:(SDImageCacheOptions)options context:(SDWebImageContext *)context {
if (data) {
UIImage *image = SDImageCacheDecodeImageData(data, key, [[self class] imageOptionsFromCacheOptions:options], context);
return image;
} else {
return nil;
}
}
1-5-7-4-4
-[SDWebImageManager callCompletionBlockForOperation:completion:error:url:]
回调错误信息
- (void)callCompletionBlockForOperation:(nullable SDWebImageCombinedOperation*)operation
completion:(nullable SDInternalCompletionBlock)completionBlock
error:(nullable NSError *)error
url:(nullable NSURL *)url {
[self callCompletionBlockForOperation:operation completion:completionBlock image:nil data:nil error:error cacheType:SDImageCacheTypeNone finished:YES url:url];
}
[SDWebImageManager callCompletionBlockForOperation:completion:image:data:error:cacheType:finished:url:]
回调结果
这里的 completionBlock 是 SDInternalCompletionBlock 类型,就是简称 InternalBlock2 的那个。
- (void)callCompletionBlockForOperation:(nullable SDWebImageCombinedOperation*)operation
completion:(nullable SDInternalCompletionBlock)completionBlock
image:(nullable UIImage *)image
data:(nullable NSData *)data
error:(nullable NSError *)error
cacheType:(SDImageCacheType)cacheType
finished:(BOOL)finished
url:(nullable NSURL *)url {
dispatch_main_async_safe(^{
if (completionBlock) {
completionBlock(image, data, error, cacheType, finished, url);
}
});
}
1-5-7-4-5 主线方法-下载业务入口
-[SDWebImageManager callDownloadProcessForOperation:url:options:context:cachedImage:cachedData:cacheType:progress:completed:]
在这里实现了requestImageWithURL:
方法的 completedBlock, SDWebImageDownloaderCompletedBlock
类型,以下把这个block简称为 LoaderCompletedBlock。
// Download process
- (void)callDownloadProcessForOperation:(nonnull SDWebImageCombinedOperation *)operation
url:(nonnull NSURL *)url
options:(SDWebImageOptions)options
context:(SDWebImageContext *)context
cachedImage:(nullable UIImage *)cachedImage
cachedData:(nullable NSData *)cachedData
cacheType:(SDImageCacheType)cacheType
progress:(nullable SDImageLoaderProgressBlock)progressBlock
completed:(nullable SDInternalCompletionBlock)completedBlock {
// 连续四次判断是否应该下载图片, &= 用法详见1-5-7-4-5-1
// Check whether we should download image from network
// 第一次判断 options,如果没有选择 SDWebImageFromCacheOnly 则应该下载; 枚举值详见1-5-7-4-5-2
BOOL shouldDownload = !SD_OPTIONS_CONTAINS(options, SDWebImageFromCacheOnly);
// 第二次判断,若没有缓存图片(cachedImage)或者 options 没有选择 SDWebImageRefreshCached 则应该下载; 注意此处是或的关系,满足其一即为真; 枚举值详见1-5-7-4-5-2
shouldDownload &= (!cachedImage || options & SDWebImageRefreshCached);
// 第三次判断,若代理不能响应 imageManager:shouldDownloadImageForURL: 方法或者 imageManager:shouldDownloadImageForURL: 方法返回 YES 则应该下载
shouldDownload &= (![self.delegate respondsToSelector:@selector(imageManager:shouldDownloadImageForURL:)] || [self.delegate imageManager:self shouldDownloadImageForURL:url]);
// 第四次判断,若 canRequestImageForURL: 方法返回 YES 则应该下载;此处 imageLoader 是 SDWebImageDownloader,详见 1-5-7-4-5-3
shouldDownload &= [self.imageLoader canRequestImageForURL:url];
if (shouldDownload) {
// 能走到这里,说明shouldDownload为真;下面这个 if 分支的条件和第二次判断并不冲突,详见1-5-7-4-5-4
if (cachedImage && options & SDWebImageRefreshCached) {
// If image was found in the cache but SDWebImageRefreshCached is provided, notify about the cached image
// AND try to re-download it in order to let a chance to NSURLCache to refresh it from server.
// 如果缓存有图片但是却选择了 SDWebImageRefreshCached 选项,回调缓存图片。为了给 NSURLCache 从服务端刷新图片的机会,会尝试重新下载。详见前面1-5-7-2
[self callCompletionBlockForOperation:operation completion:completedBlock image:cachedImage data:cachedData error:nil cacheType:cacheType finished:YES url:url];
// Pass the cached image to the image loader. The image loader should check whether the remote image is equal to the cached image.
// 把缓存图片传给 image loader, image loader 要验证服务端图片和缓存图片是否相同。
SDWebImageMutableContext *mutableContext;
if (context) {
mutableContext = [context mutableCopy];
} else {
mutableContext = [NSMutableDictionary dictionary];
}
mutableContext[SDWebImageContextLoaderCachedImage] = cachedImage;
context = [mutableContext copy];
}
@weakify(operation);
// 发起网络请求,下载图片;详见1-5-7-4-5-5
operation.loaderOperation = [self.imageLoader requestImageWithURL:url options:options context:context progress:progressBlock completed:^(UIImage *downloadedImage, NSData *downloadedData, NSError *error, BOOL finished) {
@strongify(operation);
if (!operation || operation.isCancelled) {
// Image combined operation cancelled by user
// 如果operation出错或者被取消,InternalBlock2 回调错误信息
[self callCompletionBlockForOperation:operation completion:completedBlock error:[NSError errorWithDomain:SDWebImageErrorDomain code:SDWebImageErrorCancelled userInfo:nil] url:url];
} else if (cachedImage && options & SDWebImageRefreshCached && [error.domain isEqualToString:SDWebImageErrorDomain] && error.code == SDWebImageErrorCacheNotModified) {
// Image refresh hit the NSURLCache cache, do not call the completion block
} else if ([error.domain isEqualToString:SDWebImageErrorDomain] && error.code == SDWebImageErrorCancelled) {
// Download operation cancelled by user before sending the request, don't block failed URL
// InternalBlock2 回调错误信息
[self callCompletionBlockForOperation:operation completion:completedBlock error:error url:url];
} else if (error) {
// InternalBlock2 回调错误信息
[self callCompletionBlockForOperation:operation completion:completedBlock error:error url:url];
BOOL shouldBlockFailedURL = [self shouldBlockFailedURLWithURL:url error:error];
// 保存下载失败的URL,failedURLs 就是1-5-7-1 SDWebImageRetryFailed 选项提到的黑名单
if (shouldBlockFailedURL) {
SD_LOCK(self.failedURLsLock);
[self.failedURLs addObject:url];
SD_UNLOCK(self.failedURLsLock);
}
} else {
// 正常情况会走这个分支,保存下载失败的URL,failedURLs 就是1-5-7-1 SDWebImageRetryFailed 选项提到的黑名单
if ((options & SDWebImageRetryFailed)) {
SD_LOCK(self.failedURLsLock);
[self.failedURLs removeObject:url];
SD_UNLOCK(self.failedURLsLock);
}
// 缓存部分会在另外一篇文章详细解析。
[self callStoreCacheProcessForOperation:operation url:url options:options context:context downloadedImage:downloadedImage downloadedData:downloadedData finished:finished progress:progressBlock completed:completedBlock];
}
if (finished) {
[self safelyRemoveOperationFromRunning:operation];
}
}];
} else if (cachedImage) {
// 如果查找到缓存图片并且不需要下载,走这个分支。InternalBlock2 回调,回传缓存图片
[self callCompletionBlockForOperation:operation completion:completedBlock image:cachedImage data:cachedData error:nil cacheType:cacheType finished:YES url:url];
[self safelyRemoveOperationFromRunning:operation];
} else {
// Image not in cache and download disallowed by delegate
// 代理禁止下载并且没有查找到缓存图片,InternalBlock2 回调,回传各种 nil
[self callCompletionBlockForOperation:operation completion:completedBlock image:nil data:nil error:nil cacheType:SDImageCacheTypeNone finished:YES url:url];
[self safelyRemoveOperationFromRunning:operation];
}
}
1-5-7-4-5-1
&=
的用法
这种按位与等于的写法,从最初的赋值开始每一次 &=
操作都将影响下一次结果,如果上一次结果是YES,那么本次 &=
的玩法和 &&
没有区别,同真为真。如果上一次结果是NO,那么 &=
就没得玩了,以后无论与谁 &=
都等于NO。也就是说除非结果一直都是YES,否则只要出现一次NO,那么以后都不能玩了。
1-5-7-4-5-2
SDWebImageFromCacheOnly
:默认情况下,若缓存未命中,则使用下载的图片。这个选项会阻止默认行为,只使用缓存的图片,没有缓存图片也不下载。
SDWebImageRefreshCached
:注释巴拉巴拉说了一大堆,归根结底这个选项就是允许从服务端刷新缓存图片。默认情况下,有了缓存图片就不去服务端再下载一次该URL的图片了,选了这个选项后,即便该URL的图片已经被缓存好,也会去服务端确认一下是否应该刷新。
typedef NS_OPTIONS(NSUInteger, SDWebImageOptions) {
...
/**
* Even if the image is cached, respect the HTTP response cache control, and refresh the image from remote location if needed.
* The disk caching will be handled by NSURLCache instead of SDWebImage leading to slight performance degradation.
* This option helps deal with images changing behind the same request URL, e.g. Facebook graph api profile pics.
* If a cached image is refreshed, the completion block is called once with the cached image and again with the final image.
*
* Use this flag only if you can't make your URLs static with embedded cache busting parameter.
*/
SDWebImageRefreshCached = 1 << 3,
...
/**
* By default, when the cache missed, the image is load from the loader. This flag can prevent this to load from cache only.
*/
SDWebImageFromCacheOnly = 1 << 15,
...
}
1-5-7-4-5-3
-[SDWebImageDownloader canRequestImageForURL:]
简单粗暴,只要是url存在,就始终返回YES
- (BOOL)canRequestImageForURL:(NSURL *)url {
if (!url) {
return NO;
}
// Always pass YES to let URLSession or custom download operation to determine
return YES;
}
1-5-7-4-5-4
第二次判断的条件:!cachedImage || options & SDWebImageRefreshCached
if 分支的条件:shouldDownload
并且 cachedImage && options & SDWebImageRefreshCached
按照源码的写法,如果能走 if 分支,说明第二次判断的条件和 if 分支的条件是可以同时成立的。
首先确定,若 cachedImage && options & SDWebImageRefreshCached
条件成立,则cachedImage 一定不为空,即 !cachedImage 为假;
若 !cachedImage 为假,要想第二次判断条件 !cachedImage || options & SDWebImageRefreshCached
成立(shouldDownload 为真),则 options & SDWebImageRefreshCached
必为真;
因此,第二次判断条件和 if 分支条件同时成立的情况就是:cachedImage 为真;options & SDWebImageRefreshCached
为真;得出 shouldDownload 为真,同时 if 分支条件也成立。