SDWebImage - 缓存管理

源码分析

这篇文章主要是介绍了一下SDWebImage提供的一写关于缓存的其他功能,也提供了清除缓存的方法,这里粗略的介绍一下,希望对大家在以后的文件操作中有一定的帮助。

磁盘空间的初始化方法

1
2
3
4
5
6
7
8
9
10
11
12
13
- (id)initWithNamespace:(NSString *)ns {
// iOS使用的是沙盒机制,此处makeDiskCachePath就是获取Cache目录,并
在Cache目录下创建default目录
// 比如我的mac上就显示/Users/poloby/Library/Developer/
CoreSimulator/Devices/4404872F-4DDD-4AEA-AAD3-71BA1931D4C1/
data/Containers/Data/Application/9C7E5D14-FBF0-41F1-A533-
E8ACC59FCBAC/Library/Caches/default
// 后面详解
NSString *path = [self makeDiskCachePath:ns];
// 最终的初始化,后面详解
return [self initWithNamespace:ns diskCacheDirectory:path];
}

makeDiskCachePath

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
-(NSString *)makeDiskCachePath:(NSString*)fullNamespace{
// 获取当前用户应用下的Caches目录
// 返回了一个包含用户Caches目录作为第一元素的数组,所以底下用的是
paths[0]
// 即/Users/poloby/Library/Developer/CoreSimulator/Devices/
4404872F-4DDD-4AEA-AAD3-71BA1931D4C1/data/Containers/Data/
Application/9C7E5D14-FBF0-41F1-A533-E8ACC59FCBAC/Library/
Caches/

NSArray *paths =
NSSearchPathForDirectoriesInDomains(NSCachesDirectory,
NSUserDomainMask, YES);
// 在Caches目录下构建一个fullNamespace目录,此处默认是default目录
return [paths[0]
stringByAppendingPathComponent:fullNamespace];
}

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
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
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
(id)initWithNamespace:(NSString *)ns diskCacheDirectory:
(NSString *)directory {

if ((self = [super init])) {
// 再给Caches/default/后面加上fullNamspace
// 最终可能获得的diskCachePath可能为
NSString *fullNamespace =
[@"com.hackemist.SDWebImageCache."
stringByAppendingString:ns];

// 初始化kPNGSignatureData为PNG前8字节的标志:{0x89, 0x50,
0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A}
// 用于ImageDataHasPNGPreffix这个C函数中,判断该data是不是PNG
格式

kPNGSignatureData = [NSData
dataWithBytes:kPNGSignatureBytes length:8];

// 创建名为com.hackemist.SDWebImageCache的IO的串行队列

_ioQueue =
dispatch_queue_create("com.hackemist.SDWebImageCache",
DISPATCH_QUEUE_SERIAL);

// cache存储的最长时间为60 * 60 * 24 * 7,即一个星期
_maxCacheAge = kDefaultCacheMaxCacheAge;

// 注意此处不是直接使用[[NSCache alloc] init]进行初始化的,而
是使用了一个AutoPurgeCache
// AutoPurgeCache和NSCache不同之处在于,如果AutoPurgeCache收
到一个内存警告,就会自动释放内存,调用NSCache的
removeAllObjects

_memCache = [[AutoPurgeCache alloc] init];
_memCache.name = fullNamespace;

// 初始化disk cache,一般情况下directory,除非你把Caches删除了
if (directory != nil) {
// 最终结果是/Users/poloby/Library/Developer/
CoreSimulator/Devices/4404872F-4DDD-4AEA-
AAD3-71BA1931D4C1/data/Containers/Data/Application/
9C7E5D14-FBF0-41F1-A533-E8ACC59FCBAC/Library/Caches/
default/com.hackemist.SDWebImageCache.default

_diskCachePath = [directory
stringByAppendingPathComponent:fullNamespace];
} else {
// 如果没有找到Caches目录,或者新建default目录失败。就重新
使用makeCachePath新建一个缓存目录
NSString *path = [self makeDiskCachePath:ns];
_diskCachePath = path;
}

// 默认需要解压缩图片
_shouldDecompressImages = YES;
// 新建一个NSFileManager也是放在ioQueue中的
dispatch_sync(_ioQueue, ^{
_fileManager = [NSFileManager new];
});

#if TARGET_OS_IPHONE
// 订阅了app可能发生的时间
// 出现内存警告
(UIApplicationDidReceiveMemoryWarningNotification),调用
clearMemory

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

// 程序终止(UIApplicationWillTerminateNotification),调用
cleanDisk

[[NSNotificationCenter defaultCenter] addObserver:self
selector:@selector(cleanDisk)
name:UIApplicationWillTerminateNotification
object:nil];
// 程序进入后台运行
(UIApplicationDidEnterBackgroundNotification),调用
backgroundCleanDisk

// backgroundCleanDisk就不赘述了,其实现了在后台注册了
cleanDiskWithCompletionBlock函数来处理后台的磁盘缓存

[[NSNotificationCenter defaultCenter] addObserver:self
selector:@selector(backgroundCleanDisk)
name:UIApplicationDidEnterBackgroundNotification
object:nil];
#endif
}

return self;
}

计算缓存文件的大小

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
- (void)calculateSizeWithCompletionBlock:(SDWebImageCalculateSizeBlock)completionBlock {
NSURL *diskCacheURL = [NSURL fileURLWithPath:
self.diskCachePath
isDirectory:YES];

dispatch_async(self.ioQueue, ^{
NSUInteger fileCount = 0;
NSUInteger totalSize = 0;

NSDirectoryEnumerator *fileEnumerator =
[_fileManager enumeratorAtURL:diskCacheURL
includingPropertiesForKeys:@[NSFileSize]
options:
NSDirectoryEnumerationSkipsHiddenFiles
errorHandler:NULL];

for (NSURL *fileURL in fileEnumerator) {
NSNumber *fileSize;
//获取单个文件大小的方法
[fileURL getResourceValue:&fileSize
forKey:NSURLFileSizeKey error:NULL];
totalSize += [fileSize unsignedIntegerValue];
//文件个数累加
fileCount += 1;
}

if (completionBlock) {
dispatch_async(dispatch_get_main_queue(), ^{
completionBlock(fileCount, totalSize);
});
}
});
}

获取磁盘文件个数

1
2
3
4
5
6
7
8
9
- (NSUInteger)getDiskCount {
__block NSUInteger count = 0;
dispatch_sync(self.ioQueue, ^{
NSDirectoryEnumerator *fileEnumerator = [_fileManager enumeratorAtPath:self.diskCachePath];
count = [[fileEnumerator allObjects] count];
});
return count;
}

SDWebImage定期清理缓存

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
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
- (void)cleanDiskWithCompletionBlock:
(SDWebImageNoParamsBlock)completionBlock {
dispatch_async(self.ioQueue, ^{
// 这两个变量主要是为了下面生成NSDirectoryEnumerator准备的
// 一个是记录遍历的文件目录,一个是记录遍历需要预先获取文件的哪些属性

NSURL *diskCacheURL = [NSURL fileURLWithPath:
self.diskCachePath
isDirectory:YES];

NSArray *resourceKeys = @[NSURLIsDirectoryKey,
NSURLContentModificationDateKey,
NSURLTotalFileAllocatedSizeKey];


// 递归地遍历diskCachePath这个文件夹中的所有目录,此处不是直接使
用diskCachePath,而是使用其生成的NSURL
// 此处使用includingPropertiesForKeys:resourceKeys,这样每
个file的resourceKeys对应的属性也会在遍历时预先获取到
NSDirectoryEnumerator *fileEnumerator =
[_fileManager enumeratorAtURL:diskCacheURL
includingPropertiesForKeys:resourceKeys
options:NSDirectoryEnumerationSkipsHiddenFiles
errorHandler:NULL];


// 获取文件的过期时间,SDWebImage中默认是一个星期
// 不过这里虽然称*expirationDate为过期时间,但是实质上并不是这
样。
// 其实是这样的,比如在2015/12/12/00:00:00最后一次修改文件,对
应的过期时间应该是
// 2015/12/19/00:00:00,不过现在时间是2015/12/27/00:00:00,
我先将当前时间减去1个星期,得到
// 2015/12/20/00:00:00,这个时间才是我们函数中的
expirationDate。
// 用这个expirationDate和最后一次修改时间modificationDate比较
看谁更晚就行。

NSDate *expirationDate = [NSDate
dateWithTimeIntervalSinceNow:-self.maxCacheAge];

NSMutableDictionary *cacheFiles =
[NSMutableDictionary dictionary];

NSUInteger currentCacheSize = 0;

// 在缓存的目录开始遍历文件. 此次遍历有两个目的:
//
// 1. 移除过期的文件
// 2. 同时存储每个文件的属性(比如该file是否是文件夹、该file所
需磁盘大小,修改时间)

NSMutableArray *urlsToDelete =
[[NSMutableArray alloc] init];

for (NSURL *fileURL in fileEnumerator) {
NSDictionary *resourceValues =
[fileURL resourceValuesForKeys:resourceKeys
error:NULL];

// Skip directories.
if ([resourceValues[NSURLIsDirectoryKey] boolValue])
{
continue;
}

// 移除过期文件
// 这里判断过期的方式:对比文件的最后一次修改日期和
expirationDate谁更晚,如果expirationDate更晚,就认为
该文件已经过期,具体解释见上面
NSDate *modificationDate =
resourceValues[NSURLContentModificationDateKey];

if ([[modificationDate laterDate:expirationDate]
isEqualToDate:expirationDate]) {
[urlsToDelete addObject:fileURL];
continue;
}

// 计算当前已经使用的cache大小,
// 并将对应file的属性存到cacheFiles中
NSNumber *totalAllocatedSize =
resourceValues[NSURLTotalFileAllocatedSizeKey];

currentCacheSize += [totalAllocatedSize
unsignedIntegerValue];
[cacheFiles setObject:resourceValues
forKey:fileURL];
}

for (NSURL *fileURL in urlsToDelete) {
[_fileManager removeItemAtURL:fileURL error:nil];
}
// 如果我们当前cache的大小已经超过了允许配置的缓存大小,
那就删除已经缓存的文件。
// 删除策略就是,首先删除修改时间更早的缓存文件

if (self.maxCacheSize > 0
&& currentCacheSize > self.maxCacheSize)
{
// 直接将当前cache大小降到允许最大的cache大小的一般
const NSUInteger desiredCacheSize =
self.maxCacheSize / 2;

// 根据文件修改时间来给所有缓存文件排序,按照修改时间越早越在
前的规则排序
NSArray *sortedFiles = [cacheFiles
keysSortedByValueWithOptions:NSSortConcurrent
usingComparator:^NSComparisonResult(id obj1, id
obj2) {
return [obj1[NSURLContentModificationDateKey]
compare:
obj2[NSURLContentModificationDateKey]];
}];

// 每次删除file后,就计算此时的cache的大小
// 如果此时的cache大小已经降到期望的大小了,就停止删除文件了
for (NSURL *fileURL in sortedFiles) {
if ([_fileManager
removeItemAtURL:
fileURL error:nil])
{
NSDictionary *resourceValues =
cacheFiles[fileURL];
// 根据resourceValues获取该文件所需磁盘空间大小
NSNumber *totalAllocatedSize =
resourceValues[NSURLTotalFileAllocatedSizeKey];
// 计算当前cache大小
currentCacheSize -= [totalAllocatedSize
unsignedIntegerValue];

if (currentCacheSize < desiredCacheSize) {
break;
}
}
}
}
if (completionBlock) {
dispatch_async(dispatch_get_main_queue(), ^{
completionBlock();
});
}
});
}


总结一下:

清理缓存的时机:

清除磁盘缓存
UIApplicationWillTerminateNotification
UIApplicationDidEnterBackgroundNotification

清除Cache缓存

UIApplicationDidReceiveMemoryWarningNotification
当收到这三个通知的时候回到用对应的清理缓存的方法

清理过程:

1、按照用户设置的maxCacheAge图片被缓存的最长时间

1
2
3
4
/**
* The maximum length of time to keep an image in the cache, in seconds
*/
@property (assign, nonatomic) NSInteger maxCacheAge;

将所有modificationDate时间晚于maxCacheAge时间的文件删除

2、如果经过第一步的删除之后,图片的缓存仍然大于maxCacheSize,那么需要进一步的删除图片文件

1
2
3
4
5
/**
* The maximum size of the cache, in bytes.
*/
@property (assign, nonatomic) NSUInteger maxCacheSize;

这一步的删除,最终的目的是将最终的缓存大小降低到maxCacheSize的一半,方式也是根据最后的修改时间先将所有的文件排序,然后依次删除,每一次删除都要确认是否当前的缓存大小是否小鱼maxCacheSize的一半

3、清理Cache缓存

1
2
3
4
- (void)clearMemory {
[self.memCache removeAllObjects];
}

手动清理磁盘图片缓存

直接清除,磁盘缓存目录下的所有文件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
- (void)clearDiskOnCompletion:
(SDWebImageNoParamsBlock)completion
{
dispatch_async(self.ioQueue, ^{
// 先将存储在diskCachePath中缓存全部移除,然后新建一个空的
diskCachePath
[_fileManager removeItemAtPath:self.diskCachePath
error:nil];

[_fileManager createDirectoryAtPath:self.diskCachePath
withIntermediateDirectories:YES
attributes:nil
error:NULL];

if (completion) {
dispatch_async(dispatch_get_main_queue(), ^{
completion();
});
}
});
}

通过cacheKey获取某张图片

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
- (UIImage *)imageFromDiskCacheForKey:(NSString *)key {

// First check the in-memory cache...
UIImage *image = [self imageFromMemoryCacheForKey:key];
if (image) {
return image;
}

// Second check the disk cache...
UIImage *diskImage = [self diskImageForKey:key];
if (diskImage && self.shouldCacheImagesInMemory) {
NSUInteger cost = SDCacheCostForImage(diskImage);
[self.memCache setObject:diskImage forKey:key cost:cost];
}

return diskImage;
}

注意:这里在磁盘中找到这张图片之后,会将这张图片放到缓存中 用来表示他最近使用了

通过搜索全路径获取图片数据(NSData)

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
- (NSData *)diskImageDataBySearchingAllPathsForKey:(NSString *)key {
NSString *defaultPath = [self defaultCachePathForKey:key];
NSData *data = [NSData dataWithContentsOfFile:defaultPath];
if (data) {
return data;
}

// fallback because of https://github.com/rs/SDWebImage/
pull/976 that added the extension to the disk file name
// checking the key with and without the extension

data = [NSData dataWithContentsOfFile:
[defaultPath stringByDeletingPathExtension]];

if (data) {
return data;
}

NSArray *customPaths = [self.customPaths copy];
for (NSString *path in customPaths) {
NSString *filePath = [self cachePathForKey:key
inPath:path];
NSData *imageData = [NSData
dataWithContentsOfFile:filePath];

if (imageData) {
return imageData;
}

// fallback because of https://github.com/rs/SDWebImage/
pull/976 that added the extension to the disk file name
// checking the key with and without the extension

imageData = [NSData dataWithContentsOfFile:
[filePath stringByDeletingPathExtension]];

if (imageData) {
return imageData;
}
}

return nil;
}

customPaths:是在搜索完缓存以及磁盘都没有找到的时候,查找一个只读的空间,判断图片是否存在

1
2
3
4
5
6
7
8
/**
* Add a read-only cache path to search for images pre-cached by SDImageCache
* Useful if you want to bundle pre-loaded images with your app
*
* @param path The path to use for this read-only cache path
*/
- (void)addReadOnlyCachePath:(NSString *)path;

当用户调用这个方法设置只读的路径时,会向customPaths中添加路径

获取一张磁盘中缓存的图片(UIImage)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
- (UIImage *)diskImageForKey:(NSString *)key {
NSData *data = [self
diskImageDataBySearchingAllPathsForKey:key];
if (data) {
UIImage *image = [UIImage sd_imageWithData:data];
image = [self scaledImageForKey:key image:image];
if (self.shouldDecompressImages) {
image = [UIImage decodedImageWithImage:image];
}
return image;
}
else {
return nil;
}
}

从磁盘中取出图片要经历的几个过程:

1、取出NSData

2、将NSData转换为UIImage

3、做scale适配

4、图片是否需要解码,如果需要执行解码操作

5、返回这张图片