一. 缓存的介绍
缓存在SDWebImage中是一个不可缺少的部分,它提供了这样一个功能:你传入一个url的key,我(缓存类)从内存中去寻找对应的图片,如果找到则返回给你,如果找不到我到沙盒中去寻找,如果找到再返回给你,并存储在内存中,如果找不到就告诉你找不到(当然SDWebImage这边会由下载类去下载对应图片再来缓存)。
SDWebImage的缓存功能封装在SDImageCache中,内部包含了内存缓存和磁盘缓存,我们只需要通过url的key去查询就可以了,使用十分简单。通过对SDImageCache的解读,你可以学到如何设计一个缓存类(无论是内存缓存和磁盘缓存),来提高app的性能和用户体验(减少重复的网络请求,减少用户的等待事件)。
二. 缓存的设计
缓存的接口和组成
SDImageCache主要由两部分组成:内存缓存和磁盘缓存。内存缓存由NSCache的子类AutoPurgeCache管理。磁盘缓存是储存在沙盒中,文件夹和缓存文件的创建和删除是由NSFileManager类型的成员变量_fileManager来管理。
@interface SDImageCache ()
@property (strong, nonatomic) NSCache *memCache;//内存缓存(实际是AutoPurgeCache类型)
@property (strong, nonatomic) NSString *diskCachePath;//磁盘缓存地址
@property (strong, nonatomic) NSMutableArray *customPaths;
@property (SDDispatchQueueSetterSementics, nonatomic) dispatch_queue_t ioQueue;//io队列(并行队列)
@end
@implementation SDImageCache {
NSFileManager *_fileManager;//文件管理器
}
作为一个缓存类,应该具备类似于数据库的功能:增删改查。当然改是不需要的,因为每个url对应的图片资源是固定的。所以,在设计上,SDImageCache实现了增(储存图片),删(删除单个图片或清空缓存),查(根据key查询图片)。另外,还提供了一系列查询方法,比如查询缓存的大小,磁盘缓存的数量等等。我们也将根据这几大部分功能来解读。
//-------------- 初始化 ----------------//
// 单例创建缓存
+ (SDImageCache *)sharedImageCache;
// 通过文件夹名创建缓存
- (id)initWithNamespace:(NSString *)ns;
// 通过文件夹名和文件目录名创建缓存
- (id)initWithNamespace:(NSString *)ns diskCacheDirectory:(NSString *)directory;
// 添加只读的缓存目录
- (void)addReadOnlyCachePath:(NSString *)path;
//-------------- 储存 (根据key)----------------//
- (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;
//-------------- 查询 (根据key)----------------//
- (NSOperation *)queryDiskCacheForKey:(NSString *)key done:(SDWebImageQueryCompletedBlock)doneBlock;
- (UIImage *)imageFromMemoryCacheForKey:(NSString *)key;
- (UIImage *)imageFromDiskCacheForKey:(NSString *)key;
//-------------- 删除 (根据key,删除单个)----------------//
- (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;
//-------------- 清除或清理 (内存缓存或磁盘缓存)----------------//
- (void)clearMemory;
- (void)clearDiskOnCompletion:(SDWebImageNoParamsBlock)completion;
- (void)clearDisk;
- (void)cleanDiskWithCompletionBlock:(SDWebImageNoParamsBlock)completionBlock;
- (void)cleanDisk;
//-------------- 获取缓存信息 ----------------//
- (NSUInteger)getSize;
- (NSUInteger)getDiskCount;
- (void)calculateSizeWithCompletionBlock:(SDWebImageCalculateSizeBlock)completionBlock;
- (void)diskImageExistsWithKey:(NSString *)key completion:(SDWebImageCheckCacheCompletionBlock)completionBlock;
- (BOOL)diskImageExistsWithKey:(NSString *)key;
- (NSString *)cachePathForKey:(NSString *)key inPath:(NSString *)path;
- (NSString *)defaultCachePathForKey:(NSString *)key;
// 获取磁盘缓存的位置
-(NSString *)makeDiskCachePath:(NSString*)fullNamespace;
缓存的初始化
通过sharedImageCache创建一个SDImageCache的单例对象,主要在初始化做如下操作:
- 初始化内存缓存对象(AutoPurgeCache类型,主要封装了一个注册通知,当内存警告时,清除缓存)
- 在IO线程创建管理对象
- 初始化diskCachePath的值(后面图片储存在这里,如下图)
.../Library/Caches/default/com.hackemist.SDWebImageCache.default
- 初始化一些属性的设置(shouldDecompressImages,shouldCacheImagesInMemory等)
- 注册通知(clearMemory,cleanDisk,backgroundCleanDisk)
+ (SDImageCache *)sharedImageCache {
static dispatch_once_t once;
static id instance;
dispatch_once(&once, ^{
instance = [self new];
});
return instance;
}
- (id)init {
return [self initWithNamespace:@"default"];
}
- (id)initWithNamespace:(NSString *)ns {
NSString *path = [self makeDiskCachePath:ns];
return [self initWithNamespace:ns diskCacheDirectory:path];
}
- (id)initWithNamespace:(NSString *)ns diskCacheDirectory:(NSString *)directory {
if ((self = [super init])) {
//最内层文件夹名(com.hackemist.SDWebImageCache.default)
NSString *fullNamespace = [@"com.hackemist.SDWebImageCache." stringByAppendingString:ns];
// 初始化PNG表示的data
kPNGSignatureData = [NSData dataWithBytes:kPNGSignatureBytes length:8];
// 创建有一个IO操作的串行队列
_ioQueue = dispatch_queue_create("com.hackemist.SDWebImageCache", DISPATCH_QUEUE_SERIAL);
// 初始化缓存时间
_maxCacheAge = kDefaultCacheMaxCacheAge;
// 创建缓存对象
_memCache = [[AutoPurgeCache alloc] init];
_memCache.name = fullNamespace;
// 初始化磁盘缓存地址
if (directory != nil) {
_diskCachePath = [directory stringByAppendingPathComponent:fullNamespace];
} else {
NSString *path = [self makeDiskCachePath:ns];
_diskCachePath = path;
}
// 解压压图片YES
_shouldDecompressImages = YES;
// 缓存图片YES
_shouldCacheImagesInMemory = YES;
// 不后台上传iCloud
_shouldDisableiCloud = YES;
//在IO线程创建管理对象
dispatch_sync(_ioQueue, ^{
_fileManager = [NSFileManager new];
});
//注册通知(clearMemory,cleanDisk,backgroundCleanDisk)
......
}
return self;
}
查询图片
查询图片总结起来有三种方式:
① 判断是否存在
② 直接获取对应图片(单单传递图片信息)
③ 通过Block获取图片和在哪里获取的(用于传递多个信息)
- 检查磁盘缓存是否存在key对应的图片 ------>判断是否存在
本质都是由NSFileManager对象通过图片地址去查看,看是否存在。这边一个比较疑惑的是一个用的是defaultManager,另一个是_fileManager。个人认为应该是返回BOOL是立即要结果,不适宜在其他线程调用。
//检查磁盘缓存是否存在图片
- (BOOL)diskImageExistsWithKey:(NSString *)key {
BOOL exists = NO;
// 这里使用的是defaultManager,不是_fileManager
// 从苹果文档可以看出:可以从多个线程安全地调用共享的NSFileManager对象的方法
exists = [[NSFileManager defaultManager] fileExistsAtPath:[self defaultCachePathForKey:key]];
// 要确认有带拓展名的和没带拓展名的
if (!exists) {
exists = [[NSFileManager defaultManager] fileExistsAtPath:[[self defaultCachePathForKey:key] stringByDeletingPathExtension]];
}
return exists;
}
//检查磁盘缓存是否存在图片,并在完成后回调block
- (void)diskImageExistsWithKey:(NSString *)key completion:(SDWebImageCheckCacheCompletionBlock)completionBlock {
//这里使用的是_fileManager
dispatch_async(_ioQueue, ^{
BOOL exists = [_fileManager fileExistsAtPath:[self defaultCachePathForKey:key]];
// 要确认有带拓展名的和没带拓展名的
if (!exists) {
exists = [_fileManager fileExistsAtPath:[[self defaultCachePathForKey:key] stringByDeletingPathExtension]];
}
//在主线程回调completionBlock
if (completionBlock) {
dispatch_async(dispatch_get_main_queue(), ^{
completionBlock(exists);
});
}
});
}
- 通过key去内存缓存或沙盒获取图片 ------>直接获取对应图片
通过key去内存缓存或沙盒获取图片主要分成两部分:先去内存缓存找,再去沙盒缓存找。当然你也可以单独在内存或沙盒找。
①去内存缓存获取图片,直接调用memCache的objectForKey就可以了。
② 去沙盒缓存找要先从路径中获取imageData,然后将imageData进行一系列操作来还原成image(调整方向,转换成正确比例@2x或@3x,是否需要解压)
//从沙盒缓存和内存缓存中获取图片
- (UIImage *)imageFromDiskCacheForKey:(NSString *)key {
// 首先先从内存缓存中获取
UIImage *image = [self imageFromMemoryCacheForKey:key];
if (image) {
return image;
}
// 没有的话再从沙盒缓存中获取
UIImage *diskImage = [self diskImageForKey:key];
//如果沙盒缓存中有 && 设置是需要缓存图片
if (diskImage && self.shouldCacheImagesInMemory) {
//将图片缓存在内存中
NSUInteger cost = SDCacheCostForImage(diskImage);
[self.memCache setObject:diskImage forKey:key cost:cost];
}
return diskImage;
}
//从内存缓存中获取图片
- (UIImage *)imageFromMemoryCacheForKey:(NSString *)key {
return [self.memCache objectForKey:key];
}
//通过key从沙盒中获取图片data
- (NSData *)diskImageDataBySearchingAllPathsForKey:(NSString *)key {
NSString *defaultPath = [self defaultCachePathForKey:key];
NSData *data = [NSData dataWithContentsOfFile:defaultPath];
if (data) {
return data;
}
// 注意要使用有带拓展名的和没带拓展名的地址来获取一遍
data = [NSData dataWithContentsOfFile:[defaultPath stringByDeletingPathExtension]];
if (data) {
return data;
}
//从自定义的地址获取图片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;
}
// 注意要使用有带拓展名的和没带拓展名的地址来获取一遍
imageData = [NSData dataWithContentsOfFile:[filePath stringByDeletingPathExtension]];
if (imageData) {
return imageData;
}
}
return nil;
}
//直接通过key去沙盒获取image
- (UIImage *)diskImageForKey:(NSString *)key {
//通过key从沙盒中获取图片data
NSData *data = [self diskImageDataBySearchingAllPathsForKey:key];
if (data) {
//将data转成image(其中有包括调整方向)
UIImage *image = [UIImage sd_imageWithData:data];
//将里面@2x或@3x的转换成正确比例的image
image = [self scaledImageForKey:key image:image];
//如果有设置解压属性(即提前解压属性)就去解压
if (self.shouldDecompressImages) {
//解压图片
image = [UIImage decodedImageWithImage:image];
}
return image;
}
else {
return nil;
}
}
- 查询可以key对应的图片,通过block回调 ,并返回一个新的operation------>通过Block获取图片和在哪里获取的
对比之前的获取方法,同样也是这么一个步骤:先去内存缓存找,再去沙盒缓存找。只是通过这个方法可以不仅返回一个operation,并且通过block回调返回图片和对应的位置信息。
//查询可以key对应的图片,通过block回调
- (NSOperation *)queryDiskCacheForKey:(NSString *)key done:(SDWebImageQueryCompletedBlock)doneBlock {
//判断输入参数
if (!doneBlock) {
return nil;
}
if (!key) {
doneBlock(nil, SDImageCacheTypeNone);
return nil;
}
// 首先通过url作为key从内存缓存中去获取
UIImage *image = [self imageFromMemoryCacheForKey:key];
if (image) {
doneBlock(image, SDImageCacheTypeMemory);
return nil;
}
//内存缓存没有
//创建一个operation
NSOperation *operation = [NSOperation new];
//在IO队列中去查找沙盒中的图片
dispatch_async(self.ioQueue, ^{
if (operation.isCancelled) {
return;
}
@autoreleasepool {
//通过key去沙盒获取image
UIImage *diskImage = [self diskImageForKey:key];
//如果沙盒有,并且需要缓存图片则缓存起来
if (diskImage && self.shouldCacheImagesInMemory) {
//获得图片消耗的内存大小
NSUInteger cost = SDCacheCostForImage(diskImage);
//缓存起来
[self.memCache setObject:diskImage forKey:key cost:cost];
}
dispatch_async(dispatch_get_main_queue(), ^{
doneBlock(diskImage, SDImageCacheTypeDisk);
});
}
});
return operation;
}
储存图片
储存图片的一个最主要的方法是:(主要看两个参数,是否计算(计算的意思其实就是将image转成data,因为有的只传image,没有data)和是否要储存到沙盒(没有的话就只储存到内存而已))
- (void)storeImage:(UIImage *)image recalculateFromImage:(BOOL)recalculate
imageData:(NSData *)imageData
forKey:(NSString *)key
toDisk:(BOOL)toDisk;
步骤也分成两部分,如果shouldCacheImagesInMemory = YES是需要缓存在内存,就缓存在内存中。然后根据toDisk来判断是否需要缓存在沙盒中。
由于缓存在沙盒中的是data,要缓存在沙盒中的话,如果需要重新计算(recalculate)或者data没值,都需要通过image获取图片数据,再将data保存在沙盒中。
还有一点需要注意的是,储存的文件名是通过MD5将key转码得来的。
//存储图片(是否重新计算 是否在沙盒中)
- (void)storeImage:(UIImage *)image recalculateFromImage:(BOOL)recalculate imageData:(NSData *)imageData forKey:(NSString *)key toDisk:(BOOL)toDisk {
if (!image || !key) {
return;
}
// 如果设置是需要缓存在内存,就缓存在内存中
if (self.shouldCacheImagesInMemory) {
NSUInteger cost = SDCacheCostForImage(image);
[self.memCache setObject:image forKey:key cost:cost];
}
//要缓存在沙盒中
if (toDisk) {
dispatch_async(self.ioQueue, ^{
NSData *data = imageData;
//如果有图片 && (重新计算或没有data)--->就要通过image获取data
if (image && (recalculate || !data)) {
#if TARGET_OS_IPHONE
//获取图片的透明讯息
int alphaInfo = CGImageGetAlphaInfo(image.CGImage);
//是否有透明度
BOOL hasAlpha = !(alphaInfo == kCGImageAlphaNone ||
alphaInfo == kCGImageAlphaNoneSkipFirst ||
alphaInfo == kCGImageAlphaNoneSkipLast);
BOOL imageIsPng = hasAlpha;
// 根据前缀来判断是否是png图片
if ([imageData length] >= [kPNGSignatureData length]) {
imageIsPng = ImageDataHasPNGPreffix(imageData);
}
if (imageIsPng) {
data = UIImagePNGRepresentation(image);
}
else {
data = UIImageJPEGRepresentation(image, (CGFloat)1.0);
}
#else
data = [NSBitmapImageRep representationOfImageRepsInArray:image.representations usingType: NSJPEGFileType properties:nil];
#endif
}
//缓存在沙盒中
[self storeImageDataToDisk:data forKey:key];
});
}
}
//储存图片(内存缓存和沙盒缓存)(需要将image转成data所以要recalculate)
- (void)storeImage:(UIImage *)image forKey:(NSString *)key {
[self storeImage:image recalculateFromImage:YES imageData:nil forKey:key toDisk:YES];
}
//储存图片(是否在沙盒中也缓存)(需要将image转成data所以要recalculate)
- (void)storeImage:(UIImage *)image forKey:(NSString *)key toDisk:(BOOL)toDisk {
[self storeImage:image recalculateFromImage:YES imageData:nil forKey:key toDisk:toDisk];
}
//单单缓存在沙盒中
- (void)storeImageDataToDisk:(NSData *)imageData forKey:(NSString *)key {
if (!imageData) {
return;
}
//判断_diskCachePath的路径是否存在,没有就创建路径(创建对应的文件夹)
//.../Library/Caches/default/com.hackemist.SDWebImageCache.default
if (![_fileManager fileExistsAtPath:_diskCachePath]) {
[_fileManager createDirectoryAtPath:_diskCachePath withIntermediateDirectories:YES attributes:nil error:NULL];
}
// 获取image key 的缓存路径
//.../Library/Caches/default/com.hackemist.SDWebImageCache.default/24dd60428e4a8af2a2da3d87a226ab9b.png
NSString *cachePathForKey = [self defaultCachePathForKey:key];
// 转换成 NSUrl
NSURL *fileURL = [NSURL fileURLWithPath:cachePathForKey];
//将data储存起来
[_fileManager createFileAtPath:cachePathForKey contents:imageData attributes:nil];
// 是否上传iCould
if (self.shouldDisableiCloud) {
[fileURL setResourceValue:[NSNumber numberWithBool:YES] forKey:NSURLIsExcludedFromBackupKey error:nil];
}
}
缓存的删除(单个和全部)
缓存的删除主要分成两种:
① 通过key来删除单张图片
② 删除所有的图片或删除过期的图片
- 通过key来删除单张图片
下面该方法是主要的方法,通过fromDisk的值来决定删除内存缓存还是删除所有的缓存(包括内存和沙盒)。通过completion来完成删除后的事项。
- (void)removeImageForKey:(NSString *)key fromDisk:(BOOL)fromDisk withCompletion:(SDWebImageNoParamsBlock)completion {
if (key == nil) {
return;
}
//删除内存中的缓存
if (self.shouldCacheImagesInMemory) {
[self.memCache removeObjectForKey:key];
}
//是否也要删除沙盒中的缓存
if (fromDisk) {
dispatch_async(self.ioQueue, ^{
[_fileManager removeItemAtPath:[self defaultCachePathForKey:key] error:nil];
if (completion) {
dispatch_async(dispatch_get_main_queue(), ^{
completion();
});
}
});
} else if (completion){
completion();
}
}
- 删除所有的图片或删除过期的图片
方法中带clear的都是直接清除指定的缓存。
内存缓存就是调用memCache的removeAllObjects,磁盘缓存直接清除diskCachePath文件夹和里面的内容,再创建一个空的文件夹。
//清除内存缓存
- (void)clearMemory {
[self.memCache removeAllObjects];
}
//清除磁盘缓存
- (void)clearDisk {
[self clearDiskOnCompletion:nil];
}
//清除沙盒的缓存,完成后执行传入的block
- (void)clearDiskOnCompletion:(SDWebImageNoParamsBlock)completion
{
//在io队列中异步执行清除操作
dispatch_async(self.ioQueue, ^{
//清除文件夹
[_fileManager removeItemAtPath:self.diskCachePath error:nil];
//再创建个空的文件夹
[_fileManager createDirectoryAtPath:self.diskCachePath
withIntermediateDirectories:YES
attributes:nil
error:NULL];
//主线程回调传入的block(completion)
if (completion) {
dispatch_async(dispatch_get_main_queue(), ^{
completion();
});
}
});
}
方法中带clean的是整理沙盒的缓存,清除一些旧的(内存缓存只有清除)。
清理缓存主要也有两个步骤,先清除过期的文件,如果缓存大小还是大于设置的最大缓存,就得再清理。再清理的方法是,将剩下的文件按文件的日期从旧到新排序,然后再从旧的开始删除,直到文件缓存大小小于目标缓存的一半。
//清理沙盒的缓存,完成后执行传入的block
- (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;
//用来储存将要删除的文件url
NSMutableArray *urlsToDelete = [[NSMutableArray alloc] init];
//枚举器获取所有文件的url
for (NSURL *fileURL in fileEnumerator) {
//获取文件的相关属性字典
/*
{
NSURLContentModificationDateKey = "2017-12-14 07:04:29 +0000";//日期
NSURLIsDirectoryKey = 0;//是否是文件夹
NSURLTotalFileAllocatedSizeKey = 8192;//文件大小
}
*/
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中,以fileURL为key
[cacheFiles setObject:resourceValues forKey:fileURL];
}
//删除urlsToDelete中包含的文件
for (NSURL *fileURL in urlsToDelete) {
[_fileManager removeItemAtURL:fileURL error:nil];
}
// 如果我们剩下的磁盘缓存大小还是超过配置的容量,就再次进行清理
// 当前容量大于设置的maxCacheSize
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;
}
}
}
}
//主线程回调completionBlock
if (completionBlock) {
dispatch_async(dispatch_get_main_queue(), ^{
completionBlock();
});
}
});
}
缓存的其他操作
缓存的其他操作主要是获取磁盘缓存文件的总大小和总数量。都是通过_fileManager在iO队列中去异步获取的。
//获取磁盘缓存文件总大小(在ioQueue中)
- (NSUInteger)getSize {
__block NSUInteger size = 0;
dispatch_sync(self.ioQueue, ^{
NSDirectoryEnumerator *fileEnumerator = [_fileManager enumeratorAtPath:self.diskCachePath];
for (NSString *fileName in fileEnumerator) {
NSString *filePath = [self.diskCachePath stringByAppendingPathComponent:fileName];
NSDictionary *attrs = [[NSFileManager defaultManager] attributesOfItemAtPath:filePath error:nil];
size += [attrs fileSize];
}
});
return size;
}
//获取磁盘缓存文件数量(在ioQueue中)
- (NSUInteger)getDiskCount {
__block NSUInteger count = 0;
dispatch_sync(self.ioQueue, ^{
NSDirectoryEnumerator *fileEnumerator = [_fileManager enumeratorAtPath:self.diskCachePath];
count = [[fileEnumerator allObjects] count];
});
return count;
}
//计算磁盘缓存文件总大小和数量(在ioQueue中),并通过block传出
- (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);
});
}
});
}
三. 可以学习的知识点
① ioQueue(异步队列)的使用
我们可以在缓存这部分内容中看到很多任务是在异步队列上进行的:
- 获取沙盒缓存大小
- 获取沙盒缓存文件数量
- 将图片储存在沙盒中
- 去沙盒查找图片
- 删除沙盒缓存文件
可以这样总结,跟沙盒缓存有关的操作都放在异步队列中进行。
② 通知的使用
之前SDImageCache的初始化的时候,我们发现会注册三个通知。
主要用来清理内存缓存和整理沙盒缓存,而不用我们手动去管理。
//内存警告时会清除内存缓存
[[NSNotificationCenter defaultCenter] addObserver:self
selector:@selector(clearMemory)
name:UIApplicationDidReceiveMemoryWarningNotification
object:nil];
//app终止时,会整理沙盒缓存
[[NSNotificationCenter defaultCenter] addObserver:self
selector:@selector(cleanDisk)
name:UIApplicationWillTerminateNotification
object:nil];
//app进入后台时,会在后台整理沙盒缓存
[[NSNotificationCenter defaultCenter] addObserver:self
selector:@selector(backgroundCleanDisk)
name:UIApplicationDidEnterBackgroundNotification
object:nil];
③ 用completionBlock来传递多个参数
举个例子,当我们在查询任务完成后,想获取到图片以及对应获取的位置,就可以使用Block来传递。而不是像之前一样,直接在返回值中返回一个Image。
④ 内联函数和C函数的使用
其实这个也不太明白这样的一个好处???
//最大缓存时间(一个星期)
static const NSInteger kDefaultCacheMaxCacheAge = 60 * 60 * 24 * 7;
// PNG开头是8字节的标识(一个字节包含8位二进制数,2位十六进制)
static unsigned char kPNGSignatureBytes[8] = {0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A};
static NSData *kPNGSignatureData = nil;
BOOL ImageDataHasPNGPreffix(NSData *data);
//是否是png图片
BOOL ImageDataHasPNGPreffix(NSData *data) {
NSUInteger pngSignatureLength = [kPNGSignatureData length];
if ([data length] >= pngSignatureLength) {
if ([[data subdataWithRange:NSMakeRange(0, pngSignatureLength)] isEqualToData:kPNGSignatureData]) {
return YES;
}
}
return NO;
}
//内联函数(类似宏定义)--图片消耗的内存空间
FOUNDATION_STATIC_INLINE NSUInteger SDCacheCostForImage(UIImage *image) {
return image.size.height * image.size.width * image.scale * image.scale;
}