您的位置:首页 > 其它

SDWebImage 源码阅读笔记(二)

2016-11-14 17:36 465 查看

前言

我们在第一篇文章《SDWebImage 源码阅读笔记(一)》中,已经了解到,当我们调用

1
2

[cell.imageView sd_setImageWithURL:[NSURL URLWithString:@"http://www.domain.com/path/to/image.jpg"]
placeholderImage:[UIImage imageNamed:@"placeholder.png"]];

时,其实是通过
SDWebImageManager
类进行协调,调用
SDImageCache
SDWebImageDownloader
来实现图片的缓存查询与网络下载的。那么今天在第二篇中,就让我们来对
SDImageCache
一探究竟吧:)

SDImageCache

SDImageCache.h
中你可以看到关于 SDImageCache 的描述:

SDImageCache maintains a memory cache and an optional disk cache. Disk cache write operations are performed asynchronous so it doesn’t add unnecessary latency to the UI.

该类维护了一个内存缓存与一个可选的磁盘缓存。同时,磁盘缓存的写操作是异步的,所以它不会对 UI 造成不必要的影响。

每次查询图片时,首先会根据图片的 URL 对应的 key 值先检查内存中是否有对应的图片,如果有则直接返回;如果没有则在 ioQueue 中去磁盘中查找,其 key 是根据 URL 生成的 MD5 值,找到之后先将图片缓存在内存中,然后在把图片返回:

1
23
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

- (NSOperation *)queryDiskCacheForKey:(NSString *)key done:(SDWebImageQueryCompletedBlock)doneBlock {
if (!doneBlock) {
return nil;
}

if (!key) {
doneBlock(nil, SDImageCacheTypeNone);
return nil;
}

// 首先检查内存缓存(查询是同步的),如果查找到,则直接回调 doneBlock 并返回
UIImage *image = [self imageFromMemoryCacheForKey:key];
if (image) {
doneBlock(image, SDImageCacheTypeMemory);
return nil;
}

NSOperation *operation = [NSOperation new];
dispatch_async(self.ioQueue, ^{
if (operation.isCancelled) {
return;
}
// 创建自动释放池,内存及时释放
@autoreleasepool {
// 检查磁盘缓存(查询是异步的),如果查找到,则将其放到内存缓存,并调用 doneBlock 回调
UIImage *diskImage = [self diskImageForKey:key];
if (diskImage) {
NSUInteger cost = SDCacheCostForImage(diskImage);
// 缓存至内存(NSCache)中
[self.memCache setObject:diskImage forKey:key cost:cost];
}
// 返回主线程设置图片
dispatch_async(dispatch_get_main_queue(), ^{
doneBlock(diskImage, SDImageCacheTypeDisk);
});
}
});

return operation;
}

NSCache

An NSCache object is a collection-like container, or cache, that stores key-value pairs, similar to the NSDictionary class.

NSCache 是苹果官方提供的缓存类,用法与 NSMutableDictionary 的用法很相似,在 SDWebImage 和 AFNetworking 中,使用它来管理缓存。同样是以 key-value 的形式进行存储,那么 NSCache 与 NSMutableDictionary 等集合类的区别或者说优势又是哪些呢?

NSCache 类结合了各种自动删除策略,以确保不会占用过多的系统内存。如果其它应用需要内存时,系统自动执行这些策略。当调用这些策略时,会从缓存中删除一些对象,以最大限度减少内存的占用

NSCache 是线程安全的,我们可以在不同的线程中添加、删除和查询缓存中的对象,而不需要锁定缓存区域

不像 NSMutableDictionary 对象,NSCache 对象并不会拷贝键(key),而是会强引用它

要点

在开发者自己不编写加锁代码的前提下,多个线程便可以同时访问 NSCache

NSCache 对象不拷贝键的原因在于:很多时候,键都是由不支持拷贝操作的对象来充当的。所以说,在键不支持拷贝操作的情况下,该类用起来比字典更方便

可以给 NSCache 对象设置上限,用以限制缓存中的对象总个数及「总成本」,而这些尺度则定义了缓存删减其中对象的时机。但是绝对不要把这些尺度当成可靠的「硬限制」,它们仅对 NSCache 起指导作用

将 NSPurgeableData 与 NSCache 搭配使用,可实现自动清除数据的功能,也就是说,当 NSPurgeableData 对象所占内存为系统所丢弃时,该对象自身也会从缓存中移除

如果缓存使用得当,那么应用程序的响应速度就能提高。只有那种「重新计算起来很费事的」数据,才值得放入缓存,比如那些需要从网络获取或者从磁盘读取的数据

内存查询是同步,磁盘查询是异步

磁盘

磁盘缓存的处理则是使用 NSFileManager 对象来实现的。默认以
com.hackemist.SDWebImageCache.default
为磁盘的缓存命名空间,程序运行后,可以在应用程序的文件夹
Library/Caches/default/com.hackemist.SDWebImageCache.default
下看到一些缓存文件。另外,SDImageCache 还定义了一个串行队列,来异步存储图片。

在磁盘查询的时候,会在后台将 NSData 转成 UIImage,并完成相关的解码工作:

1
23
4
5
6
7
8
9
10
11
12
13
14

- (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;
}
}

要点

为何要进行 decode,参见:Avoiding Image Decompression Sickness

存储图片

当下载完图片后,会先将图片保存到 NSCache 中,并把图片像素大小作为该对象的 cost 值,同时如果需要保存到硬盘,会先判断图片的格式,PNG 或者 JPEG,并保存对应的 NSData 到缓存路径中,文件名为 URL 的 MD5 值:

1
23
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

- (void)storeImage:(UIImage *)image recalculateFromImage:(BOOL)recalculate imageData:(NSData *)imageData forKey:(NSString *)key toDisk:(BOOL)toDisk {
...
// 内存缓存,将其存入 NSCache 中,同时传入图片的消耗值,cost 为像素值(当内存受限或者所有缓存对象的总代价超过了最大允许的值时,缓存会移除其中的一些对象)
[self.memCache setObject:image forKey:key cost:image.size.height * image.size.width * image.scale * image.scale];
if (toDisk) {
// 如果确定需要磁盘缓存,则将缓存操作作为一个任务放入 ioQueue 中
dispatch_async(self.ioQueue, ^{
// 构建一个 data,用来存储到 disk 中,默认值为 imageData
NSData *data = imageData;
if (image && (recalculate || !data)) {
#if TARGET_OS_IPHONE
// 需要确定图片是 PNG 还是 JPEG。PNG 图片容易检测,因为有一个唯一签名。PNG 图像的前 8 个字节总是包含以下值:137 80 78 71 13 10 26 10
// 在 imageData 为 nil 的情况下假定图像为 PNG。我们将其当作 PNG 以避免丢失透明度。而当有图片数据时,我们检测其前缀,确定图片的类型
BOOL imageIsPng = YES;
if ([imageData length] >= [kPNGSignatureData length]) {
imageIsPng = ImageDataHasPNGPreffix(imageData);
}
// 如果 image 是 PNG 格式,就是用 UIImagePNGRepresentation 将其转化为 NSData,否则按照 JPEG 格式转化,并且压缩质量为 1,即无压缩
if (imageIsPng) {
data = UIImagePNGRepresentation(image);
}
else {
data = UIImageJPEGRepresentation(image, (CGFloat)1.0);
}
#else
data = [NSBitmapImageRep representationOfImageRepsInArray:image.representations usingType: NSJPEGFileType properties:nil];
#endif
}
// 创建缓存文件并存储图片(使用 fileManager)
if (data) {
if (![_fileManager fileExistsAtPath:_diskCachePath]) {
[_fileManager createDirectoryAtPath:_diskCachePath withIntermediateDirectories:YES attributes:nil error:NULL];
}
// 保存 data 到指定的路径中
[_fileManager createFileAtPath:[self defaultCachePathForKey:key] contents:data attributes:nil];
}
});
}
}

清理图片

SDImageCache 会在系统发出低内存警告时释放内存,并且在程序进入 UIApplicationWillTerminateNotification 时,清理磁盘缓存,清理磁盘的机制是:

删除过期的图片,默认 7 天过期,可以通过 maxCacheAge 修改过期天数。

如果缓存的数据大小超过设置的最大缓存 maxCacheSize,则会按照文件最后修改时间的逆序,以每次一半的递归来移除那些过早的文件,直到缓存的实际大小小于我们设置的最大使用空间,可以通过修改 maxCacheSize 来改变最大缓存大小。

1
23
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

- (void)cleanDiskWithCompletionBlock:(SDWebImageNoParamsBlock)completionBlock {
dispatch_async(self.ioQueue, ^{
NSURL *diskCacheURL = [NSURL fileURLWithPath:self.diskCachePath isDirectory:YES];
NSArray *resourceKeys = @[NSURLIsDirectoryKey, NSURLContentModificationDateKey, NSURLTotalFileAllocatedSizeKey];

// 该枚举器预先获取缓存文件的有用的属性
NSDirectoryEnumerator *fileEnumerator = [_fileManager enumeratorAtURL:diskCacheURL
includingPropertiesForKeys:resourceKeys
options:NSDirectoryEnumerationSkipsHiddenFiles
errorHandler:NULL];

NSDate *expirationDate = [NSDate dateWithTimeIntervalSinceNow:-self.maxCacheAge];
NSMutableDictionary *cacheFiles = [NSMutableDictionary dictionary];
NSUInteger currentCacheSize = 0;

// 枚举缓存文件夹中所有文件,该迭代有两个目的:移除比过期日期更老的文件;存储文件属性以备后面执行基于缓存大小的清理操作
NSMutableArray *urlsToDelete = [[NSMutableArray alloc] init];
for (NSURL *fileURL in fileEnumerator) {
NSDictionary *resourceValues = [fileURL resourceValuesForKeys:resourceKeys error:NULL];

// 跳过文件夹
if ([resourceValues[NSURLIsDirectoryKey] boolValue]) {
continue;
}

// 移除早于有效期的老文件
NSDate *modificationDate = resourceValues[NSURLContentModificationDateKey];
if ([[modificationDate laterDate:expirationDate] isEqualToDate:expirationDate]) {
[urlsToDelete addObject:fileURL];
continue;
}

// 存储文件的引用并计算所有文件的总大小,以备后用
NSNumber *totalAllocatedSize = resourceValues[NSURLTotalFileAllocatedSizeKey];
currentCacheSize += [totalAllocatedSize unsignedIntegerValue];
[cacheFiles setObject:resourceValues forKey:fileURL];
}

for (NSURL *fileURL in urlsToDelete) {
[_fileManager removeItemAtURL:fileURL error:nil];
}

// 如果磁盘缓存的大小大于我们配置的最大大小,则执行基于文件大小的清理,我们首先删除最早的文件
if (self.maxCacheSize > 0 && currentCacheSize > self.maxCacheSize) {
// 以设置的最大缓存大小的一半作为清理目标
const NSUInteger desiredCacheSize = self.maxCacheSize / 2;

// 按照最后修改时间来排序剩下的缓存文件
NSArray *sortedFiles = [cacheFiles keysSortedByValueWithOptions:NSSortConcurrent
usingComparator:^NSComparisonResult(id obj1, id obj2) {
return [obj1[NSURLContentModificationDateKey] compare:obj2[NSURLContentModificationDateKey]];
}];

// 删除文件,直到缓存总大小降到我们期望的大小
for (NSURL *fileURL in sortedFiles) {
if ([_fileManager removeItemAtURL:fileURL error:nil]) {
NSDictionary *resourceValues = cacheFiles[fileURL];
NSNumber *totalAllocatedSize = resourceValues[NSURLTotalFileAllocatedSizeKey];
currentCacheSize -= [totalAllocatedSize unsignedIntegerValue];

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

关于
SDImageCache
的内容大致就这么多,接下来就让我们一起来看一下
SDWebImageDownloader
中都暗藏了哪些玄机吧!

原文:http://itangqi.me/2016/03/21/the-notes-of-learning-sdwebimage-two/
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: