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

SDImageCache maintains a memory cache and an optional disk cache.

SDImageCache包括内存缓存和磁盘缓存,内存缓存使用的是继承自 NSCacheAutoPurgeCache,而磁盘缓存就是基于文件的读写。

先查看SDImageCache的接口,看下都包括哪些功能,然后一一讲解代码。

存储的功能:

1
2
3
4
- (void)storeImage:(UIImage *)image forKey:(NSString *)key;
- (void)storeImage:(UIImage *)image forKey:(NSString *)key toDisk:(BOOL)toDisk;
- (void)storeImage:(UIImage *)image recalculateFromImage:(BOOL)recalculate imageData:(NSData *)imageData forKey:(NSString *)key toDisk:(BOOL)toDisk;
- (void)storeImageDataToDisk:(NSData *)imageData forKey:(NSString *)key;

这四个方法的前两个直接调用的第三个,所以我们从第三个方法入手。
看代码:

1
2
3
4
5
// if memory cache is enabled
if (self.shouldCacheImagesInMemory) {
NSUInteger cost = SDCacheCostForImage(image);
[self.memCache setObject:image forKey:key cost:cost];
}

如果内存缓存可用,就将图片通过 NSCache 的接口 - (void)setObject: forKey: cost: ;存入。计算 cost 的方法是:

1
2
3
FOUNDATION_STATIC_INLINE NSUInteger SDCacheCostForImage(UIImage *image) {
return image.size.height * image.size.width * image.scale * image.scale;
}

也就是一张图片的像素数量。
如果需要存入磁盘,一般情况下我们是将 imageData 直接存入的,但是如果 recalculate 的值是 YES ,或者没有 imageData,那我们就需要将 image 转成 NSData 存入磁盘。具体的实现是判断这个 image 有没有透明通道或者它的前八个字节是不是规定的 PNG 那固定的八个字节,如果是则就调用 UIImagePNGRepresentation 方法转成 NSData ,如果不是那就调用 UIImageJPEGRepresentation 这个方法。有了 data 之后,就要调用那四个存储方法的第四个 storeImageDataToDisk
通过 key 和 _diskCachePath 得到缓存文件的具体路径,在使用 NSFileManager- (BOOL)createFileAtPath: contents: attributes: ; 方法,将数据写入磁盘中。

1
2
3
4
// disable iCloud backup
if (self.shouldDisableiCloud) {
[fileURL setResourceValue:[NSNumber numberWithBool:YES] forKey:NSURLIsExcludedFromBackupKey error:nil];
}

这段代码是避免该文件被 iCloud 备份。

这些读写操作都放到了SDImageCache的一个串行队列中ioQueue。我觉得是因为_fileManager是自己创建的:

1
2
3
dispatch_sync(_ioQueue, ^{
_fileManager = [NSFileManager new];
});

是为了保障它的线程安全,在 SDImageCache 这个类的所有文件读写操作,都会放到 ioQueue 这个队列执行。而[NSFileManager defaultManager]是系统提供,本身就是线程安全的。

查询的功能:

1
2
3
- (NSOperation *)queryDiskCacheForKey:(NSString *)key done:(SDWebImageQueryCompletedBlock)doneBlock;
- (UIImage *)imageFromMemoryCacheForKey:(NSString *)key;
- (UIImage *)imageFromDiskCacheForKey:(NSString *)key;

第一个查询方法,在讲 SDWebImageManager 时已经讲过了。
第二个方法,就是调用的 NSCache 中的- (nullable ObjectType)objectForKey:的方法。
第三个方法中,会先到内存缓存去查找,如果没有命中,则去磁盘缓存中查找,大概就是通过 key 获取具体的路径找到对应的文件取出 NSData ,在经过一些处理转成 image 返回。

删除的功能:

1
2
3
4
- (void)removeImageForKey:(NSString *)key;
- (void)removeImageForKey:(NSString *)key withCompletion:(SDWebImageNoParamsBlock)completion;
- (void)removeImageForKey:(NSString *)key fromDisk:(BOOL)fromDisk;
- (void)removeImageForKey:(NSString *)key fromDisk:(BOOL)fromDisk withCompletion:(SDWebImageNoParamsBlock)completion;

前三个方法都是调用的第四个,所以我们看第四个方法就好了。
如果有内存缓存则调用NSCache中的- (void)removeObjectForKey:,如果 fromDisk 为 YES,则调用NSFileManager- (BOOL)removeItemAtPath: error:方法,删除指定缓存文件的路径即可。

清除的功能:

1
2
3
- (void)clearMemory;
- (void)clearDiskOnCompletion:(SDWebImageNoParamsBlock)completion;
- (void)clearDisk;

第一个方法直接调用NSCache- (void)removeAllObjects;。第二个方法,直接调用了NSFileManager- (BOOL)removeItemAtPath: error:删除指定缓存目录的路径即可。第三个方法调用的第二个方法。

清理的功能:

1
2
- (void)cleanDiskWithCompletionBlock:(SDWebImageNoParamsBlock)completionBlock;
- (void)cleanDisk;

清理缓存就是清理掉一些过期的文件和超最大缓存大小限制的文件。

看第一个方法,首先获取磁盘缓存的路径 URL。然后通过以下代码获取所有缓存文件的一些属性:

1
2
3
4
5
6
7
NSArray *resourceKeys = @[NSURLIsDirectoryKey, NSURLContentModificationDateKey, NSURLTotalFileAllocatedSizeKey];
// This enumerator prefetches useful properties for our cache files.
NSDirectoryEnumerator *fileEnumerator = [_fileManager enumeratorAtURL:diskCacheURL
includingPropertiesForKeys:resourceKeys
options:NSDirectoryEnumerationSkipsHiddenFiles
errorHandler:NULL];

这些属性分别是,是否是目录,文件的修改日期和文件大小。NSDate *expirationDate = [NSDate dateWithTimeIntervalSinceNow:-self.maxCacheAge];这一句则是获取缓存过期的日期。
然后 for-in 遍历 fileEnumerator

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
NSMutableArray *urlsToDelete = [[NSMutableArray alloc] init];
for (NSURL *fileURL in fileEnumerator) {
NSDictionary *resourceValues = [fileURL resourceValuesForKeys:resourceKeys error:NULL];
// Skip directories.
if ([resourceValues[NSURLIsDirectoryKey] boolValue]) {
continue;
}
// Remove files that are older than the expiration date;
NSDate *modificationDate = resourceValues[NSURLContentModificationDateKey];
if ([[modificationDate laterDate:expirationDate] isEqualToDate:expirationDate]) {
[urlsToDelete addObject:fileURL];
continue;
}
// Store a reference to this file and account for its total size.
NSNumber *totalAllocatedSize = resourceValues[NSURLTotalFileAllocatedSizeKey];
currentCacheSize += [totalAllocatedSize unsignedIntegerValue];
[cacheFiles setObject:resourceValues forKey:fileURL];
}

获取文件路径的属性字典,如果是目录则跳过,比较修改日期和过期日期哪个更晚一些,如果是过期日期则说明该文件过期,放入urlsToDelete数组中。将文件大小累加到currentCacheSize上,并将不是过期的这些缓存文件记录到 cacheFiles 中,key 是文件的 URL ,value 是对应的属性字典。

之后,遍历urlsToDelete数组删除这些过期文件:

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

然后,判断没有过期的这些文件的总大小有没有超过最大的缓存大小 self.maxCacheSize
如果有的话,将 cacheFiles 里的 value 按照文件的修改日期进行排序,返回一个排好序的数组。取 self.maxCacheSize 大小的一半,作为清理缓存的界限const NSUInteger desiredCacheSize = self.maxCacheSize / 2;。遍历排序后的数组,一个个文件删除,删除一个就从之前的总缓存文件大小的值减去删除后的文件大小,再比较有没有小于清理缓存的界限值 desiredCacheSize。如果小于了,则跳出循环。最后在主线程回调 completionBlock(); 。这样就达到了清理磁盘缓存的目的。

计算缓存大小:

1
2
3
- (NSUInteger)getSize;
- (NSUInteger)getDiskCount;
- (void)calculateSizeWithCompletionBlock:(SDWebImageCalculateSizeBlock)completionBlock;

第一个方法就是遍历缓存目录的所有文件,获取这些文件路径,通过 [[NSFileManager defaultManager] attributesOfItemAtPath:filePath error:nil] 获得一个字典在通过 fileSize 方法获取文件大小,累加起来就是缓存的大小。
第二个和第三个方法都是获取指定缓存路径的 NSDirectoryEnumerator 遍历取对应的值,和上面相差不大,不在赘述。

查询缓存是否存在:

1
2
- (void)diskImageExistsWithKey:(NSString *)key completion:(SDWebImageCheckCacheCompletionBlock)completionBlock;
- (BOOL)diskImageExistsWithKey:(NSString *)key;

这些方法的实现基本就是调用

1
exists = [[NSFileManager defaultManager] fileExistsAtPath:[self defaultCachePathForKey:key]];

这个方法,不在赘述。

最后说下 clearMemorycleanDiskbackgroundCleanDisk 的调用时机,在 - (id)initWithNamespace:(NSString *)ns diskCacheDirectory:(NSString *)directory{} 这个初始化方法中,注册了三个通知分别是:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
[[NSNotificationCenter defaultCenter] addObserver:self
selector:@selector(clearMemory)
name:UIApplicationDidReceiveMemoryWarningNotification
object:nil];
[[NSNotificationCenter defaultCenter] addObserver:self
selector:@selector(cleanDisk)
name:UIApplicationWillTerminateNotification
object:nil];
[[NSNotificationCenter defaultCenter] addObserver:self
selector:@selector(backgroundCleanDisk)
name:UIApplicationDidEnterBackgroundNotification
object:nil];

报内存警告时调用 clearMemory 清除内存缓存,程序即将终止的时候调用 cleanDisk 清理过期或超大小限制的磁盘缓存,而程序进入后台的时候,调用 backgroundCleanDisk ,在后台执行 cleanDiskWithCompletionBlock 清理任务。

至此,SDImageCache 的大部分方法就讲解完了。

相关阅读:
SDWebImage 源码阅读记录(一)
SDWebImageManager 源码阅读记录(二)
SDWebImageDownloader 源码阅读记录(三)
SDWebImageDownloaderOperation 源码阅读记录(四)
SDWebImageCache 源码阅读记录(五)
SDWebImage相关类 源码阅读记录(六)