SDWebImage源码学习笔记 ☞ SDImageCache

SDWebImage-源码学习笔记.png

前言

这是本系列的第 5 篇,也是最后一篇,主要讨论处理缓存的类 SDImageCache 及相关类 SDMemoryCacheSDImageCacheConfig 等。

正文

2 个缓存相关的枚举

先介绍 SDImageCache.h 中定义的 2 个缓存相关的枚举:SDImageCacheTypeSDImageCacheOptions

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
typedef NS_ENUM(NSInteger, SDImageCacheType) {
// 不缓存,从网络下载数据
SDImageCacheTypeNone,
// 磁盘缓存
SDImageCacheTypeDisk,
// 内存缓存
SDImageCacheTypeMemory
};

typedef NS_OPTIONS(NSUInteger, SDImageCacheOptions) {
// 即使内存中有缓存,也要强制查询磁盘缓存
SDImageCacheQueryDataWhenInMemory = 1 << 0,
// 强制同步查询磁盘缓存
SDImageCacheQueryDiskSync = 1 << 1,
// 压缩大图
SDImageCacheScaleDownLargeImages = 1 << 2
};

几个重要属性:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
/// *** 缓存配置信息 ***
@property (nonatomic, nonnull, readonly) SDImageCacheConfig *config;
/// 内存缓存的最大消耗
@property (assign, nonatomic) NSUInteger maxMemoryCost;
/// 内存缓存的最大缓存数量
@property (assign, nonatomic) NSUInteger maxMemoryCountLimit;

/// *** 内存缓存 ***
@property (strong, nonatomic, nonnull) SDMemoryCache *memCache;
/// 磁盘缓存路径
@property (strong, nonatomic, nonnull) NSString *diskCachePath;
///
@property (strong, nonatomic, nullable) NSMutableArray<NSString *> *customPaths;
/// 读写操作的串行队列
@property (strong, nonatomic, nullable) dispatch_queue_t ioQueue;
/// 用于操作文件的 fileManager
@property (strong, nonatomic, nonnull) NSFileManager *fileManager;

其中 2 个属性需要重点关注一下:

config 所属类 SDImageCacheConfig 定义了很短属性,只提供了一个init方法,在里边给所有属性付了初值,详见下方法代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
static const NSInteger kDefaultCacheMaxCacheAge = 60 * 60 * 24 * 7; // 1 week

@implementation SDImageCacheConfig

- (instancetype)init {
if (self = [super init]) {
_shouldDecompressImages = YES;
_shouldDisableiCloud = YES;
_shouldCacheImagesInMemory = YES;
_shouldUseWeakMemoryCache = YES;
_diskCacheReadingOptions = 0;
_diskCacheWritingOptions = NSDataWritingAtomic;
_maxCacheAge = kDefaultCacheMaxCacheAge;
_maxCacheSize = 0;
_diskCacheExpireType = SDImageCacheConfigExpireTypeModificationDate;
}
return self;
}

@end

memCache 它属于一个继承自 NSCache 的缓存类 SDMemoryCache, 他有一个关键属性:

1
2
// weakCache 是 NSMapTable 类型
@property (nonatomic, strong, nonnull) NSMapTable<KeyType, ObjectType> *weakCache;

下面观察一下 SDMemoryCache 的初始化及相关代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
- (instancetype)initWithConfig:(SDImageCacheConfig *)config {
self = [super init];
if (self) {

self.weakCache = [[NSMapTable alloc] initWithKeyOptions:NSPointerFunctionsStrongMemory valueOptions:NSPointerFunctionsWeakMemory capacity:0];

// 其他省略 ...

[[NSNotificationCenter defaultCenter] addObserver:self
selector:@selector(didReceiveMemoryWarning:)
name:UIApplicationDidReceiveMemoryWarningNotification
object:nil];
}
return self;
}

- (void)didReceiveMemoryWarning:(NSNotification *)notification {
// 注意:此处是调用的 suoper 方法,所以并没有移除 weak cache,如果是调用 self 重写的 removeAllObjects 方法,就会移除 weak cache。
[super removeAllObjects];
}

- (void)dealloc {
[[NSNotificationCenter defaultCenter] removeObserver:self
name:UIApplicationDidReceiveMemoryWarningNotification
object:nil];
}

当遇到内存警告的时候,缓存会被清除,但 weak cache 并不会被移除,如果手动清除的话,weak cache 当然会被移除。

这里 value 设置成 weak 可以避免可能的循环引用,虽然是 weak,不过,image 实例可以被其他对象持有,像 imageView,这种情况下,value 就不是 nil。

方法

好了,我们还是切回来继续讨论 SDImageCache 提供的方法吧!

首先,是 SDImageCache 的创建方法,它给我们提供了一个单例方法 + (nonnull instancetype)sharedImageCache,下边是他的方法实现:

1
2
3
4
5
6
7
8
+ (nonnull instancetype)sharedImageCache {
static dispatch_once_t once;
static id instance;
dispatch_once(&once, ^{
instance = [self new];
});
return instance;
}

然后,看看初始化方法,我们发现所有初始化方法最后均调用了同一个核心方法 - (nonnull instancetype)initWithNamespace: diskCacheDirectory:,具体作用见下方代码注释。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
- (instancetype)init {
return [self initWithNamespace:@"default"];
}

- (nonnull instancetype)initWithNamespace:(nonnull NSString *)ns {
NSString *path = [self makeDiskCachePath:ns];
return [self initWithNamespace:ns diskCacheDirectory:path];
}

- (nonnull instancetype)initWithNamespace:(nonnull NSString *)ns
diskCacheDirectory:(nonnull NSString *)directory {
if ((self = [super init])) {
NSString *fullNamespace = [@"com.hackemist.SDWebImageCache." stringByAppendingString:ns];

// 创建一个 IO 串行队列 (依次执行操作)
_ioQueue = dispatch_queue_create("com.hackemist.SDWebImageCache", DISPATCH_QUEUE_SERIAL);

// 初始化内存缓存
_config = [[SDImageCacheConfig alloc] init];
_memCache = [[SDMemoryCache alloc] initWithConfig:_config];
_memCache.name = fullNamespace;

// 初始化磁盘缓存路径
if (directory != nil) {
_diskCachePath = [directory stringByAppendingPathComponent:fullNamespace];
} else {
NSString *path = [self makeDiskCachePath:ns];
_diskCachePath = path;
}

dispatch_sync(_ioQueue, ^{
self.fileManager = [NSFileManager new];
});

#if SD_UIKIT
// App 即将关闭的时候,清除过期缓存
[[NSNotificationCenter defaultCenter] addObserver:self
selector:@selector(deleteOldFiles)
name:UIApplicationWillTerminateNotification
object:nil];
// App 即将进入后台的时候,清除过期缓存
[[NSNotificationCenter defaultCenter] addObserver:self
selector:@selector(backgroundDeleteOldFiles)
name:UIApplicationDidEnterBackgroundNotification
object:nil];
#endif
}

return self;
}

记得 上一篇 介绍 SDWebImageManager 的时候,是这样使用 imageCache 的:

1
2
3
4
5
6
operation.cacheOperation = [self.imageCache queryCacheOperationForKey:key
options:cacheOptions
done:^(UIImage *cachedImage, NSData *cachedData, SDImageCacheType cacheType)
{
// 查询完成后的操作在这里,可能查到了,也可能没查到 ...
}

现在,我们就来揭开这个方法的什么面纱。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
- (nullable NSOperation *)queryCacheOperationForKey:(nullable NSString *)key
options:(SDImageCacheOptions)options
done:(nullable SDCacheQueryCompletedBlock)doneBlock
{
// 1.校验参数
if (!key) {
if (doneBlock) {
doneBlock(nil, nil, SDImageCacheTypeNone);
}
return nil;
}

// 2.查询内存缓存 (NSCache)
UIImage *image = [self imageFromMemoryCacheForKey:key];
BOOL shouldQueryMemoryOnly = (image && !(options & SDImageCacheQueryDataWhenInMemory));
if (shouldQueryMemoryOnly) {
if (doneBlock) {
doneBlock(image, nil, SDImageCacheTypeMemory);
}
return nil;
}

NSOperation *operation = [NSOperation new];

// 3.将获取缓存及解压的 ‘耗时’ 操作封装成一个 block
void(^queryDiskBlock)(void) = ^{

// 如果已经取消,不作任何处理,直接返回。
if (operation.isCancelled) {
return;
}

@autoreleasepool {

NSData *diskData = [self diskImageDataBySearchingAllPathsForKey:key];
UIImage *diskImage;
SDImageCacheType cacheType = SDImageCacheTypeDisk;

if (image) {

// A > 从 memery 取的

diskImage = image;
cacheType = SDImageCacheTypeMemory;

} else if (diskData) {

// B > 如果内存没有,但是从 disc 取到了,需要解压

diskImage = [self diskImageForKey:key data:diskData options:options];

if (diskImage && self.config.shouldCacheImagesInMemory) { // 缓存到内存
NSUInteger cost = SDCacheCostForImage(diskImage); // 计算大小
[self.memCache setObject:diskImage forKey:key cost:cost];
}
}

if (doneBlock) {
if (options & SDImageCacheQueryDiskSync) { // 同步执行完成回调
doneBlock(diskImage, diskData, cacheType);
} else { // 异步执行完成回调
dispatch_async(dispatch_get_main_queue(), ^{
doneBlock(diskImage, diskData, cacheType);
});
}
}
}
};

// 4.执行查询磁盘缓存的 block
if (options & SDImageCacheQueryDiskSync) {
queryDiskBlock();
} else {
dispatch_async(self.ioQueue, queryDiskBlock);
}

return operation;
}

如上边的代码所示,主要分了这么 4 步:

① 校验参数 — 如果 key 不存在,直接 doneBlock,返回 nil。

② 查询内存缓存 NSCache — 如果内存中有,并且没有强制要求必须查询磁盘,则 执行 doneBlock,将 image 返回。

③ 将获取缓存及解压的 ‘耗时’ 操作封装成一个 block — 这是为了最后执行异步操作的方便。

④ 执行查询磁盘缓存的 block — 如果设置了 SDImageCacheQueryDiskSync,则同步执行;否则,默认是异步执行。

第 ③ 步中查询磁盘缓存的 queryDiskBlock 里边有两个比较重要的方法:- (nullable NSData *)diskImageDataBySearchingAllPathsForKey:- (nullable UIImage *)diskImageForKey: data: options:,下边分别了解一下这两个方法:

  • diskImageDataBySearchingAllPathsForKey: 这个方法用于查询磁盘缓存,实现及代码注释如下,拼接缓存路径的方法就不展开了,其中文件名的生成是对传入的 key 执行了一次 MD5。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
- (nullable NSData *)diskImageDataBySearchingAllPathsForKey:(nullable NSString *)key {
// 1.尝试 通过默认路径查询磁盘缓存
NSString *defaultPath = [self defaultCachePathForKey:key];
NSData *data = [NSData dataWithContentsOfFile:defaultPath options:self.config.diskCacheReadingOptions error:nil];
if (data) {
return data;
}

// 2.异常情况的处理:更换了路径再取一次,新路径是将默认路径的后缀去掉 (如果有的话)
data = [NSData dataWithContentsOfFile:defaultPath.stringByDeletingPathExtension options:self.config.diskCacheReadingOptions error:nil];
if (data) {
return data;
}

// 3.遍历所有用户自定义的路径,执行类似 1、2 的操作,查询磁盘缓存
NSArray<NSString *> *customPaths = [self.customPaths copy];
for (NSString *path in customPaths) {
NSString *filePath = [self cachePathForKey:key inPath:path];
NSData *imageData = [NSData dataWithContentsOfFile:filePath options:self.config.diskCacheReadingOptions error:nil];
if (imageData) {
return imageData;
}

imageData = [NSData dataWithContentsOfFile:filePath.stringByDeletingPathExtension options:self.config.diskCacheReadingOptions error:nil];
if (imageData) {
return imageData;
}
}
// 没查到的话,返回 nil
return nil;
}
  • diskImageForKey: data: options: 此方法的作用是对从 Disc 直接取的 data,进行 解码、解压操作,实现代码如下。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
- (nullable UIImage *)diskImageForKey:(nullable NSString *)key data:(nullable NSData *)data options:(SDImageCacheOptions)options {
if (data) {

// 1.解码
UIImage *image = [[SDWebImageCodersManager sharedInstance] decodedImageWithData:data];
image = [self scaledImageForKey:key image:image];

// 2.解压
if (self.config.shouldDecompressImages) {
BOOL shouldScaleDown = options & SDImageCacheScaleDownLargeImages;

image = [[SDWebImageCodersManager sharedInstance] decompressedImageWithImage:image data:&data options:@{SDWebImageCoderScaleDownLargeImagesKey: @(shouldScaleDown)}];
}

return image;
} else {
return nil;
}
}

此方法分别使用了 SDWebImageCodersManager 的 2 个重要方法:

  • 解码方法 - (UIImage *)decodedImageWithData:data,其中 coder 可以理解为一个解码器,SDWebImage 提供了多种 coder,如 SDWebImageIOCoderSDWebImageGIFCoder 分别用于解码某一种类型的图片,如果新增一种图片,可以将对应的 coder(需遵守协议:SDWebImageCoder) 添加到 coders 里即可。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// SDWebImageCodersManager.m

- (UIImage *)decodedImageWithData:(NSData *)data {

LOCK(self.codersLock);
NSArray<id<SDWebImageCoder>> *coders = self.coders;
UNLOCK(self.codersLock);

for (id<SDWebImageCoder> coder in coders.reverseObjectEnumerator) {
if ([coder canDecodeFromData:data]) {
return [coder decodedImageWithData:data];
}
}
return nil;
}
  • 解压方法 - (UIImage *)decompressedImageWithImage:image data:data options:optionsDict,这个方法和上边的解码方法都属于 SDWebImageCoder 这个协议,与解码方法类似,也是针对不同类型的 image 有不同的 coder。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// SDWebImageCodersManager.m

- (UIImage *)decompressedImageWithImage:(UIImage *)image
data:(NSData *__autoreleasing _Nullable *)data
options:(nullable NSDictionary<NSString*, NSObject*>*)optionsDict {
if (!image) {
return nil;
}
LOCK(self.codersLock);
NSArray<id<SDWebImageCoder>> *coders = self.coders;
UNLOCK(self.codersLock);
for (id<SDWebImageCoder> coder in coders.reverseObjectEnumerator) {
if ([coder canDecodeFromData:*data]) {
UIImage *decompressedImage = [coder decompressedImageWithImage:image data:data options:optionsDict];
decompressedImage.sd_imageFormat = image.sd_imageFormat;
return decompressedImage;
}
}
return nil;
}

那么,这些 coder 是什么时候加进去的,又是怎么添加的呢?其实,这些逻辑都在 SDWebImageCodersManager 的实现代码里。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// SDWebImageCodersManager.m

- (instancetype)init {
if (self = [super init]) {
// 初始化 coders
NSMutableArray<id<SDWebImageCoder>> *mutableCoders = [@[[SDWebImageImageIOCoder sharedCoder]] mutableCopy];
#ifdef SD_WEBP
[mutableCoders addObject:[SDWebImageWebPCoder sharedCoder]];
#endif
_coders = [mutableCoders copy];
_codersLock = dispatch_semaphore_create(1);
}
return self;
}

从初始化方法可以看出来,此时只给 coders 添加了一种 coder,即 SDWebImageImageIOCoder,它是用来对普通的 JPG、PNG 等图片解码的。

为了支持对其他类型图片(如 GIF)的解码,manager 给我们提供了下边这个添加 coder 的方法,代码逻辑很简单,就不多做解释了。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// SDWebImageCodersManager.m

- (void)addCoder:(nonnull id<SDWebImageCoder>)coder {
if (![coder conformsToProtocol:@protocol(SDWebImageCoder)]) {
return;
}
LOCK(self.codersLock);
NSMutableArray<id<SDWebImageCoder>> *mutableCoders = [self.coders mutableCopy];
if (!mutableCoders) {
mutableCoders = [NSMutableArray array];
}
[mutableCoders addObject:coder];
self.coders = [mutableCoders copy];
UNLOCK(self.codersLock);
}

小结

关于缓存相关的类暂时就先介绍到这里,当然还有很多细节没来得及讨论,不过可以查看 demo 中的注释。

到此,关于 SDWebImage 的源码学习就告一段落了,目前的理解可能有点肤浅,以后随着理解的深入,会不定时的更新。

0%