我们知道移动端有三级缓存:远程的服务器,还有本地的内存缓存,硬盘缓存。SDImageCache的存在就是为了解决图片的本地缓存。SDImageCache是SDWebImage一个很重要的部分。为SDWebImage提供了以下功能:
内存缓存,基于NSCache
异步的硬盘缓存,不会阻塞主线程
提供同步和异步两种方式来查询是否有缓存图片
提供同步和异步来获取缓存的图片
删除缓存图片
SDImageCacheConfig
我们先来看看SDImageCacheConfig,SDImageCacheConfig是用来图片缓存的配置信息。
//SDImageCacheConfigExpireType定义的是以什么方式来计算图片的过期时间
typedef NS_ENUM(NSUInteger, SDImageCacheConfigExpireType) {
//图片最近访问的时间
SDImageCacheConfigExpireTypeAccessDate,
//默认:图片最近修改的时间
SDImageCacheConfigExpireTypeModificationDate
};
SDImageCacheConfig的配置属性和注释如下:
@interface SDImageCacheConfig : NSObject
//预解码图片,默认YES;
//预解码图片可以提升性能,但会消耗太多的内存
@property (assign, nonatomic) BOOL shouldDecompressImages;
//取消iCloud备份
@property (assign, nonatomic) BOOL shouldDisableiCloud;
//是否禁用内存缓存
@property (assign, nonatomic) BOOL shouldCacheImagesInMemory;
//开启SDMemoryCache内部维护的一张图片弱引用表
//好处:当收到内存警告,SDMemoryCache会移除图片的缓存,但是有些图片此时已经被一些诸如UIImageView强引用这,使用这个弱引用表就能访问到图片,避免后面再去query硬盘缓存
@property (assign, nonatomic) BOOL shouldUseWeakMemoryCache;
//硬盘图片读取的配置选项,默认是0
@property (assign, nonatomic) NSDataReadingOptions diskCacheReadingOptions;
//把图片存入硬盘的配置选项,默认NSDataWritingAtomic原子操作
@property (assign, nonatomic) NSDataWritingOptions diskCacheWritingOptions;
//图片最大的缓存时间,默认1星期
@property (assign, nonatomic) NSInteger maxCacheAge;
//最大的缓存大小,如果maxCacheSize>0,在清除硬盘缓存的时候会先把缓存时间操作maxCacheAge的图片清除掉,然后再清除图片到总缓存大小在maxCacheSize * 0.5以下
@property (assign, nonatomic) NSUInteger maxCacheSize;
//硬盘缓存图片过期时间的计算方式,默认是最近修改的时间
@property (assign, nonatomic) SDImageCacheConfigExpireType diskCacheExpireType;
@end
SDImageCacheConfig的初始化函数:
static const NSInteger kDefaultCacheMaxCacheAge = 60 * 60 * 24 * 7; // 1 week
- (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;
}
SDImageCache
SDImageCache使用SDMemoryCache(继承NSCache)来实现内存缓存,使用NSFileManager来进行图片的硬盘存取。
SDMemoryCache.h
下面我们先来阅读下SDMemoryCache.h文件
//用来表示图片的缓存类型
typedef NS_ENUM(NSInteger, SDImageCacheType) {
//图片来自服务器
SDImageCacheTypeNone,
//图片来自硬盘缓存
SDImageCacheTypeDisk,
//图片来自内存缓存
SDImageCacheTypeMemory
};
//缓存图片的方式
typedef NS_OPTIONS(NSUInteger, SDImageCacheOptions) {
//当内存有图片,仍然查询硬盘缓存
SDImageCacheQueryDataWhenInMemory = 1 << 0,
//同步的方式来获取硬盘缓存(默认异步)
SDImageCacheQueryDiskSync = 1 << 1,
//缩小大图(>60M)
SDImageCacheScaleDownLargeImages = 1 << 2
};
@interface SDImageCache : NSObject
@property (nonatomic, nonnull, readonly) SDImageCacheConfig *config;
//内存缓存的最大值(图片像素)
@property (assign, nonatomic) NSUInteger maxMemoryCost;
//内存缓存的最大数量(图片数量)
@property (assign, nonatomic) NSUInteger maxMemoryCountLimit;
//暴露的单例对象
+ (nonnull instancetype)sharedImageCache;
//指定命名空间,图片存到对应的沙盒目录中
- (nonnull instancetype)initWithNamespace:(nonnull NSString *)ns;
//指定命名空间和沙盒目录
- (nonnull instancetype)initWithNamespace:(nonnull NSString *)ns
diskCacheDirectory:(nonnull NSString *)directory;
#pragma mark - Cache paths
//获取磁盘缓存路径
- (nullable NSString *)makeDiskCachePath:(nonnull NSString*)fullNamespace;
//添加只读路径
- (void)addReadOnlyCachePath:(nonnull NSString *)path;
#pragma mark - Store Ops
//异步的方式存储图片到内存和硬盘
- (void)storeImage:(nullable UIImage *)image
forKey:(nullable NSString *)key
completion:(nullable SDWebImageNoParamsBlock)completionBlock;
//异步的方式存储图片到内存和硬盘(可选,toDisk为YES才会存入硬盘)
- (void)storeImage:(nullable UIImage *)image
forKey:(nullable NSString *)key
toDisk:(BOOL)toDisk
completion:(nullable SDWebImageNoParamsBlock)completionBlock;
//当我们把图片存入硬盘时,需要把图片压缩为二进制格式才能存。当我们下载完图片后,得到的是图片的二进制数据,这个方法提供了imageData的入口,来避免对图片额外的压缩消耗
- (void)storeImage:(nullable UIImage *)image
imageData:(nullable NSData *)imageData
forKey:(nullable NSString *)key
toDisk:(BOOL)toDisk
completion:(nullable SDWebImageNoParamsBlock)completionBlock;
//同步的方式把图片的二进制数据存入硬盘
- (void)storeImageDataToDisk:(nullable NSData *)imageData forKey:(nullable NSString *)key;
#pragma mark - Query and Retrieve Ops
//异步的方式查询硬盘中是否有key对应的缓存图片
- (void)diskImageExistsWithKey:(nullable NSString *)key completion:(nullable SDWebImageCheckCacheCompletionBlock)completionBlock;
//同步的方式查询硬盘中是否有key对应的缓存图片
- (BOOL)diskImageDataExistsWithKey:(nullable NSString *)key;
//同步的方式获取硬盘缓存的图片二进制数据
- (nullable NSData *)diskImageDataForKey:(nullable NSString *)key;
//异步的方式来获取硬盘缓存的图片
- (nullable NSOperation *)queryCacheOperationForKey:(nullable NSString *)key done:(nullable SDCacheQueryCompletedBlock)doneBlock;
//异步的方式来获取硬盘缓存的图片
- (nullable NSOperation *)queryCacheOperationForKey:(nullable NSString *)key options:(SDImageCacheOptions)options done:(nullable SDCacheQueryCompletedBlock)doneBlock;
//同步的方式来获取内存缓存的图片
- (nullable UIImage *)imageFromMemoryCacheForKey:(nullable NSString *)key;
/同步的方式获取硬盘缓存的图片
- (nullable UIImage *)imageFromDiskCacheForKey:(nullable NSString *)key;
//同步的方式,先查询内存中有没有缓存的图片,如果没有再查询硬盘中有没有缓存的图片
- (nullable UIImage *)imageFromCacheForKey:(nullable NSString *)key;
#pragma mark - Remove Ops
//异步的方式移除缓存中的图片,包括内存和硬盘
- (void)removeImageForKey:(nullable NSString *)key withCompletion:(nullable SDWebImageNoParamsBlock)completion;
//异步的方式移除缓存中的图片,包括内存和硬盘(可选,fromDisk为YES移除硬盘缓存)
- (void)removeImageForKey:(nullable NSString *)key fromDisk:(BOOL)fromDisk withCompletion:(nullable SDWebImageNoParamsBlock)completion;
#pragma mark - Cache clean Ops
//清除内存缓存
- (void)clearMemory;
//异步方式清除硬盘缓存
- (void)clearDiskOnCompletion:(nullable SDWebImageNoParamsBlock)completion;
//异步方式清除过期的图片
- (void)deleteOldFilesWithCompletionBlock:(nullable SDWebImageNoParamsBlock)completionBlock;
#pragma mark - Cache Info
//同步方式计算缓存目录的大小
- (NSUInteger)getSize;
//同步方式计算缓存的图片数量
- (NSUInteger)getDiskCount;
//异步的方式获取缓存图片数量和大小
- (void)calculateSizeWithCompletionBlock:(nullable SDWebImageCalculateSizeBlock)completionBlock;
#pragma mark - Cache Paths
//指定key,获取图片的缓存路径
- (nullable NSString *)cachePathForKey:(nullable NSString *)key inPath:(nonnull NSString *)path;
//指定key,获取图片默认的缓存路径
- (nullable NSString *)defaultCachePathForKey:(nullable NSString *)key;
@end
SDImageCache.m
SDMemoryCache继承自NSCache。NSCache可以设置totalCostLimit,来限制缓存的总成本消耗,所以我们再添加缓存的时候需要
- (void)setObject:(ObjectType)obj forKey:(KeyType)key cost:(NSUInteger)g;
通过cost来指定缓存对象消耗的成本。
SDImageCache如何计算图片消耗的成本?
SDImageCache把图片的像素点来计算图片的消耗成本
FOUNDATION_STATIC_INLINE NSUInteger SDCacheCostForImage(UIImage *image) {
#if SD_MAC
return image.size.height * image.size.width;
#elif SD_UIKIT || SD_WATCH
return image.size.height * image.size.width * image.scale * image.scale;
#endif
}
SDMemoryCache
解析以注释的形式在代码中体现:
@interface SDMemoryCache <KeyType, ObjectType> ()
@property (nonatomic, strong, nonnull) SDImageCacheConfig *config;
//弱引用表
@property (nonatomic, strong, nonnull) NSMapTable<KeyType, ObjectType> *weakCache;
//多线程锁保证多线程环境下weakCache数据安全
@property (nonatomic, strong, nonnull) dispatch_semaphore_t weakCacheLock;
- (instancetype)init NS_UNAVAILABLE;
- (instancetype)initWithConfig:(nonnull SDImageCacheConfig *)config;
@end
@implementation SDMemoryCache
- (void)dealloc {
[[NSNotificationCenter defaultCenter] removeObserver:self name:UIApplicationDidReceiveMemoryWarningNotification object:nil];
}
- (instancetype)initWithConfig:(SDImageCacheConfig *)config {
self = [super init];
if (self) {
//初始化弱引用表,当收到内存警告,内存缓存虽然被清理,但是有些图片已经被其他对象强引用着,这时weakCache维持这些图片的弱引用。如果需要获取这些图片就不用去硬盘获取了
//NSPointerFunctionsWeakMemory,对值进行弱引用,不会对引用计数+1
self.weakCache = [[NSMapTable alloc] initWithKeyOptions:NSPointerFunctionsStrongMemory valueOptions:NSPointerFunctionsWeakMemory capacity:0];
self.weakCacheLock = dispatch_semaphore_create(1);
self.config = config;
//监听内存警告通知
[[NSNotificationCenter defaultCenter] addObserver:self
selector:@selector(didReceiveMemoryWarning:)
name:UIApplicationDidReceiveMemoryWarningNotification
object:nil];
}
return self;
}
- (void)didReceiveMemoryWarning:(NSNotification *)notification {
//当收到内存警告通知,移除内存中缓存的图片
//同时保留weakCache,维持对被强引用着的图片的访问
[super removeAllObjects];
}
- (void)setObject:(id)obj forKey:(id)key cost:(NSUInteger)g {
[super setObject:obj forKey:key cost:g];
if (!self.config.shouldUseWeakMemoryCache) {
return;
}
if (key && obj) {
// 存入弱引用表
LOCK(self.weakCacheLock);
[self.weakCache setObject:obj forKey:key];
UNLOCK(self.weakCacheLock);
}
}
- (id)objectForKey:(id)key {
id obj = [super objectForKey:key];
if (!self.config.shouldUseWeakMemoryCache) {
return obj;
}
if (key && !obj) {
LOCK(self.weakCacheLock);
obj = [self.weakCache objectForKey:key];
UNLOCK(self.weakCacheLock);
if (obj) {
// 把通过弱引用表获取的图片添加到内存缓存中
NSUInteger cost = 0;
if ([obj isKindOfClass:[UIImage class]]) {
cost = SDCacheCostForImage(obj);
}
[super setObject:obj forKey:key cost:cost];
}
}
return obj;
}
- (void)removeObjectForKey:(id)key {
[super removeObjectForKey:key];
if (!self.config.shouldUseWeakMemoryCache) {
return;
}
if (key) {
// 从weakCache移除
LOCK(self.weakCacheLock);
[self.weakCache removeObjectForKey:key];
UNLOCK(self.weakCacheLock);
}
}
- (void)removeAllObjects {
[super removeAllObjects];
if (!self.config.shouldUseWeakMemoryCache) {
return;
}
// 清除弱引用表
LOCK(self.weakCacheLock);
[self.weakCache removeAllObjects];
UNLOCK(self.weakCacheLock);
}
#else
- (instancetype)initWithConfig:(SDImageCacheConfig *)config {
self = [super init];
return self;
}
#endif
@end
SDImageCache私有拓展的属性:
//内存缓存管理实例
@property (strong, nonatomic, nonnull) SDMemoryCache *memCache;
//硬盘缓存路径
@property (strong, nonatomic, nonnull) NSString *diskCachePath;
//读取缓存图片额外的路径,以通过addReadOnlyCachePath:这个方法往里边添加路径
@property (strong, nonatomic, nullable) NSMutableArray<NSString *> *customPaths;
//访问操作硬盘缓存时用到的串行队列
@property (strong, nonatomic, nullable) dispatch_queue_t ioQueue;
//存储图片到硬盘的文件管理者
@property (strong, nonatomic, nonnull) NSFileManager *fileManager;
初始化函数最终都会调用
- (nonnull instancetype)initWithNamespace:(nonnull NSString *)ns
diskCacheDirectory:(nonnull NSString *)directory
- (nonnull instancetype)initWithNamespace:(nonnull NSString *)ns
diskCacheDirectory:(nonnull NSString *)directory {
if ((self = [super init])) {
//初始化参数
NSString *fullNamespace = [@"com.hackemist.SDWebImageCache." stringByAppendingString:ns];
_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
//监听通知来清除过期的图片缓存数据
[[NSNotificationCenter defaultCenter] addObserver:self
selector:@selector(deleteOldFiles)
name:UIApplicationWillTerminateNotification
object:nil];
[[NSNotificationCenter defaultCenter] addObserver:self
selector:@selector(backgroundDeleteOldFiles)
name:UIApplicationDidEnterBackgroundNotification
object:nil];
#endif
}
return self;
}
-
如果进入沙盒查看缓存的图片,可以发现文件名是用过md5的格式来命名
- (nullable NSString *)cachedFileNameForKey:(nullable NSString *)key { const char *str = key.UTF8String; if (str == NULL) { str = ""; } unsigned char r[CC_MD5_DIGEST_LENGTH]; //计算key的md5值 CC_MD5(str, (CC_LONG)strlen(str), r); NSURL *keyURL = [NSURL URLWithString:key]; NSString *ext = keyURL ? keyURL.pathExtension : key.pathExtension; //md5值拼接文件后缀 NSString *filename = [NSString stringWithFormat:@"%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%@", r[0], r[1], r[2], r[3], r[4], r[5], r[6], r[7], r[8], r[9], r[10], r[11], r[12], r[13], r[14], r[15], ext.length == 0 ? @"" : [NSString stringWithFormat:@".%@", ext]]; return filename; }
-
异步缓存图片
- (void)storeImage:(nullable UIImage *)image imageData:(nullable NSData *)imageData forKey:(nullable NSString *)key toDisk:(BOOL)toDisk completion:(nullable SDWebImageNoParamsBlock)completionBlock { if (!image || !key) { if (completionBlock) { completionBlock(); } return; } // 如果允许内存缓存,先把图片缓存到内存 if (self.config.shouldCacheImagesInMemory) { NSUInteger cost = SDCacheCostForImage(image); [self.memCache setObject:image forKey:key cost:cost]; } if (toDisk) { dispatch_async(self.ioQueue, ^{ //一般图片的大小都不会很小,对图片进行编码过程中也会产出一些开销不小的临时对象,在子线程中添加自动释放池,可以提前释放这些对象,缓解内存压力。 @autoreleasepool { NSData *data = imageData; if (!data && image) { SDImageFormat format; if (SDCGImageRefContainsAlpha(image.CGImage)) { format = SDImageFormatPNG; } else { format = SDImageFormatJPEG; } //把图片进行编码,得到可以存储的二进制数据 data = [[SDWebImageCodersManager sharedInstance] encodedDataWithImage:image format:format]; } [self _storeImageDataToDisk:data forKey:key]; } if (completionBlock) { dispatch_async(dispatch_get_main_queue(), ^{ completionBlock(); }); } }); } else { if (completionBlock) { completionBlock(); } } }
内部方法:使用的把图片二进制数据存入硬盘
- (void)_storeImageDataToDisk:(nullable NSData *)imageData forKey:(nullable NSString *)key { if (!imageData || !key) { return; } if (![self.fileManager fileExistsAtPath:_diskCachePath]) { //如果还没有缓存目录,通过fileManager生成缓存目录 [self.fileManager createDirectoryAtPath:_diskCachePath withIntermediateDirectories:YES attributes:nil error:NULL]; } // 获取缓存的文件名,对key进行md5加密 NSString *cachePathForKey = [self defaultCachePathForKey:key]; NSURL *fileURL = [NSURL fileURLWithPath:cachePathForKey]; //保存到硬盘 [imageData writeToURL:fileURL options:self.config.diskCacheWritingOptions error:nil]; // 禁止iCloud备份 if (self.config.shouldDisableiCloud) { [fileURL setResourceValue:@YES forKey:NSURLIsExcludedFromBackupKey error:nil]; } }
-
因为从硬盘查询是否有缓存图片会是一个比较耗时的操作,所以SDImageCache还提供了异步的方式,通过block回调查询结果
- (void)diskImageExistsWithKey:(nullable NSString *)key completion:(nullable SDWebImageCheckCacheCompletionBlock)completionBlock { dispatch_async(self.ioQueue, ^{ BOOL exists = [self _diskImageDataExistsWithKey:key]; if (completionBlock) { dispatch_async(dispatch_get_main_queue(), ^{ completionBlock(exists); }); } }); }
-
在命名空间指定的目录和自定义只读路径中查询获取图片
- (nullable NSData *)diskImageDataBySearchingAllPathsForKey:(nullable NSString *)key { NSString *defaultPath = [self defaultCachePathForKey:key]; //在缓存目录查找获取图片 NSData *data = [NSData dataWithContentsOfFile:defaultPath options:self.config.diskCacheReadingOptions error:nil]; if (data) { return data; } data = [NSData dataWithContentsOfFile:defaultPath.stringByDeletingPathExtension options:self.config.diskCacheReadingOptions error:nil]; if (data) { return data; } //在自定义的只读路径中查询获取图片 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; } } return nil; }
-
从硬盘中获取图片(同步的方式获取),查询到图片后shouldCacheImagesInMemory=YES同步保存到内存缓存中,下次再获取的时候直接从内存中获取
- (nullable UIImage *)imageFromDiskCacheForKey:(nullable NSString *)key { UIImage *diskImage = [self diskImageForKey:key]; if (diskImage && self.config.shouldCacheImagesInMemory) { NSUInteger cost = SDCacheCostForImage(diskImage); //同步缓存到内存 [self.memCache setObject:diskImage forKey:key cost:cost]; } return diskImage; } - (nullable UIImage *)imageFromCacheForKey:(nullable NSString *)key { //先从内存中尝试获取缓存图片 UIImage *image = [self imageFromMemoryCacheForKey:key]; if (image) { return image; } //内存中没有缓存,尝试从硬盘中获取 imageFromDiskCacheForKey 就是上面的方法 image = [self imageFromDiskCacheForKey:key]; return image; } - (nullable UIImage *)diskImageForKey:(nullable NSString *)key { NSData *data = [self diskImageDataForKey:key]; return [self diskImageForKey:key data:data]; } - (nullable UIImage *)diskImageForKey:(nullable NSString *)key data:(nullable NSData *)data { return [self diskImageForKey:key data:data options:0]; } - (nullable UIImage *)diskImageForKey:(nullable NSString *)key data:(nullable NSData *)data options:(SDImageCacheOptions)options { if (data) { //实例化一个uiimage,此时的uiimage还没有进行解码 UIImage *image = [[SDWebImageCodersManager sharedInstance] decodedImageWithData:data]; image = [self scaledImageForKey:key image:image]; if (self.config.shouldDecompressImages) { //对image进行预解码 BOOL shouldScaleDown = options & SDImageCacheScaleDownLargeImages; image = [[SDWebImageCodersManager sharedInstance] decompressedImageWithImage:image data:&data options:@{SDWebImageCoderScaleDownLargeImagesKey: @(shouldScaleDown)}]; } return image; } else { return nil; } }
-
异步的方式获取硬盘缓存图片
- (nullable NSOperation *)queryCacheOperationForKey:(nullable NSString *)key options:(SDImageCacheOptions)options done:(nullable SDCacheQueryCompletedBlock)doneBlock { if (!key) { if (doneBlock) { doneBlock(nil, nil, SDImageCacheTypeNone); } return nil; } // 首先检查内存中是否有缓存的图片 UIImage *image = [self imageFromMemoryCacheForKey:key]; //当内存中有缓存,并且SDImageCacheQueryDataWhenInMemory(内存中有缓存仍然查询硬盘缓存)为false,立即返回内存缓存的图片 BOOL shouldQueryMemoryOnly = (image && !(options & SDImageCacheQueryDataWhenInMemory)); if (shouldQueryMemoryOnly) { if (doneBlock) { doneBlock(image, nil, SDImageCacheTypeMemory); } return nil; } NSOperation *operation = [NSOperation new]; void(^queryDiskBlock)(void) = ^{ if (operation.isCancelled) { // operation取消,直接返回 return; } @autoreleasepool { //从硬盘获取缓存 NSData *diskData = [self diskImageDataBySearchingAllPathsForKey:key]; UIImage *diskImage; SDImageCacheType cacheType = SDImageCacheTypeDisk; if (image) { // 如果内存本来就有缓存图片,则把内存缓存的图片返回,可以省去对图片再一次解码 diskImage = image; cacheType = SDImageCacheTypeMemory; } else if (diskData) { // 如果内存没有缓存的图片,那么从硬盘得到的是图片压缩的二进制数据,使用前需要先解码 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); }); } } } }; if (options & SDImageCacheQueryDiskSync) { queryDiskBlock(); } else { dispatch_async(self.ioQueue, queryDiskBlock); } return operation; }
- 从缓存中移除图片
- (void)removeImageForKey:(nullable NSString *)key fromDisk:(BOOL)fromDisk withCompletion:(nullable SDWebImageNoParamsBlock)completion {
if (key == nil) {
return;
}
if (self.config.shouldCacheImagesInMemory) {
[self.memCache removeObjectForKey:key];
}
if (fromDisk) {
dispatch_async(self.ioQueue, ^{
[self.fileManager removeItemAtPath:[self defaultCachePathForKey:key] error:nil];
if (completion) {
dispatch_async(dispatch_get_main_queue(), ^{
completion();
});
}
});
} else if (completion){
completion();
}
}
- 清空缓存
//清空内存缓存
- (void)clearMemory {
[self.memCache removeAllObjects];
}
//清空硬盘缓存
- (void)clearDiskOnCompletion:(nullable SDWebImageNoParamsBlock)completion {
dispatch_async(self.ioQueue, ^{
[self.fileManager removeItemAtPath:self.diskCachePath error:nil];
[self.fileManager createDirectoryAtPath:self.diskCachePath
withIntermediateDirectories:YES
attributes:nil
error:NULL];
if (completion) {
dispatch_async(dispatch_get_main_queue(), ^{
completion();
});
}
});
}
- SDImageCache在app退出,或者进入到后台,都会对缓存目录下的图片进行清理,把过期的图片移除掉
- (void)deleteOldFilesWithCompletionBlock:(nullable SDWebImageNoParamsBlock)completionBlock {
dispatch_async(self.ioQueue, ^{
NSURL *diskCacheURL = [NSURL fileURLWithPath:self.diskCachePath isDirectory:YES];
NSURLResourceKey cacheContentDateKey = NSURLContentModificationDateKey;
switch (self.config.diskCacheExpireType) {
case SDImageCacheConfigExpireTypeAccessDate:
//最近访问的时间到现在作为过期时间
cacheContentDateKey = NSURLContentAccessDateKey;
break;
case SDImageCacheConfigExpireTypeModificationDate:
//最近修改的时间到现在作为过期时间
cacheContentDateKey = NSURLContentModificationDateKey;
break;
default:
break;
}
NSArray<NSString *> *resourceKeys = @[NSURLIsDirectoryKey, cacheContentDateKey, NSURLTotalFileAllocatedSizeKey];
// This enumerator prefetches useful properties for our cache files.
NSDirectoryEnumerator *fileEnumerator = [self.fileManager enumeratorAtURL:diskCacheURL
includingPropertiesForKeys:resourceKeys
options:NSDirectoryEnumerationSkipsHiddenFiles
errorHandler:NULL];
//通过SDImageCacheConfig.maxCacheAge计算出该被移除的缓存时间边界值
NSDate *expirationDate = [NSDate dateWithTimeIntervalSinceNow:-self.config.maxCacheAge];
//用来保存未过期的缓存图片
NSMutableDictionary<NSURL *, NSDictionary<NSString *, id> *> *cacheFiles = [NSMutableDictionary dictionary];
//用来记录当前缓存大小
NSUInteger currentCacheSize = 0;
//用来保存过期图片的url
NSMutableArray<NSURL *> *urlsToDelete = [[NSMutableArray alloc] init];
// 在缓存目录下遍历图片,把过期的缓存图片移除掉,把未过期的图片添加到cacheFiles来进行下一步的清理工作(以size作为清理标准)
for (NSURL *fileURL in fileEnumerator) {
NSError *error;
NSDictionary<NSString *, id> *resourceValues = [fileURL resourceValuesForKeys:resourceKeys error:&error];
// 跳过文件夹
if (error || !resourceValues || [resourceValues[NSURLIsDirectoryKey] boolValue]) {
continue;
}
// 移除过期的图片
NSDate *modifiedDate = resourceValues[cacheContentDateKey];
//laterDate:返回的是较晚的时间,如果修改时间比上面计算的缓存时间边界值还早,就说明该缓存图片已经过期了,添加到urlsToDelete等待删除
if ([[modifiedDate laterDate:expirationDate] isEqualToDate:expirationDate]) {
[urlsToDelete addObject:fileURL];
continue;
}
NSNumber *totalAllocatedSize = resourceValues[NSURLTotalFileAllocatedSizeKey];
currentCacheSize += totalAllocatedSize.unsignedIntegerValue;
cacheFiles[fileURL] = resourceValues;
}
//循环遍历,移除过期的图片
for (NSURL *fileURL in urlsToDelete) {
[self.fileManager removeItemAtURL:fileURL error:nil];
}
// 如果我们设置了SDImageCacheConfig.maxCacheSize,并且当前缓存目录的大小大于config.maxCacheSize,需要对缓存目录进行二次清理,直到缓存目录大小 <= config.maxCacheSize/2
if (self.config.maxCacheSize > 0 && currentCacheSize > self.config.maxCacheSize) {
const NSUInteger desiredCacheSize = self.config.maxCacheSize / 2;
//把缓存的图片从新排序,较早的图片放在前面
NSArray<NSURL *> *sortedFiles = [cacheFiles keysSortedByValueWithOptions:NSSortConcurrent
usingComparator:^NSComparisonResult(id obj1, id obj2) {
return [obj1[NSURLContentModificationDateKey] compare:obj2[NSURLContentModificationDateKey]];
}];
// 删除图片知道缓存目录大小 <= config.maxCacheSize/2
for (NSURL *fileURL in sortedFiles) {
if ([self.fileManager removeItemAtURL:fileURL error:nil]) {
NSDictionary<NSString *, id> *resourceValues = cacheFiles[fileURL];
NSNumber *totalAllocatedSize = resourceValues[NSURLTotalFileAllocatedSizeKey];
currentCacheSize -= totalAllocatedSize.unsignedIntegerValue;
if (currentCacheSize < desiredCacheSize) {
break;
}
}
}
}
if (completionBlock) {
dispatch_async(dispatch_get_main_queue(), ^{
completionBlock();
});
}
});
}
- 同步的方式获取缓存目录的大小和文件数
- (NSUInteger)getSize {
__block NSUInteger size = 0;
dispatch_sync(self.ioQueue, ^{
NSDirectoryEnumerator *fileEnumerator = [self.fileManager enumeratorAtPath:self.diskCachePath];
for (NSString *fileName in fileEnumerator) {
NSString *filePath = [self.diskCachePath stringByAppendingPathComponent:fileName];
NSDictionary<NSString *, id> *attrs = [self.fileManager attributesOfItemAtPath:filePath error:nil];
size += [attrs fileSize];
}
});
return size;
}
- (NSUInteger)getDiskCount {
__block NSUInteger count = 0;
dispatch_sync(self.ioQueue, ^{
NSDirectoryEnumerator *fileEnumerator = [self.fileManager enumeratorAtPath:self.diskCachePath];
count = fileEnumerator.allObjects.count;
});
return count;
}
- 异步的方式获取缓存目录的大小和文件数,block回调
- (void)calculateSizeWithCompletionBlock:(nullable SDWebImageCalculateSizeBlock)completionBlock {
NSURL *diskCacheURL = [NSURL fileURLWithPath:self.diskCachePath isDirectory:YES];
dispatch_async(self.ioQueue, ^{
NSUInteger fileCount = 0;
NSUInteger totalSize = 0;
NSDirectoryEnumerator *fileEnumerator = [self.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);
});
}
});
}