SDWebImage源码阅读(六)SDImageCacheConfig/SDImageCache(下)
在上篇中已经了解分析了 SDImageCache.h 文件中所有的方法和属性。大概对 SDImageCache 能实现的功能已经有了全面的认识。在这篇则着重学习研究这些功能的实现过程和实现原理。
SDImageCache 是 SDWebImage 里面用来做缓存的类,虽然只是针对的图片的缓存,但是其实在 iOS 开发甚至在程序开发中,缓存类对缓存文件的类型区别而要单独针对文件类型做处理的需要并不多,不管是图片的缓存还是别的类型文件的缓存,它们在缓存原理上是基本一致的。通过对 SDImageCache 针对图片的缓存处理的实现的学习,在以后有开发需求要做别的类型文件的缓存处理的时候,都可以对其模仿和学习,创建类似的缓存管理类。
下面开始学习 SDImageCache.m 的代码:
首先是在 SDImageCache.m 内部嵌套定义了一个继承自 NSCache 的类 AutoPurgeCache。
NSCache 在收到内存警告的时候会释放自身的一部分资源,而设计 AutoPurgeCache 的目的是,在收到内存警告的时候,释放内存缓存中的所有资源。
AutoPurgeCache 类 .h/.m 全部的内容:
1 // See https://github.com/rs/SDWebImage/pull/1141 for discussion 2 @interface AutoPurgeCache : NSCache 3 @end 4 5 @implementation AutoPurgeCache 6 7 - (nonnull instancetype)init { 8 self = [super init]; 9 if (self) { 10 #if SD_UIKIT 11 [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(removeAllObjects) name:UIApplicationDidReceiveMemoryWarningNotification object:nil]; 12 #endif 13 } 14 return self; 15 } 16 17 - (void)dealloc { 18 #if SD_UIKIT 19 [[NSNotificationCenter defaultCenter] removeObserver:self name:UIApplicationDidReceiveMemoryWarningNotification object:nil]; 20 #endif 21 } 22 23 @end
如果是当前开发平台包含 UIKit框架,在 AutoPurgeCache 类的 init 方法里面添加一个名字为
UIApplicationDidReceiveMemoryWarningNotification 的通知,接收到通知的时候执行
removeAllObjects 方法,删除缓存的所有资源。
在该类的 dealloc 方法里面,移除名字是
UIApplicationDidReceiveMemoryWarningNotification 的通知。
UIApplicationDidReceiveMemoryWarningNotification 是定义在 UIApplication.h 里面的一个不可变的字符串常量:
1 UIKIT_EXTERN NSNotificationName const UIApplicationDidReceiveMemoryWarningNotification;
2 typedef NSString *NSNotificationName NS_EXTENSIBLE_STRING_ENUM;
当应用程序接收到内存警告是会发送该通知,让应用程序清理内存。
接下来学习一下 NSCache 这个类:
NSCache 是包含在 Foundation 框架里面的一个类,它的 .h 全部代码才 40 多行:
1 #import <Foundation/NSObject.h> 2 3 @class NSString; 4 @protocol NSCacheDelegate; 5 6 NS_ASSUME_NONNULL_BEGIN 7 8 NS_CLASS_AVAILABLE(10_6, 4_0) 9 @interface NSCache <KeyType, ObjectType> : NSObject { 10 @private 11 id _delegate; 12 void *_private[5]; 13 void *_reserved; 14 } 15 16 @property (copy) NSString *name; 17 18 @property (nullable, assign) id<NSCacheDelegate> delegate; 19 20 - (nullable ObjectType)objectForKey:(KeyType)key; 21 - (void)setObject:(ObjectType)obj forKey:(KeyType)key; // 0 cost 22 - (void)setObject:(ObjectType)obj forKey:(KeyType)key cost:(NSUInteger)g; 23 - (void)removeObjectForKey:(KeyType)key; 24 25 - (void)removeAllObjects; 26 27 @property NSUInteger totalCostLimit; // limits are imprecise/not strict 28 @property NSUInteger countLimit; // limits are imprecise/not strict 29 @property BOOL evictsObjectsWithDiscardedContent; 30 31 @end 32 33 @protocol NSCacheDelegate <NSObject> 34 @optional 35 - (void)cache:(NSCache *)cache willEvictObject:(id)obj; 36 @end 37 38 NS_ASSUME_NONNULL_END
NSCache 简要介绍:
NSCache 是官方提供的缓存类,具体的使用和 NSMutableDictionary 类似,在 AFNetWorking 和 SDWebImage 里面用来管理缓存。
NSCache 在系统内存很低时,会自动释放对象(模拟器不会释放),建议在接收到内存警告时主动调用 removeAllObject 方法释放对象。
NSCache 是线程安全的,在多线程操作的时候,不需要对 NSCache 加锁。
NSCache 的 key 只是对对象进行 Strong 引用,不是拷贝,在清理的时候计算的是实际大小而不是引用的大小。
NSCache 具有自动删除的功能,以减少系统占用的内存。
NSCache 的键对象不会像 NSMutableDictionary 中那样被复制。(键不需要实现 NSCopying 协议)
NSCache 的属性和方法:
1 @property NSUInteger totalCostLimit; // limits are imprecise/not strict
totalCostLimit 设置缓存占用的内存大小,并不是一个严格限制,当内存占用超过了 totalCostLimit 设定的值,系统会清空一部分缓存,直至总消耗低于 totalCostLimit 的值。默认值是 0 ,表示没有限制。
1 @property (copy) NSString *name;
名称。
1 @property (nullable, assign) id<NSCacheDelegate> delegate;
设置代理。
1 @property NSUInteger countLimit; // limits are imprecise/not strict
countLimit 能够缓存的对象的最大数量,这也不是一个严格的控制。默认值是 0,表示没有限制。
1 @property BOOL evictsObjectsWithDiscardedContent;
evictsObjectsWithDiscardedContent 用来标识缓存是否自动舍弃那些内存已经被丢弃的对象(默认该属性为 YES),如果为 YES,则在对象的内存被丢弃时舍弃对象。标识缓存是否回收废弃的内容。
1 - (nullable ObjectType)objectForKey:(KeyType)key;
获取缓存对象,基于 key-value 对。
1 - (void)setObject:(ObjectType)obj forKey:(KeyType)key; // 0 cost
在缓存中设置指定键名对应的值存储,考虑缓存的限制属性。
1 - (void)setObject:(ObjectType)obj forKey:(KeyType)key cost:(NSUInteger)g;
在缓存中设置指定键名对应的值存储,cost 是提前知道该缓存对象占用的字节数,用于计算记录在缓存中的所有对象的缓存大小,也会考虑缓存的限制属性,建议直接使用上个没有 cost 参数的方法。
当出现内存警告或者超出缓存限制的时候,缓存会开启一个回收过程删除部分对象,释放内存。
1 - (void)removeObjectForKey:(KeyType)key;
删除缓存中指定键名的对象。
1 - (void)removeAllObjects;
删除缓存中所有的对象。
NSCacheDelegate 代理
1 @protocol NSCacheDelegate <NSObject> 2 @optional 3 - (void)cache:(NSCache *)cache willEvictObject:(id)obj; 4 @end
实现 NSCacheDelegate 代理的对象,在缓存对象即将被清理的时候,系统回调代理方法。
第一个参数:是当前的缓存(NSCache),不要修改该对象。
第二个参数:是当前将要被清理的对象,如果需要存储该对象,可以在这里操作(把该对象存入 Sqlite 或者 CoreData)。
该代理方法会在缓存对象即将被清理的时候调用,如下场景会调用:
1. 使用 - (void)removeObjectForKey:(KeyType)key; 手动删除指定对象的时候。
2.缓存对象超过了 NSCache 的属性限制(totalCostLimit 和 countLimit)。
3.App 进入后台的时候会调用。
4.系统发出内存警告的时候(UIApplicationDidReceiveMemoryWarningNotification)。
下面接着看 SDImageCache.m 的代码:
计算一个 UIImage 的 SDCacheCostForImage(UIImage *image)
图片在缓存中的大小是通过像素来衡量的。
1 FOUNDATION_STATIC_INLINE NSUInteger SDCacheCostForImage(UIImage *image) { 2 #if SD_MAC 3 return image.size.height * image.size.width; 4 #elif SD_UIKIT || SD_WATCH 5 return image.size.height * image.size.width * image.scale * image.scale; 6 #endif 7 }
FOUNDATION_STATIC_INLINE
1 #if !defined(FOUNDATION_STATIC_INLINE) 2 #define FOUNDATION_STATIC_INLINE static __inline__ 3 #endif
FOUNDATION_STATIC_INLINE 是 Foundation 框架中 NSObjCRuntime.h 里面定义内联函数的前缀修饰符。
static __inline__ 表示该函数是一个具有文件内部访问权限的内联函数,所谓的内联函数就是建议编译器在调用时将函数展开。建议的意思就是说编译器不一定会按照你的建议做。因此内联函数尽量不要写的太复杂。
Properties
1 #pragma mark - Properties 2 @property (strong, nonatomic, nonnull) NSCache *memCache; 3 @property (strong, nonatomic, nonnull) NSString *diskCachePath; 4 @property (strong, nonatomic, nullable) NSMutableArray<NSString *> *customPaths; 5 @property (SDDispatchQueueSetterSementics, nonatomic, nullable) dispatch_queue_t ioQueue;
1 @implementation SDImageCache { 2 NSFileManager *_fileManager; 3 }
memCache NSCache 类型的变量,表示内存容器。
diskCachePath 这个应该能望名知意,表示磁盘缓存的路径。
customPaths 这是一个里面存放 NSString 的字符串可变数组,表示里面存放的都是自定义的缓存路径,当扫描图片和向磁盘缓存图片的时候要用到这些指定的路径。- (void)addReadOnlyCachePath:(nonnull NSString *)path; 这个方法即向 customPaths 数组里面添加自定义的路径,当读取图片的时候,customPaths 数组里面的所有路径都会被扫描。
ioQueue 这是用于输入和输出的队列,队列其实往往可以当做一种"锁"来使用,把某些任务放在串行队列里面按照顺序一步一步的执行,必须考虑线程是否安全。
_fileManager 表示一个 NSFileManager 文件管理者。
Singleton, init, dealloc
1 #pragma mark - Singleton, init, dealloc 2 3 + (nonnull instancetype)sharedImageCache { 4 static dispatch_once_t once; 5 static id instance; 6 dispatch_once(&once, ^{ 7 instance = [self new]; 8 }); 9 return instance; 10 } 11 12 - (instancetype)init { 13 return [self initWithNamespace:@"default"]; 14 } 15 16 - (nonnull instancetype)initWithNamespace:(nonnull NSString *)ns { 17 NSString *path = [self makeDiskCachePath:ns]; 18 return [self initWithNamespace:ns diskCacheDirectory:path]; 19 } 20 21 - (nonnull instancetype)initWithNamespace:(nonnull NSString *)ns 22 diskCacheDirectory:(nonnull NSString *)directory { 23 if ((self = [super init])) { 24 NSString *fullNamespace = [@"com.hackemist.SDWebImageCache." stringByAppendingString:ns]; 25 26 // Create IO serial queue 27 _ioQueue = dispatch_queue_create("com.hackemist.SDWebImageCache", DISPATCH_QUEUE_SERIAL); 28 29 _config = [[SDImageCacheConfig alloc] init]; 30 31 // Init the memory cache 32 _memCache = [[AutoPurgeCache alloc] init]; 33 _memCache.name = fullNamespace; 34 35 // Init the disk cache 36 if (directory != nil) { 37 _diskCachePath = [directory stringByAppendingPathComponent:fullNamespace]; 38 } else { 39 NSString *path = [self makeDiskCachePath:ns]; 40 _diskCachePath = path; 41 } 42 43 dispatch_sync(_ioQueue, ^{ 44 _fileManager = [NSFileManager new]; 45 }); 46 47 #if SD_UIKIT 48 // Subscribe to app events 49 [[NSNotificationCenter defaultCenter] addObserver:self 50 selector:@selector(clearMemory) 51 name:UIApplicationDidReceiveMemoryWarningNotification 52 object:nil]; 53 54 [[NSNotificationCenter defaultCenter] addObserver:self 55 selector:@selector(deleteOldFiles) 56 name:UIApplicationWillTerminateNotification 57 object:nil]; 58 59 [[NSNotificationCenter defaultCenter] addObserver:self 60 selector:@selector(backgroundDeleteOldFiles) 61 name:UIApplicationDidEnterBackgroundNotification 62 object:nil]; 63 #endif 64 } 65 66 return self; 67 } 68 69 - (void)dealloc { 70 [[NSNotificationCenter defaultCenter] removeObserver:self]; 71 SDDispatchQueueRelease(_ioQueue); 72 } 73 74 - (void)checkIfQueueIsIOQueue { 75 const char *currentQueueLabel = dispatch_queue_get_label(DISPATCH_CURRENT_QUEUE_LABEL); 76 const char *ioQueueLabel = dispatch_queue_get_label(self.ioQueue); 77 if (strcmp(currentQueueLabel, ioQueueLabel) != 0) { 78 NSLog(@"This method should be called from the ioQueue"); 79 } 80 }
这一部分的代码,首先时单例方法 + (nonnull instancetype)sharedImageCache
的实现,在 dispatch_once_t 中创建全程序使用的 SDWebCache 类。下面是 SDWebImage 的三个初始化方法,方法1 和 方法2 最终都会调用方法3 完成初始化。下面着重看一下方法3 的初始化过程:
- (nonnull instancetype)initWithNamespace:(nonnull NSString *)ns
diskCacheDirectory:(nonnull NSString *)directory{}
1 NSString *fullNamespace = [@"com.hackemist.SDWebImageCache." stringByAppendingString:ns];
@"com.hackemist.SDWebImageCache." 与传入的 Namespace 拼接成完整的命名空间赋值给字符串变量 fullNamespace,使用 init 初始化方法时 Namespace 默认使用 @"default",此时 fullNamespace 值等于 @"com.hackemist.SDWebImageCache.default"。
1 // Create IO serial queue 2 _ioQueue = dispatch_queue_create("com.hackemist.SDWebImageCache", DISPATCH_QUEUE_SERIAL);
初始化串行队列 _ioQueue。
1 _config = [[SDImageCacheConfig alloc] init];
初始化 SDImageCache 的配置类 _config。(SDImageCacheConfig 类)
1 // Init the memory cache 2 _memCache = [[AutoPurgeCache alloc] init]; 3 _memCache.name = fullNamespace;
初始化 _memCache(AutoPurgeCache 类),并把 fullNameSpace 赋值为 _memCache 的 name 属性。_memCache 的主要作用是当收到内存警告的时候,释放内存中与 SDWebImage 相关的缓存中的所有资源。
1 // Init the disk cache 2 if (directory != nil) { 3 _diskCachePath = [directory stringByAppendingPathComponent:fullNamespace]; 4 } else { 5 NSString *path = [self makeDiskCachePath:ns]; 6 _diskCachePath = path; 7 } 8 9 dispatch_sync(_ioQueue, ^{ 10 _fileManager = [NSFileManager new]; 11 });
判断 directory 是否为 nil,directory 是没有拼接 fullNamespace 的磁盘缓存的路径,不为空的时候(自定义的缓存路径,默认的是存在 Library 下的 Caches 文件夹里面)就直接拼接 fullNamespace,这里需要注意一下 - (NSString *)stringByAppendingPathComponent:(NSString *)str; 这个方法是 Foundation 框架下的 NSPathUtilities 类下面的方法,它会自动在 directory 末尾和 fullNamespace 中间加 "/"。当 directory 为 nil 的时候,path 是默认的路径,即系统 Library 下的 Caches 拼接 fullNamespace 组成的路径。
1 // /Users/jay/Library/Developer/CoreSimulator/Devices/74A07152-3D06-44F3-9DE6-C4B1778BC448/data/Containers/Data/Application/647E08DA-FAED-49F8-A36C-765962B83117/Library/Caches/com.hackemist.SDWebImageCache.default
在 _ioQueue 队列同步初始化 _fileManager。
1 #if SD_UIKIT 2 // Subscribe to app events 3 [[NSNotificationCenter defaultCenter] addObserver:self 4 selector:@selector(clearMemory) 5 name:UIApplicationDidReceiveMemoryWarningNotification 6 object:nil]; 7 8 [[NSNotificationCenter defaultCenter] addObserver:self 9 selector:@selector(deleteOldFiles) 10 name:UIApplicationWillTerminateNotification 11 object:nil]; 12 13 [[NSNotificationCenter defaultCenter] addObserver:self 14 selector:@selector(backgroundDeleteOldFiles) 15 name:UIApplicationDidEnterBackgroundNotification 16 object:nil]; 17 #endif
这里判断是否 SD_UIKIT 为真,即当前的开发平台是 iOS 或者 TV,如果为真添加了三个监听通知,
当发送 UIApplicationDidReceiveMemoryWarningNotification 通知时执行 clearMemory,
当发送 UIApplicationWillTerminateNotification(程序将要结束时)通知时执行 deleteOldFiles,
当发送 UIApplicationDidEnterBackgroundNotification (程序进入后台)通知时执行
backgroundDeleteOldFiles。
1 - (void)dealloc { 2 [[NSNotificationCenter defaultCenter] removeObserver:self]; 3 SDDispatchQueueRelease(_ioQueue); 4 }
在 SDImageCache 类销毁的时候,移除所有的通知,SDDispatchQueueRelease 是在 SDWebImageCompat 里面定义的用来将 GCD 对象释放的宏,GCD 中的对象在 iOS 6.0 之前是不参与 ARC 的,而在 6.0 之后,在 ARC 下使用 GCD 是不用关心释放问题。
1 - (void)checkIfQueueIsIOQueue { 2 const char *currentQueueLabel = dispatch_queue_get_label(DISPATCH_CURRENT_QUEUE_LABEL); 3 const char *ioQueueLabel = dispatch_queue_get_label(self.ioQueue); 4 if (strcmp(currentQueueLabel, ioQueueLabel) != 0) { 5 NSLog(@"This method should be called from the ioQueue"); 6 } 7 }
使用 dispatch_queue_get_label 返回创建队列时的添加的自定义的队列的标识(const char *_Nullable label),主队列返回的是: "com.apple.main-thread"。_ioQueue 返回的是 "com.hackemist.SDWebImageCache",使用 DISPATCH_CURRENT_QUEUE_LABEL 做参数可返回当前队列的标识。由此检查当前队列是不是 _ioQueue。
Cache paths
1 #pragma mark - Cache paths 2 3 - (void)addReadOnlyCachePath:(nonnull NSString *)path { 4 if (!self.customPaths) { 5 self.customPaths = [NSMutableArray new]; 6 } 7 8 if (![self.customPaths containsObject:path]) { 9 [self.customPaths addObject:path]; 10 } 11 } 12 13 - (nullable NSString *)cachePathForKey:(nullable NSString *)key inPath:(nonnull NSString *)path { 14 NSString *filename = [self cachedFileNameForKey:key]; 15 return [path stringByAppendingPathComponent:filename]; 16 } 17 18 - (nullable NSString *)defaultCachePathForKey:(nullable NSString *)key { 19 return [self cachePathForKey:key inPath:self.diskCachePath]; 20 } 21 22 - (nullable NSString *)cachedFileNameForKey:(nullable NSString *)key { 23 const char *str = key.UTF8String; 24 if (str == NULL) { 25 str = ""; 26 } 27 unsigned char r[CC_MD5_DIGEST_LENGTH]; 28 CC_MD5(str, (CC_LONG)strlen(str), r); 29 NSString *filename = [NSString stringWithFormat:@"%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%@", 30 r[0], r[1], r[2], r[3], r[4], r[5], r[6], r[7], r[8], r[9], r[10], 31 r[11], r[12], r[13], r[14], r[15], [key.pathExtension isEqualToString:@""] ? @"" : [NSString stringWithFormat:@".%@", key.pathExtension]]; 32 33 return filename; 34 } 35 36 - (nullable NSString *)makeDiskCachePath:(nonnull NSString*)fullNamespace { 37 NSArray<NSString *> *paths = NSSearchPathForDirectoriesInDomains(NSCachesDirectory, NSUserDomainMask, YES); 38 return [paths[0] stringByAppendingPathComponent:fullNamespace]; 39 }
这一部分的代码主要是和缓存路径相关的操作。
- (void)addReadOnlyCachePath:(nonnull NSString *)path {} 首先是添加自定义的缓存路径进 _customPaths 数组里面。SDWebImage 除了使用自己默认的 Library/Caches 路径,还能把自定义的路径指定为它默认的缓存路径或者其它的缓存路径。
1 - (nullable NSString *)cachedFileNameForKey:(nullable NSString *)key { 2 const char *str = key.UTF8String; 3 if (str == NULL) { 4 str = ""; 5 } 6 unsigned char r[CC_MD5_DIGEST_LENGTH]; 7 CC_MD5(str, (CC_LONG)strlen(str), r); 8 NSString *filename = [NSString stringWithFormat:@"%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%@", 9 r[0], r[1], r[2], r[3], r[4], r[5], r[6], r[7], r[8], r[9], r[10], 10 r[11], r[12], r[13], r[14], r[15], [key.pathExtension isEqualToString:@""] ? @"" : [NSString stringWithFormat:@".%@", key.pathExtension]]; 11 12 return filename; 13 }
主要先看一下这个方法的实现,这个方法的主要功能是根据图片的 URL 返回一个 MD5 处理的文件名。
这里的参数 key 多为图片的 URL,把图片的 URL 使用 MD5 方式转化,同时当 URL 有后缀的时候,做加点处理。
1 - (nullable NSString *)makeDiskCachePath:(nonnull NSString*)fullNamespace { 2 NSArray<NSString *> *paths = NSSearchPathForDirectoriesInDomains(NSCachesDirectory, NSUserDomainMask, YES); 3 return [paths[0] stringByAppendingPathComponent:fullNamespace]; 4 }
这个方法主要是根据传进来的命名空间 fullNamespace (默认的命名空间 @"com.hackemist.SDWebImageCache.default")拼接 Library/Caches 返回图片磁盘缓存的路径。
1 - (nullable NSString *)cachePathForKey:(nullable NSString *)key inPath:(nonnull NSString *)path { 2 NSString *filename = [self cachedFileNameForKey:key]; 3 return [path stringByAppendingPathComponent:filename]; 4 }
获取某个键的缓存路径(需要缓存路径根文件夹)。
根据传入的 key 获得文件名然后和缓存路径的根文件夹的路径 path(根据参数 path 传入,多为默认的缓存路径,或者自己创建并指定的缓存路径) 拼接,返回 key 指定的图像的完整路径。
1 - (nullable NSString *)defaultCachePathForKey:(nullable NSString *)key { 2 return [self cachePathForKey:key inPath:self.diskCachePath]; 3 }
获取某个键的默认的缓存路径。
缓存路径存在的情况:
1.使用 - (instancetype)init 方法初始化 SDImageCache 的时候:
Library/Caches / default / com.hackemist.SDWebImageCache.default
2.使用 - (nonnull instancetype)initWithNamespace:(nonnull NSString *)ns 初始化的时候:
Library/Caches / ns / com.hackemist.SDWebImageCache.ns
3.- (nonnull instancetype)initWithNamespace:(nonnull NSString *)ns diskCacheDirectory:(nonnull NSString *)directory 初始化的时候:
ns 不是空 directory 不是空的时候:
directory / com.hackemist.SDWebImageCache.ns
ns 不是空 directory 是空 的时候:
Library/Caches / ns
ns 是空 directory 不是空的时候:
directory / com.hackemist.SDWebImageCache
ns 是空 directory 是空的时候:
Library/Caches
Store Ops
1 #pragma mark - Store Ops 2 3 - (void)storeImage:(nullable UIImage *)image 4 forKey:(nullable NSString *)key 5 completion:(nullable SDWebImageNoParamsBlock)completionBlock { 6 [self storeImage:image imageData:nil forKey:key toDisk:YES completion:completionBlock]; 7 } 8 9 - (void)storeImage:(nullable UIImage *)image 10 forKey:(nullable NSString *)key 11 toDisk:(BOOL)toDisk 12 completion:(nullable SDWebImageNoParamsBlock)completionBlock { 13 [self storeImage:image imageData:nil forKey:key toDisk:toDisk completion:completionBlock]; 14 } 15 16 - (void)storeImage:(nullable UIImage *)image 17 imageData:(nullable NSData *)imageData 18 forKey:(nullable NSString *)key 19 toDisk:(BOOL)toDisk 20 completion:(nullable SDWebImageNoParamsBlock)completionBlock { 21 if (!image || !key) { 22 if (completionBlock) { 23 completionBlock(); 24 } 25 return; 26 } 27 // if memory cache is enabled 28 if (self.config.shouldCacheImagesInMemory) { 29 NSUInteger cost = SDCacheCostForImage(image); 30 [self.memCache setObject:image forKey:key cost:cost]; 31 } 32 33 if (toDisk) { 34 dispatch_async(self.ioQueue, ^{ 35 NSData *data = imageData; 36 37 if (!data && image) { 38 SDImageFormat imageFormatFromData = [NSData sd_imageFormatForImageData:data]; 39 data = [image sd_imageDataAsFormat:imageFormatFromData]; 40 } 41 42 [self storeImageDataToDisk:data forKey:key]; 43 if (completionBlock) { 44 dispatch_async(dispatch_get_main_queue(), ^{ 45 completionBlock(); 46 }); 47 } 48 }); 49 } else { 50 if (completionBlock) { 51 completionBlock(); 52 } 53 } 54 } 55 56 - (void)storeImageDataToDisk:(nullable NSData *)imageData forKey:(nullable NSString *)key { 57 if (!imageData || !key) { 58 return; 59 } 60 61 [self checkIfQueueIsIOQueue]; 62 63 if (![_fileManager fileExistsAtPath:_diskCachePath]) { 64 [_fileManager createDirectoryAtPath:_diskCachePath withIntermediateDirectories:YES attributes:nil error:NULL]; 65 } 66 67 // get cache Path for image key 68 NSString *cachePathForKey = [self defaultCachePathForKey:key]; 69 // transform to NSUrl 70 NSURL *fileURL = [NSURL fileURLWithPath:cachePathForKey]; 71 72 [_fileManager createFileAtPath:cachePathForKey contents:imageData attributes:nil]; 73 74 // disable iCloud backup 75 if (self.config.shouldDisableiCloud) { 76 [fileURL setResourceValue:@YES forKey:NSURLIsExcludedFromBackupKey error:nil]; 77 } 78 }
这部分的函数主要用来存储图片的。
- (void)storeImage:(nullable UIImage *)image forKey:(nullable NSString *)key completion:(nullable SDWebImageNoParamsBlock)completionBlock {}
存储图片同时到磁盘缓存和内存缓存。
- (void)storeImage:(nullable UIImage *)image forKey:(nullable NSString *)key toDisk:(BOOL)toDisk completion:(nullable SDWebImageNoParamsBlock)completionBlock {}
加一个布尔类型的 toDisk,表示选择是否存入磁盘缓存。
上面的两个方法的实现都是调用:
- (void)storeImage:(nullable UIImage *)image imageData:(nullable NSData *)imageData forKey:(nullable NSString *)key toDisk:(BOOL)toDisk completion:(nullable SDWebImageNoParamsBlock)completionBlock {}
来完成的。
1 - (void)storeImage:(nullable UIImage *)image 2 imageData:(nullable NSData *)imageData 3 forKey:(nullable NSString *)key 4 toDisk:(BOOL)toDisk 5 completion:(nullable SDWebImageNoParamsBlock)completionBlock { 6 if (!image || !key) { 7 if (completionBlock) { 8 completionBlock(); 9 } 10 return; 11 } 12 // if memory cache is enabled 13 if (self.config.shouldCacheImagesInMemory) { 14 NSUInteger cost = SDCacheCostForImage(image); 15 [self.memCache setObject:image forKey:key cost:cost]; 16 } 17 18 if (toDisk) { 19 dispatch_async(self.ioQueue, ^{ 20 NSData *data = imageData; 21 22 if (!data && image) { 23 SDImageFormat imageFormatFromData = [NSData sd_imageFormatForImageData:data]; 24 data = [image sd_imageDataAsFormat:imageFormatFromData]; 25 } 26 27 [self storeImageDataToDisk:data forKey:key]; 28 if (completionBlock) { 29 dispatch_async(dispatch_get_main_queue(), ^{ 30 completionBlock(); 31 }); 32 } 33 }); 34 } else { 35 if (completionBlock) { 36 completionBlock(); 37 } 38 } 39 }
如果 image 不存在或者 key 不存在的时候,执行 completionBlock 并 return。
根据缓存配置类 _config 的 _shouldCacheImagesInMemory 属性,判断是否把图像存入内存中,
先计算出图像的占用内存,使用 _memCache 缓存图像到内存中。这个过程是非常快的,因此不用考虑线程。
如果 toDisk 为真,在串行队列 _ioQueue 中异步执行:
首先判断 data 和 image 两个参数,data 是 image 转化为的 NSData 数据。
下面把 data 数据按处理过的文件名 key 保存在磁盘里面。(这里存入磁盘的数据是图像的 NSData 数据,不是 UIImage 图像,或者被压缩过的 UIImage 图像)
1 [self storeImageDataToDisk:data forKey:key];
这个方法的实现下面会详细讲述。
1 if (completionBlock) { 2 dispatch_async(dispatch_get_main_queue(), ^{ 3 completionBlock(); 4 }); 5 }
当图像 data 数据存入磁盘完毕,如果 completionBlock 实现了,则在 _ioQueue 队列里面调取主线执行 completionBlock 。
下面如果 toDisk 不为真,则把 image 存入缓存以后,如果 completionBlock 实现了,则直接执行 completionBlock。
1 - (void)storeImageDataToDisk:(nullable NSData *)imageData forKey:(nullable NSString *)key { 2 if (!imageData || !key) { 3 return; 4 } 5 6 [self checkIfQueueIsIOQueue]; 7 8 if (![_fileManager fileExistsAtPath:_diskCachePath]) { 9 [_fileManager createDirectoryAtPath:_diskCachePath withIntermediateDirectories:YES attributes:nil error:NULL]; 10 } 11 12 // get cache Path for image key 13 NSString *cachePathForKey = [self defaultCachePathForKey:key]; 14 // transform to NSUrl 15 NSURL *fileURL = [NSURL fileURLWithPath:cachePathForKey]; 16 17 [_fileManager createFileAtPath:cachePathForKey contents:imageData attributes:nil]; 18 19 // disable iCloud backup 20 if (self.config.shouldDisableiCloud) { 21 [fileURL setResourceValue:@YES forKey:NSURLIsExcludedFromBackupKey error:nil]; 22 } 23 }
这个方法主要是实现把图像转化为 NSData 类型数据根据文件名 key 存入磁盘中。
首先如果 imageData 或者 key 不存在则直接返回。
然后检查当前是不是在 _ioQueue 队列里执行,这个方法只是一个打印处理提示处理。
使用文件管理类 _fileManager 判断 _diskCachePath 路径下是否有文件或者文件夹。
如果不存在则创建多级目录。
然后获得文件的完整路径 cachePathForkey。
根据完整的路径字符串转化为 NSURL。
使用文件管理类写文件到指定的路径中。把imageData 写入 cachePathForKey 路径下。
禁用 iCloud 备份。把文件的 URL 地址的 NSURLIsExcludedFromBackupKey 置为 YES。
Query and Retrieve Ops
这部分的代码很多,不贴出来了,只是把各个方法拆分一一的解析。
1 - (void)diskImageExistsWithKey:(nullable NSString *)key completion:(nullable SDWebImageCheckCacheCompletionBlock)completionBlock { 2 dispatch_async(_ioQueue, ^{ 3 BOOL exists = [_fileManager fileExistsAtPath:[self defaultCachePathForKey:key]]; 4 5 // fallback because of https://github.com/rs/SDWebImage/pull/976 that added the extension to the disk file name 6 // checking the key with and without the extension 7 if (!exists) { 8 exists = [_fileManager fileExistsAtPath:[self defaultCachePathForKey:key].stringByDeletingPathExtension]; 9 } 10 11 if (completionBlock) { 12 dispatch_async(dispatch_get_main_queue(), ^{ 13 completionBlock(exists); 14 }); 15 } 16 }); 17 }
这个方法看名字就很好理解,根据 key 检测默认的磁盘缓存路径中是否存在指定的图像。由于检测磁盘较慢,检测的结果并不是在函数的返回值里面返回,当检测完毕会执行检测完成的 completionBlock 回调,这个 block 只有一个布尔类型的参数,表示是否检测到指定图片。
在 _ioQueue 队列里面异步执行。
先根据 key 获得默认的完整的存储路径,检测文件是否存在。
如果不存在,则删除路径的末尾的扩展名再检测一遍。
然后调取主线程把检测结果 YES 或 NO 作为 block 参数执行 completionBlock 。
1 - (nullable UIImage *)imageFromMemoryCacheForKey:(nullable NSString *)key { 2 return [self.memCache objectForKey:key]; 3 }
根据 key 返回内存中存在的指定的图像。
1 - (nullable UIImage *)imageFromDiskCacheForKey:(nullable NSString *)key { 2 UIImage *diskImage = [self diskImageForKey:key]; 3 if (diskImage && self.config.shouldCacheImagesInMemory) { 4 NSUInteger cost = SDCacheCostForImage(diskImage); 5 [self.memCache setObject:diskImage forKey:key cost:cost]; 6 } 7 8 return diskImage; 9 }
根据 key 搜索全部磁盘缓存路径中存在的指定的图像。如果图像存在并且缓存配置类的 shouldCacheImagesInMemory 属性为 YES (表示使用内存缓存),则把得到的图像存入内存缓存中。返回获得的指定的图像。
1 - (nullable UIImage *)imageFromCacheForKey:(nullable NSString *)key { 2 // First check the in-memory cache... 3 UIImage *image = [self imageFromMemoryCacheForKey:key]; 4 if (image) { 5 return image; 6 } 7 8 // Second check the disk cache... 9 image = [self imageFromDiskCacheForKey:key]; 10 return image; 11 }
根据 key 获得缓存中指定的图片,首先检测内存缓存中是否有图像,如果有直接返回,如果没有,再去磁盘中检测图像是否存在,然后返回。
1 - (nullable NSData *)diskImageDataBySearchingAllPathsForKey:(nullable NSString *)key { 2 NSString *defaultPath = [self defaultCachePathForKey:key]; 3 NSData *data = [NSData dataWithContentsOfFile:defaultPath]; 4 if (data) { 5 return data; 6 } 7 8 // fallback because of https://github.com/rs/SDWebImage/pull/976 that added the extension to the disk file name 9 // checking the key with and without the extension 10 data = [NSData dataWithContentsOfFile:defaultPath.stringByDeletingPathExtension]; 11 if (data) { 12 return data; 13 } 14 15 NSArray<NSString *> *customPaths = [self.customPaths copy]; 16 for (NSString *path in customPaths) { 17 NSString *filePath = [self cachePathForKey:key inPath:path]; 18 NSData *imageData = [NSData dataWithContentsOfFile:filePath]; 19 if (imageData) { 20 return imageData; 21 } 22 23 // fallback because of https://github.com/rs/SDWebImage/pull/976 that added the extension to the disk file name 24 // checking the key with and without the extension 25 imageData = [NSData dataWithContentsOfFile:filePath.stringByDeletingPathExtension]; 26 if (imageData) { 27 return imageData; 28 } 29 } 30 31 return nil; 32 }
根据 key 搜索全部的磁盘缓存路径,返回指定的图像的 NSData 数据。
首先从默认的缓存路径下读取 NSData 数据,如果有则直接返回 data。
如果不存在的时候删除默认路径的扩展名再读取一次,如果有则直接返回 data。
如果不存在的时候,便利存放自定义的缓存路径的数组 _customPaths,读取图像的 NSData 数据。每一次读取都是先从完整路径下面去读,读不到的时候再删除路径的扩展名去读数据。如果依然读不到则进入下次循环。
1 - (nullable UIImage *)diskImageForKey:(nullable NSString *)key { 2 NSData *data = [self diskImageDataBySearchingAllPathsForKey:key]; 3 if (data) { 4 UIImage *image = [UIImage sd_imageWithData:data]; 5 image = [self scaledImageForKey:key image:image]; 6 if (self.config.shouldDecompressImages) { 7 image = [UIImage decodedImageWithImage:image]; 8 } 9 return image; 10 } 11 else { 12 return nil; 13 } 14 }
首先搜索全部的缓存路径获得图像的 NSData 数据,如果数据存在则把 NSData 转化为 UIImage,根据 key 调整图像的大小(如果 key 里面有 @2x 或者 @3x 的时候,scale 是 2 或者 3),根据配置类的 _shouldDecompressImages 属性是否解压图像,最后返回图像。
1 - (nullable UIImage *)scaledImageForKey:(nullable NSString *)key image:(nullable UIImage *)image { 2 return SDScaledImageForKey(key, image); 3 }
1 - (nullable NSOperation *)queryCacheOperationForKey:(nullable NSString *)key done:(nullable SDCacheQueryCompletedBlock)doneBlock { 2 if (!key) { 3 if (doneBlock) { 4 doneBlock(nil, nil, SDImageCacheTypeNone); 5 } 6 return nil; 7 } 8 9 // First check the in-memory cache... 10 UIImage *image = [self imageFromMemoryCacheForKey:key]; 11 if (image) { 12 NSData *diskData = nil; 13 if ([image isGIF]) { 14 diskData = [self diskImageDataBySearchingAllPathsForKey:key]; 15 } 16 if (doneBlock) { 17 doneBlock(image, diskData, SDImageCacheTypeMemory); 18 } 19 return nil; 20 } 21 22 NSOperation *operation = [NSOperation new]; 23 dispatch_async(self.ioQueue, ^{ 24 if (operation.isCancelled) { 25 // do not call the completion if cancelled 26 return; 27 } 28 29 @autoreleasepool { 30 NSData *diskData = [self diskImageDataBySearchingAllPathsForKey:key]; 31 UIImage *diskImage = [self diskImageForKey:key]; 32 if (diskImage && self.config.shouldCacheImagesInMemory) { 33 NSUInteger cost = SDCacheCostForImage(diskImage); 34 [self.memCache setObject:diskImage forKey:key cost:cost]; 35 } 36 37 if (doneBlock) { 38 dispatch_async(dispatch_get_main_queue(), ^{ 39 doneBlock(diskImage, diskData, SDImageCacheTypeDisk); 40 }); 41 } 42 } 43 }); 44 45 return operation; 46 }
这个根据 key 检测缓存的操作,返回值是一个 NSOperation 对象,方便后面的取消检测操作,主要磁盘缓存比较耗时,检测内存缓存比较快。当检测到图像的时候会执行一个 SDCacheQueryCompletedBlock 的 doneBlock。下面是这个 block 在 SDImageCache.h 里面的定义。
1 typedef NS_ENUM(NSInteger, SDImageCacheType) { 2 /** 3 * The image wasn't available the SDWebImage caches, but was downloaded from the web. 4 */ 5 SDImageCacheTypeNone, 6 /** 7 * The image was obtained from the disk cache. 8 */ 9 SDImageCacheTypeDisk, 10 /** 11 * The image was obtained from the memory cache. 12 */ 13 SDImageCacheTypeMemory 14 }; 15 16 typedef void(^SDCacheQueryCompletedBlock)(UIImage * _Nullable image, NSData * _Nullable data, SDImageCacheType cacheType);
如果 key 不存在,则直接执行 doneBlock,参数 image 是 nil data 是 nil 缓存类型 SDImageCacheType 是 SDImageCacheTypeNone。返回 nil。
如果 key 存在,首先检测内存缓存,如果有 image 图像,如果 image 是 GIF 图像,从磁盘读取 image 图像的 data 数据,执行 doneBlock ,返回 nil。
如果缓存路径中没有找到图像,则接下来到磁盘中去找图像。
在 _ioQueue 中异步执行,创建一个 NSOperation 类型的 operation,如果 operation 取消了则直接 return。
如果没有取消,在一个自动释放池里面,从全部的磁盘缓存路径中遍历,获得图像的 data 数据,和 UIImage 对象,如果 UIImage 存在且缓存配置类 _shouldCacheImagesInMemory 为真,即保存到内存缓存中,则把 UIImage 对象再保存到内存缓存中。然后执行 doneBlock。返回 operation。
Remove Ops
1 - (void)removeImageForKey:(nullable NSString *)key withCompletion:(nullable SDWebImageNoParamsBlock)completion { 2 [self removeImageForKey:key fromDisk:YES withCompletion:completion]; 3 } 4 5 - (void)removeImageForKey:(nullable NSString *)key fromDisk:(BOOL)fromDisk withCompletion:(nullable SDWebImageNoParamsBlock)completion { 6 if (key == nil) { 7 return; 8 } 9 10 if (self.config.shouldCacheImagesInMemory) { 11 [self.memCache removeObjectForKey:key]; 12 } 13 14 if (fromDisk) { 15 dispatch_async(self.ioQueue, ^{ 16 [_fileManager removeItemAtPath:[self defaultCachePathForKey:key] error:nil]; 17 18 if (completion) { 19 dispatch_async(dispatch_get_main_queue(), ^{ 20 completion(); 21 }); 22 } 23 }); 24 } else if (completion){ 25 completion(); 26 } 27 28 }
根据 key 删除指定的 image,如果 key 等于 nil,则直接返回。
首先判断缓存配置类的 _shouldCacheImagesInMemory 是否为YES,如果是 YES 则使用 _memCache 根据 key 删除缓存中的 image。
根据 fromDisk 判断是否删除磁盘缓存中的指定的 image。
如果 fromDisk 为真,在 _ioQueue 队列中异步删除默认路径下的指定的图像。删除完成调取主线执行完成的 block (一个没有参数没有返回值的 block)。
1 typedef void(^SDWebImageNoParamsBlock)();
Mem Cache settings
1 # pragma mark - Mem Cache settings 2 3 - (void)setMaxMemoryCost:(NSUInteger)maxMemoryCost { 4 self.memCache.totalCostLimit = maxMemoryCost; 5 } 6 7 - (NSUInteger)maxMemoryCost { 8 return self.memCache.totalCostLimit; 9 } 10 11 - (NSUInteger)maxMemoryCountLimit { 12 return self.memCache.countLimit; 13 } 14 15 - (void)setMaxMemoryCountLimit:(NSUInteger)maxCountLimit { 16 self.memCache.countLimit = maxCountLimit; 17 }
重写 _maxMemoryCost 的 set 和 get 方法。
_maxMemoryCost 的 set 方法设置 _memCache.totalCostLimit 内存缓存的最大容量。
_maxMemoryCost 的 get 方法直接返回 _memCache.totalCostLimit。
重写 _maxMemoryCountLimit 的 set 和 get 方法。
_maxMemoryCountLimit 的 set 方法设置 _memCache.countLimit 内存缓存的最大文件数量。
_maxMemoryCountLimit 的 get 方法直接返回 _maxCache.countLimit。
Cache clean Ops
清除数据。
1 - (void)clearMemory { 2 [self.memCache removeAllObjects]; 3 }
清空内存缓存的所有数据。
1 - (void)clearDiskOnCompletion:(nullable SDWebImageNoParamsBlock)completion { 2 dispatch_async(self.ioQueue, ^{ 3 [_fileManager removeItemAtPath:self.diskCachePath error:nil]; 4 [_fileManager createDirectoryAtPath:self.diskCachePath 5 withIntermediateDirectories:YES 6 attributes:nil 7 error:NULL]; 8 9 if (completion) { 10 dispatch_async(dispatch_get_main_queue(), ^{ 11 completion(); 12 }); 13 } 14 }); 15 }
在 _ioQueue 队列异步删除 _diskCachePath 的所有数据,然后再创建 _diskCachePath 路径的文件夹。删除完毕调取主线执行 completion block。
1 - (void)deleteOldFiles { 2 [self deleteOldFilesWithCompletionBlock:nil]; 3 } 4 5 - (void)deleteOldFilesWithCompletionBlock:(nullable SDWebImageNoParamsBlock)completionBlock { 6 dispatch_async(self.ioQueue, ^{ 7 NSURL *diskCacheURL = [NSURL fileURLWithPath:self.diskCachePath isDirectory:YES]; 8 NSArray<NSString *> *resourceKeys = @[NSURLIsDirectoryKey, NSURLContentModificationDateKey, NSURLTotalFileAllocatedSizeKey]; 9 10 // This enumerator prefetches useful properties for our cache files. 11 NSDirectoryEnumerator *fileEnumerator = [_fileManager enumeratorAtURL:diskCacheURL 12 includingPropertiesForKeys:resourceKeys 13 options:NSDirectoryEnumerationSkipsHiddenFiles 14 errorHandler:NULL]; 15 16 NSDate *expirationDate = [NSDate dateWithTimeIntervalSinceNow:-self.config.maxCacheAge]; 17 NSMutableDictionary<NSURL *, NSDictionary<NSString *, id> *> *cacheFiles = [NSMutableDictionary dictionary]; 18 NSUInteger currentCacheSize = 0; 19 20 // Enumerate all of the files in the cache directory. This loop has two purposes: 21 // 22 // 1. Removing files that are older than the expiration date. 23 // 2. Storing file attributes for the size-based cleanup pass. 24 NSMutableArray<NSURL *> *urlsToDelete = [[NSMutableArray alloc] init]; 25 for (NSURL *fileURL in fileEnumerator) { 26 NSError *error; 27 NSDictionary<NSString *, id> *resourceValues = [fileURL resourceValuesForKeys:resourceKeys error:&error]; 28 29 // Skip directories and errors. 30 if (error || !resourceValues || [resourceValues[NSURLIsDirectoryKey] boolValue]) { 31 continue; 32 } 33 34 // Remove files that are older than the expiration date; 35 NSDate *modificationDate = resourceValues[NSURLContentModificationDateKey]; 36 if ([[modificationDate laterDate:expirationDate] isEqualToDate:expirationDate]) { 37 [urlsToDelete addObject:fileURL]; 38 continue; 39 } 40 41 // Store a reference to this file and account for its total size. 42 NSNumber *totalAllocatedSize = resourceValues[NSURLTotalFileAllocatedSizeKey]; 43 currentCacheSize += totalAllocatedSize.unsignedIntegerValue; 44 cacheFiles[fileURL] = resourceValues; 45 } 46 47 for (NSURL *fileURL in urlsToDelete) { 48 [_fileManager removeItemAtURL:fileURL error:nil]; 49 } 50 51 // If our remaining disk cache exceeds a configured maximum size, perform a second 52 // size-based cleanup pass. We delete the oldest files first. 53 if (self.config.maxCacheSize > 0 && currentCacheSize > self.config.maxCacheSize) { 54 // Target half of our maximum cache size for this cleanup pass. 55 const NSUInteger desiredCacheSize = self.config.maxCacheSize / 2; 56 57 // Sort the remaining cache files by their last modification time (oldest first). 58 NSArray<NSURL *> *sortedFiles = [cacheFiles keysSortedByValueWithOptions:NSSortConcurrent 59 usingComparator:^NSComparisonResult(id obj1, id obj2) { 60 return [obj1[NSURLContentModificationDateKey] compare:obj2[NSURLContentModificationDateKey]]; 61 }]; 62 63 // Delete files until we fall below our desired cache size. 64 for (NSURL *fileURL in sortedFiles) { 65 if ([_fileManager removeItemAtURL:fileURL error:nil]) { 66 NSDictionary<NSString *, id> *resourceValues = cacheFiles[fileURL]; 67 NSNumber *totalAllocatedSize = resourceValues[NSURLTotalFileAllocatedSizeKey]; 68 currentCacheSize -= totalAllocatedSize.unsignedIntegerValue; 69 70 if (currentCacheSize < desiredCacheSize) { 71 break; 72 } 73 } 74 } 75 } 76 if (completionBlock) { 77 dispatch_async(dispatch_get_main_queue(), ^{ 78 completionBlock(); 79 }); 80 } 81 }); 82 }
清空旧的数据。
在 _ioQueue 队列里面异步执行。
_diskCachePath 做参数返回它的 NSURL diskCacheURL,定义一个存放文件系统资源键的数组 resourceKeys 里面放了3个资源键 NSURLResourceKey:
1 NSArray<NSString *> *resourceKeys = @[NSURLIsDirectoryKey, NSURLContentModificationDateKey, NSURLTotalFileAllocatedSizeKey];
NSURLIsDirectoryKey 是否是文件夹(只读,值为布尔类型)
NSURLContentModificationDateKey 资源的最后修改时间(可读可写,值为 NSDate 类型)
NSURLTotalFileAllocatedSizeKey 文件分配的内存大小(只读,值为 NSNumber 类型)
1 /* enumeratorAtURL:includingPropertiesForKeys:options:errorHandler: returns an NSDirectoryEnumerator rooted at the provided directory URL. The NSDirectoryEnumerator returns NSURLs from the -nextObject method. The optional 'includingPropertiesForKeys' parameter indicates which resource properties should be pre-fetched and cached with each enumerated URL. The optional 'errorHandler' block argument is invoked when an error occurs. Parameters to the block are the URL on which an error occurred and the error. When the error handler returns YES, enumeration continues if possible. Enumeration stops immediately when the error handler returns NO. 2 3 If you wish to only receive the URLs and no other attributes, then pass '0' for 'options' and an empty NSArray ('[NSArray array]') for 'keys'. If you wish to have the property caches of the vended URLs pre-populated with a default set of attributes, then pass '0' for 'options' and 'nil' for 'keys'. 4 */ 5 - (nullable NSDirectoryEnumerator<NSURL *> *)enumeratorAtURL:(NSURL *)url includingPropertiesForKeys:(nullable NSArray<NSURLResourceKey> *)keys options:(NSDirectoryEnumerationOptions)mask errorHandler:(nullable BOOL (^)(NSURL *url, NSError *error))handler NS_AVAILABLE(10_6, 4_0);
这个方法把 diskCacheURL\reslurceKeys 数组和一个 NSDirectoryEnumerationOptions 当参数传入,返回一个 NSDirectoryEnumerator<NSURL *> * 对象,这个对象里面存放的都是 NSURL。mask(过滤参数) 传入的是 NSDirectoryEnumerationSkipsHiddenFiles 是指忽略隐藏文件。
1 NSDate *expirationDate = [NSDate dateWithTimeIntervalSinceNow:-self.config.maxCacheAge];
过期日期,默认的缓存的最大日期 _config.maxCacheAge 是一周,计算距离当前一周前的时间点 expirationDate。
1 NSMutableDictionary<NSURL *, NSDictionary<NSString *, id> *> *cacheFiles = [NSMutableDictionary dictionary]; 2 NSUInteger currentCacheSize = 0;
定义一个可变字典 cacheFiles 和 一个NSUInteger 类型的 currentCacheSize。
cacheFiles 字典的 每一个 key 都是一个 NSURL ,每一个 key 对应的 value 是一个字典。这每一个 NSURL 对应的是 _diskCachePath 路径下每一个文件(包括文件夹或者图片文件)的路径的 URL,这个每一个 key 对应的字典是这每个 URL 下文件的不同的属性和属性值所组成的字典。
currentCacheSize 纪录的是 _diskCachePath 路径下,所有不过期的图片文件的总的占用的内存大小。
下面开始枚举缓存目录中的所有文件,这个循环有两个目的:
1.移除大于过期日期的文件。
2.存储基于大小的清除通道的文件属性。
1 NSMutableArray<NSURL *> *urlsToDelete = [[NSMutableArray alloc] init];
定义一个里面存放的都是 NSURL 的可变数组,用来存放下面 forIn 循环里面判断得出的需要删除的 NSURL。
下面开始使用 ForIn 遍历 fileEnumerator。
1 NSError *error; 2 NSDictionary<NSString *, id> *resourceValues = [fileURL resourceValuesForKeys:resourceKeys error:&error];
对每一个 fileURL 取 resourceKeys 数组里面每一个 NSURLResourceKey 作为 key 时对应的 value 值,并把值放在 resourceValues 字典里面。
1 // Skip directories and errors. 2 if (error || !resourceValues || [resourceValues[NSURLIsDirectoryKey] boolValue]) { 3 continue; 4 }
当取 resourceValues 时有 error 和 当前的 fileURL 是文件夹的时候,跳出本次循环,进入下次循环。
1 // Remove files that are older than the expiration date; 2 NSDate *modificationDate = resourceValues[NSURLContentModificationDateKey]; 3 if ([[modificationDate laterDate:expirationDate] isEqualToDate:expirationDate]) { 4 [urlsToDelete addObject:fileURL]; 5 continue; 6 }
从 resourceValues 里面取出 NSURLContentModificationDateKey 的值,即当前文件的最后修改的时间的 NSDate 的值并把该值赋给 modificationDate 。
比较 modificationDate 和 expirationDate 两个时间点谁是较晚的时间点,如果是 expirationDate,即表示当前循环的这个 URL 下的文件的最后修改时间是在当前时间点一周之前的时间点的,那当前循环的这个 URL 下面的文件是需要删除的,把当前的这个 URL 加入到 urlsToDelete 数组里面,跳出本次循环,进入下次循环。
1 // Store a reference to this file and account for its total size. 2 NSNumber *totalAllocatedSize = resourceValues[NSURLTotalFileAllocatedSizeKey]; 3 currentCacheSize += totalAllocatedSize.unsignedIntegerValue; 4 cacheFiles[fileURL] = resourceValues;
如果当前循环的 URL 下是文件,且没有超出缓存限制时间,则首先通过 resourceValues 字典的 NSURLTotalFileAllocatedSizeKey 的 value 值获得当前文件所占内存的大小 totalAllocatedSize。 把 totalAllocatedSize 的值转化为无符号 Integer 的值和循环开始时定义的纪录当前没有过期的图片总的缓存容量的 NSUInterger 类型的 currentCacheSize 变量作加法运算并把结果赋给 currentCacheSize。
并把当前循环下的不过期的图片文件的路径的 URL 作 key,对应的每个 URL 下文件的不同的属性和属性值所组成的字典作 value,添加到 cacheFiles 这个可变字典里面。
1 for (NSURL *fileURL in urlsToDelete) { 2 [_fileManager removeItemAtURL:fileURL error:nil]; 3 }
当上一个循环结束,对上个循环的里面纪录需要删除的图片文件的 URL 的 urlsToDelete 数组进行遍历,删除每一个 URL 下的文件。
1 // If our remaining disk cache exceeds a configured maximum size, perform a second 2 // size-based cleanup pass. We delete the oldest files first. 3 if (self.config.maxCacheSize > 0 && currentCacheSize > self.config.maxCacheSize) { 4 // Target half of our maximum cache size for this cleanup pass. 5 const NSUInteger desiredCacheSize = self.config.maxCacheSize / 2; 6 7 // Sort the remaining cache files by their last modification time (oldest first). 8 NSArray<NSURL *> *sortedFiles = [cacheFiles keysSortedByValueWithOptions:NSSortConcurrent 9 usingComparator:^NSComparisonResult(id obj1, id obj2) { 10 return [obj1[NSURLContentModificationDateKey] compare:obj2[NSURLContentModificationDateKey]]; 11 }]; 12 13 // Delete files until we fall below our desired cache size. 14 for (NSURL *fileURL in sortedFiles) { 15 if ([_fileManager removeItemAtURL:fileURL error:nil]) { 16 NSDictionary<NSString *, id> *resourceValues = cacheFiles[fileURL]; 17 NSNumber *totalAllocatedSize = resourceValues[NSURLTotalFileAllocatedSizeKey]; 18 currentCacheSize -= totalAllocatedSize.unsignedIntegerValue; 19 20 if (currentCacheSize < desiredCacheSize) { 21 break; 22 } 23 } 24 } 25 }
如果删除完过期文件后,剩下的文件缓存还是超出了缓存类配置的缓存空间的大小,则接下来执行第二个清除方案,基于大小的清除方案。首先删除时间最老的文件。
如果缓存配置类配置的最大缓存空间 _maxCacheSize 的大小大于 0 并且当前的所有的不过期的缓存文件的大小总和大于缓存配置类配置的最大的缓存空间。
1 // Target half of our maximum cache size for this cleanup pass. 2 const NSUInteger desiredCacheSize = self.config.maxCacheSize / 2;
定义一个 NSUInteger 类型的变量 desiredCacheSize,值为当前缓存配置类的 _maxCacheSize 的一半。
1 // Sort the remaining cache files by their last modification time (oldest first). 2 NSArray<NSURL *> *sortedFiles = [cacheFiles keysSortedByValueWithOptions:NSSortConcurrent 3 usingComparator:^NSComparisonResult(id obj1, id obj2) { 4 return [obj1[NSURLContentModificationDateKey] compare:obj2[NSURLContentModificationDateKey]]; 5 }];
把当前的这些不过期的缓存文件,按上次的修改时间排序。最早的文件放在第一位。
1 - (NSArray<KeyType> *)keysSortedByValueWithOptions:(NSSortOptions)opts usingComparator:(NSComparator NS_NOESCAPE)cmptr NS_AVAILABLE(10_6, 4_0);
这个函数方法是一个对字典进行排序,这个方法会返回拍好序的字典的所有 key。NSSortOptions 是排序设置:
1 typedef NS_OPTIONS(NSUInteger, NSSortOptions) { 2 NSSortConcurrent = (1UL << 0), 3 NSSortStable = (1UL << 4), 4 };
NSSortConcurrent 是并发排序,效率高,但可能不稳定,NSSortStable 是稳定排序,但可能效率不如 NSSortConcurrent 高。排序的规则通过 cmptr block 的实现确定:
1 typedef NS_ENUM(NSInteger, NSComparisonResult) {NSOrderedAscending = -1L, NSOrderedSame, NSOrderedDescending};
该 block 返回值是一个 NSComparisonResult,取字典里的每一个 value 值的字典的 NSURLContentModificationDateKey 作比较。
1 // Delete files until we fall below our desired cache size. 2 for (NSURL *fileURL in sortedFiles) { 3 if ([_fileManager removeItemAtURL:fileURL error:nil]) { 4 NSDictionary<NSString *, id> *resourceValues = cacheFiles[fileURL]; 5 NSNumber *totalAllocatedSize = resourceValues[NSURLTotalFileAllocatedSizeKey]; 6 currentCacheSize -= totalAllocatedSize.unsignedIntegerValue; 7 8 if (currentCacheSize < desiredCacheSize) { 9 break; 10 } 11 } 12 }
拍完序后开始删除文件,每删除一个文件就把 currentCacheSize 减去删除文件的占用内存的大小。直到 currentCacheSize 的大小小于 desiredCacheSize 跳出循环。
如果 completionBlock 实现了,则调取主线程执行 completionBlock。
backgroundDeleteOldFiles
1 #if SD_UIKIT 2 - (void)backgroundDeleteOldFiles { 3 Class UIApplicationClass = NSClassFromString(@"UIApplication"); 4 if(!UIApplicationClass || ![UIApplicationClass respondsToSelector:@selector(sharedApplication)]) { 5 return; 6 } 7 UIApplication *application = [UIApplication performSelector:@selector(sharedApplication)]; 8 __block UIBackgroundTaskIdentifier bgTask = [application beginBackgroundTaskWithExpirationHandler:^{ 9 // Clean up any unfinished task business by marking where you 10 // stopped or ending the task outright. 11 [application endBackgroundTask:bgTask]; 12 bgTask = UIBackgroundTaskInvalid; 13 }]; 14 15 // Start the long-running task and return immediately. 16 [self deleteOldFilesWithCompletionBlock:^{ 17 [application endBackgroundTask:bgTask]; 18 bgTask = UIBackgroundTaskInvalid; 19 }]; 20 } 21 #endif
申请一段时间在后台删除旧数据
Cache Info
1 #pragma mark - Cache Info 2 3 - (NSUInteger)getSize { 4 __block NSUInteger size = 0; 5 dispatch_sync(self.ioQueue, ^{ 6 NSDirectoryEnumerator *fileEnumerator = [_fileManager enumeratorAtPath:self.diskCachePath]; 7 for (NSString *fileName in fileEnumerator) { 8 NSString *filePath = [self.diskCachePath stringByAppendingPathComponent:fileName]; 9 NSDictionary<NSString *, id> *attrs = [[NSFileManager defaultManager] attributesOfItemAtPath:filePath error:nil]; 10 size += [attrs fileSize]; 11 } 12 }); 13 return size; 14 }
在 _ioQueue 队列里面异步计算 _diskCachePath 路径下所有的缓存文件的大小。
1 /* enumeratorAtPath: returns an NSDirectoryEnumerator rooted at the provided path. If the enumerator cannot be created, this returns NULL. Because NSDirectoryEnumerator is a subclass of NSEnumerator, the returned object can be used in the for...in construct. 2 */ 3 - (nullable NSDirectoryEnumerator<NSString *> *)enumeratorAtPath:(NSString *)path;
这个方法返回的 fileEnumerator 里面存放的是 _diskCachePath 路径下所有的文件的名字。
遍历 fileEnumerator,使用 NSFileManager 计算出所有文件的大小,返回 size。
1 - (NSUInteger)getDiskCount { 2 __block NSUInteger count = 0; 3 dispatch_sync(self.ioQueue, ^{ 4 NSDirectoryEnumerator *fileEnumerator = [_fileManager enumeratorAtPath:self.diskCachePath]; 5 count = fileEnumerator.allObjects.count; 6 }); 7 return count; 8 }
返回所有文件的数量。
1 - (void)calculateSizeWithCompletionBlock:(nullable SDWebImageCalculateSizeBlock)completionBlock { 2 NSURL *diskCacheURL = [NSURL fileURLWithPath:self.diskCachePath isDirectory:YES]; 3 4 dispatch_async(self.ioQueue, ^{ 5 NSUInteger fileCount = 0; 6 NSUInteger totalSize = 0; 7 8 NSDirectoryEnumerator *fileEnumerator = [_fileManager enumeratorAtURL:diskCacheURL 9 includingPropertiesForKeys:@[NSFileSize] 10 options:NSDirectoryEnumerationSkipsHiddenFiles 11 errorHandler:NULL]; 12 13 for (NSURL *fileURL in fileEnumerator) { 14 NSNumber *fileSize; 15 [fileURL getResourceValue:&fileSize forKey:NSURLFileSizeKey error:NULL]; 16 totalSize += fileSize.unsignedIntegerValue; 17 fileCount += 1; 18 } 19 20 if (completionBlock) { 21 dispatch_async(dispatch_get_main_queue(), ^{ 22 completionBlock(fileCount, totalSize); 23 }); 24 } 25 }); 26 }
计算文件大小和文件数量,结果在 block 里面返回。
1 NSDirectoryEnumerator *fileEnumerator = [_fileManager enumeratorAtURL:diskCacheURL 2 includingPropertiesForKeys:@[NSFileSize] 3 options:NSDirectoryEnumerationSkipsHiddenFiles 4 errorHandler:NULL];
在 _ioQueue 队列里面异步执行操作。
这里的参数 (nullable NSArray<NSURLResourceKey> *)keys 里面只有一个 FOUNDATION_EXPORT NSFileAttributeKey const NSFileSize; 用于获得文件大小。
如果 completionBlock 实现了,调取主线程在 执行该 block,block 两个参数一个纪录文件总个数,一个纪录文件的占用内存总大小。
1 typedef void(^SDWebImageCalculateSizeBlock)(NSUInteger fileCount, NSUInteger totalSize);
END
参考链接:http://www.jianshu.com/p/a33d5abf686b
http://www.jianshu.com/p/5e69e211b161
http://www.jianshu.com/p/8ad9ff204f73
http://blog.csdn.net/songchunmin_/article/details/51103677