iOS缓存 NSCache详解及SDWebImage缓存策略源码分析你要知道的NSCache都在这里

时间:2022-05-07
本文章向大家介绍iOS缓存 NSCache详解及SDWebImage缓存策略源码分析你要知道的NSCache都在这里,主要内容包括你要知道的NSCache都在这里、SDWebImage的缓存策略、补充、备注、基本概念、基础应用、原理机制和需要注意的事项等,并结合实例形式分析了其使用技巧,希望通过本文能帮助到大家理解应用这部分内容。

你要知道的NSCache都在这里

转载请注明出处 https://cloud.tencent.com/developer/user/1605429

本篇文章首先会详细讲解NSCache的基本使用,NSCacheFoundation框架提供的缓存类的实现,使用方式类似于可变字典,由于NSMutableDictionary的存在,很多人在实现缓存时都会使用可变字典,但NSCache在实现缓存功能时比可变字典更方便,最重要的是它是线程安全的,而NSMutableDictionary不是线程安全的,在多线程环境下使用NSCache是更好的选择。接着,会通过源码讲解SDWebImage的缓存策略。最后简要补充了第三方YYCache的实现思路。

NSCache

NSCache的使用很方便,提供了类似可变字典的使用方式,但它比可变字典更适用于实现缓存,最重要的原因为NSCache是线程安全的,使用NSMutableDictionary自定义实现缓存时需要考虑加锁和释放锁,NSCache已经帮我们做好了这一步。其次,在内存不足时NSCache会自动释放存储的对象,不需要手动干预,如果是自定义实现需要监听内存状态然后做进一步的删除对象的操作。还有一点就是NSCache的键key不会被复制,所以key不需要实现NSCopying协议。

上面讲解的三点就是NSCache相比于NSMutableDictionary实现缓存功能的优点,在需要实现缓存时应当优先考虑使用NSCache

首先看一下NSCache提供的属性和相关方法:

//名称
@property (copy) NSString *name;

//NSCacheDelegate代理
@property (nullable, assign) id<NSCacheDelegate> delegate;

//通过key获取value,类似于字典中通过key取value的操作
- (nullable ObjectType)objectForKey:(KeyType)key;

//设置key、value
- (void)setObject:(ObjectType)obj forKey:(KeyType)key; // 0 cost

/*
设置key、value
cost表示obj这个value对象的占用的消耗?可以自行设置每个需要添加进缓存的对象的cost值
这个值与后面的totalCostLimit对应,如果添加进缓存的cost总值大于totalCostLimit就会自动进行删除
感觉在实际开发中直接使用setObject:forKey:方法就可以解决问题了
*/
- (void)setObject:(ObjectType)obj forKey:(KeyType)key cost:(NSUInteger)g;

//根据key删除value对象
- (void)removeObjectForKey:(KeyType)key;

//删除保存的所有的key-value
- (void)removeAllObjects;

/*
NSCache能够占用的消耗?的限制
当NSCache缓存的对象的总cost值大于这个值则会自动释放一部分对象直到占用小于该值
非严格限制意味着如果保存的对象超出这个大小也不一定会被删除
这个值就是与前面setObject:forKey:cost:方法对应
*/
@property NSUInteger totalCostLimit;    // limits are imprecise/not strict

/*
缓存能够保存的key-value个数的最大数量
当保存的数量大于该值就会被自动释放
非严格限制意味着如果超出了这个数量也不一定会被删除
*/
@property NSUInteger countLimit;    // limits are imprecise/not strict
/*
这个值与NSDiscardableContent协议有关,默认为YES
当一个类实现了该协议,并且这个类的对象不再被使用时意味着可以被释放
*/
@property BOOL evictsObjectsWithDiscardedContent;

@end

//NSCacheDelegate协议
@protocol NSCacheDelegate <NSObject>
@optional
//上述协议只有这一个方法,缓存中的一个对象即将被删除时被回调
- (void)cache:(NSCache *)cache willEvictObject:(id)obj;
@end

通过接口可以看出,NSCache提供的方法都很简单,属性的意义也很明确,接下来举一个简单的栗子:

//定义一个CacheTest类实现NSCacheDelegate代理
@interface CacheTest: NSObject <NSCacheDelegate>

@end

@implementation CacheTest

//当缓存中的一个对象即将被删除时会回调该方法
- (void)cache:(NSCache *)cache willEvictObject:(id)obj
{
    NSLog(@"Remove Object %@", obj);
}

@end

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        //创建一个NSCache缓存对象
        NSCache *cache = [[NSCache alloc] init];
        //设置缓存中的对象个数最大为5个
        [cache setCountLimit:5];
        //创建一个CacheTest类作为NSCache对象的代理
        CacheTest *ct = [[CacheTest alloc] init];
        //设置代理
        cache.delegate = ct;
        
        //创建一个字符串类型的对象添加进缓存中,其中key为Test
        NSString *test = @"Hello, World";
        [cache setObject:test forKey:@"Test"];
        
        //遍历十次用于添加
        for (int i = 0; i < 10; i++)
        {
            [cache setObject:[NSString stringWithFormat:@"Hello%d", i] forKey:[NSString stringWithFormat:@"World%d", i]];
            NSLog(@"Add key:%@  value:%@ to Cache", [NSString stringWithFormat:@"Hello%d", i], [NSString stringWithFormat:@"World%d", i]);
        }
        
        for (int i = 0; i < 10; i++)
        {
            NSLog(@"Get value:%@ for key:%@", [cache objectForKey:[NSString stringWithFormat:@"World%d", i]], [NSString stringWithFormat:@"World%d", i]);
        }
        
        [cache removeAllObjects];
        
        for (int i = 0; i < 10; i++)
        {
            NSLog(@"Get value:%@ for key:%@", [cache objectForKey:[NSString stringWithFormat:@"World%d", i]], [NSString stringWithFormat:@"World%d", i]);
        }
        
        NSLog(@"Test %@", test);
    }
    
    return 0;
}

输出结果如下:

//第一个for循环输出
Add key:Hello0  value:World0 to Cache
Add key:Hello1  value:World1 to Cache
Add key:Hello2  value:World2 to Cache
Add key:Hello3  value:World3 to Cache
Remove Object Hello, World
Add key:Hello4  value:World4 to Cache
Remove Object Hello0
Add key:Hello5  value:World5 to Cache
Remove Object Hello1
Add key:Hello6  value:World6 to Cache
Remove Object Hello2
Add key:Hello7  value:World7 to Cache
Remove Object Hello3
Add key:Hello8  value:World8 to Cache
Remove Object Hello4
Add key:Hello9  value:World9 to Cache
//第二个for循环输出
Get value:(null) for key:World0
Get value:(null) for key:World1
Get value:(null) for key:World2
Get value:(null) for key:World3
Get value:(null) for key:World4
Get value:Hello5 for key:World5
Get value:Hello6 for key:World6
Get value:Hello7 for key:World7
Get value:Hello8 for key:World8
Get value:Hello9 for key:World9
//removeAllObjects输出
Remove Object Hello5
Remove Object Hello6
Remove Object Hello7
Remove Object Hello8
Remove Object Hello9
//最后一个for循环输出
Get value:(null) for key:World0
Get value:(null) for key:World1
Get value:(null) for key:World2
Get value:(null) for key:World3
Get value:(null) for key:World4
Get value:(null) for key:World5
Get value:(null) for key:World6
Get value:(null) for key:World7
Get value:(null) for key:World8
Get value:(null) for key:World9
//输出test字符串
Test Hello, World

上面的代码创建了一个NSCache对象,设置了其最大可缓存对象的个数为5个,从输出可以看出,当我们要添加第六个对象时NSCache自动删除了我们添加的第一个对象并触发了NSCacheDelegate的回调方法,添加第七个时也是同样的,删除了缓存中的一个对象才能添加进去。

在第二个for循环中,我们通过key取出所有的缓存对象,前五个对象取出都为nil,因为在添加后面的对象时前面的被删除了,所以,当我们从缓存中获取对象时一定要判断是否为空,我们无法保证缓存中的某个对象不会被删除。

接着调用了NSCacheremoveAllObjects方法,一旦调用该方法,NSCache就会将其中保存的所有对象都释放掉,所以,可以看到调用该方法后NSCacheDelegate的回调方法执行了五次,将NSCache中的所有缓存对象都清空了。

在最后一个for循环中,根据key获取缓存中的对象时可以发现都为空了,因为都被释放了。

前面还创建了一个字符串的局部变量,在最开始将其加入到了缓存中,后来随着其他对象的添加,该字符串被缓存释放了,但由于局部变量对其持有强引用所以使用test还是可以访问到的,这是最基本的ARC知识,所以,NSCache在释放一个对象时只是不再指向这个对象,即,该对象的引用计数减一,如果有其他指针指向它,这个对象不会被释放。

上面就是NSCache的基本用法了,我们只需要设置对象和获取对象,其他事情NSCache都帮我们做完了,因此,实现缓存功能时,使用NSCache就是我们的不二之选。

再看一个栗子:

- (void)viewWillAppear:(BOOL)animated
{
    self.cache = [[NSCache alloc] init];
    [self.cache setCountLimit:5];
    self.cache.delegate = self;
    [self.cache setObject:@"AA" forKey:@"BBB"];
    [self.cache setObject:@"MMMM" forKey:@"CCC"];
}

- (void)cache:(NSCache *)cache willEvictObject:(id)obj
{
    NSLog(@"REMOVE %@", obj);
}

这是一个有视图控制器的栗子,我们创建了一个NSCache对象,并在其中添加了对象,当点击home键,程序进入后台后,可以发现NSCacheDelegate的回调函数触发了,所以,当程序进入后台,NSCache对象会自动释放所有的对象。如果在模拟器上模拟内存警告,也可以发现NSCache会释放所有的对象。所以NSCache删除缓存中的对象会在以下情形中发生:

  • NSCache缓存对象自身被释放
  • 手动调用removeObjectForKey:方法
  • 手动调用removeAllObjects
  • 缓存中对象的个数大于countLimit,或,缓存中对象的总cost值大于totalCostLimit
  • 程序进入后台后
  • 收到系统的内存警告

SDWebImage的缓存策略

在了解了NSCache的基本使用后,现在来通过SDWebImage的源码看看它是怎样进行图片的缓存操作的。由于篇幅的问题,本文将源码中的英文注释删掉了,有需要的读者可以对照着注释源码查阅本文章。本节内容包括了GCDNSOperation等多线程相关的知识,有疑问的读者可以查阅本博客iOS多线程——你要知道的GCD都在这里 以及 iOS多线程——你要知道的NSOperation都在这里 相关内容,本文就不再赘述了。

首先看一下官方给的设置图片后执行时序图:

SDWebImage执行时序图

整个执行流程非常清晰明了,本篇文章的重点在第四步、第五步和第八步,关于网络下载,以后会在讲解NSURLSession后进行相关的源码分析。查看SDWebImage的源码,与缓存有关的一共有四个文件SDImageCacheConfigSDImageCache,首先看一下SDImageCacheConfig的头文件:

@interface SDImageCacheConfig : NSObject

//是否压缩图片,默认为YES,压缩图片可以提高性能,但是会消耗内存
@property (assign, nonatomic) BOOL shouldDecompressImages;

//是否关闭iCloud备份,默认为YES
@property (assign, nonatomic) BOOL shouldDisableiCloud;

//是否使用内存做缓存,默认为YES
@property (assign, nonatomic) BOOL shouldCacheImagesInMemory;

/** 缓存图片的最长时间,单位是秒,默认是缓存一周
 * 这个缓存图片最长时间是使用磁盘缓存才有意义
 * 使用内存缓存在前文中讲解的几种情况下会自动删除缓存对象
 * 超过最长时间后,会将磁盘中存储的图片自动删除
 */
@property (assign, nonatomic) NSInteger maxCacheAge;

//缓存占用最大的空间,单位是字节
@property (assign, nonatomic) NSUInteger maxCacheSize;

@end

NSCacheConfig类可以看得出来就是一个配置类,保存一些缓存策略的信息,没有太多可以讲解的地方,看懂就好,看一下NSCacheConfig.m文件的源码:

static const NSInteger kDefaultCacheMaxCacheAge = 60 * 60 * 24 * 7; // 1 week

@implementation SDImageCacheConfig

- (instancetype)init {
    if (self = [super init]) {
        _shouldDecompressImages = YES;
        _shouldDisableiCloud = YES;
        _shouldCacheImagesInMemory = YES;
        _maxCacheAge = kDefaultCacheMaxCacheAge;
        _maxCacheSize = 0;
    }
    return self;
}

@end

从上面源码可以看出相关属性的默认值,以及maxCacheAge的默认值为一周时间。

接下来,看一下真正执行缓存操作的SDImageCache类的头文件,接下来的源码分析都是按照源码的顺序来的,只是分为了几个小块,读者也可以按顺序对照源码一起查看:

//获取图片的方式类别枚举
typedef NS_ENUM(NSInteger, SDImageCacheType) {
    //不是从缓存中拿到的,从网上下载的
    SDImageCacheTypeNone,
    //从磁盘中获取的
    SDImageCacheTypeDisk,
    //从内存中获取的
    SDImageCacheTypeMemory
};

//查找缓存完成后的回调块
typedef void(^SDCacheQueryCompletedBlock)(UIImage * _Nullable image, NSData * _Nullable data, SDImageCacheType cacheType);
//在缓存中根据指定key查找图片的回调块
typedef void(^SDWebImageCheckCacheCompletionBlock)(BOOL isInCache);
//计算磁盘缓存图片个数和占用内存大小的回调块
typedef void(^SDWebImageCalculateSizeBlock)(NSUInteger fileCount, NSUInteger totalSize);

上面是一些辅助用的定义,获取图片方式的枚举以及各种情况下的回调块。

/*
SDWebImage真正执行缓存的类
SDImageCache支持内存缓存,默认也可以进行磁盘存储,也可以选择不进行磁盘存储
*/
@interface SDImageCache : NSObject

#pragma mark - Properties

//SDImageCacheConfig对象,缓存策略的配置
@property (nonatomic, nonnull, readonly) SDImageCacheConfig *config;

//内存缓存的最大cost,以像素为单位,后面有具体计算方法
@property (assign, nonatomic) NSUInteger maxMemoryCost;

//内存缓存,缓存对象的最大个数
@property (assign, nonatomic) NSUInteger maxMemoryCountLimit;

上面这一部分是属性的声明,属性很少,但我们在NSCache中都见过了,首先是SDImageCacheConfig,即前面讲解的缓存策略配置,maxMemoryCost其实就是NSCachetotalCostLimit,这里它使用像素为单位进行计算,maxMemoryCountLimit其实就是NSCachecountLimit,需要注意的是SDImageCache继承自NSObject没有继承NSCache,所以它需要保存这些属性。

#pragma mark - Singleton and initialization

//单例方法用来获取一个SDImageCache对象
+ (nonnull instancetype)sharedImageCache;

/*
初始化方法,根据指定的namespace创建一个SDImageCache类的对象
这个namespace默认值是default
主要用于磁盘缓存时创建文件夹时作为其名称使用
*/
- (nonnull instancetype)initWithNamespace:(nonnull NSString *)ns;

//初始化方法,根据指定namespace以及磁盘缓存的文件夹路径来创建一个SDImageCache的对象
- (nonnull instancetype)initWithNamespace:(nonnull NSString *)ns
                       diskCacheDirectory:(nonnull NSString *)directory NS_DESIGNATED_INITIALIZER;

上面几个方法就是其初始化方法,提供了类方法用于获取一个单例对象,使用单例对象就会使用所有的默认配置,下面两个初始化构造函数提供了两个接口但真正进行初始化的是最后一个,通过这样的设计尽可能的抽象出所有共同的部分,简化代码,而且思路更清晰。

#pragma mark - Cache paths
//根据fullNamespace构造一个磁盘缓存的文件夹路径
- (nullable NSString *)makeDiskCachePath:(nonnull NSString*)fullNamespace;

/*
添加一个只读的缓存路径,以后在查找磁盘缓存时也会从这个路径中查找
主要用于查找提前添加的图片
*/
- (void)addReadOnlyCachePath:(nonnull NSString *)path;

上面两个方法主要用于构造磁盘缓存的文件夹路径以及添加一个指定路径到缓存中,以后搜索缓存时也会从这个路径中查找,这样设计就提供了可扩展性,如果以后需要修改缓存路径,只需把之前的路径添加进来即可。

#pragma mark - Store Ops

/*
根据给定的key异步存储图片
image 要存储的图片
key 一张图片的唯一ID,一般使用图片的URL
completionBlock 完成异步存储后的回调块
该方法并不执行任何实际的操作,而是直接调用下面的下面的那个方法
*/
- (void)storeImage:(nullable UIImage *)image
            forKey:(nullable NSString *)key
        completion:(nullable SDWebImageNoParamsBlock)completionBlock;

/*
同上,该方法并不是真正的执行者,而是需要调用下面的那个方法
根据给定的key异步存储图片
image 要存储的图片
key 唯一ID,一般使用URL
toDisk 是否缓存到磁盘中
completionBlock 缓存完成后的回调块
*/
- (void)storeImage:(nullable UIImage *)image
            forKey:(nullable NSString *)key
            toDisk:(BOOL)toDisk
        completion:(nullable SDWebImageNoParamsBlock)completionBlock;

/*
根据给定的key异步存储图片,真正的缓存执行者
image 要存储的图片
imageData 要存储的图片的二进制数据即NSData数据
key 唯一ID,一般使用URL
toDisk 是否缓存到磁盘中
completionBlock
*/
- (void)storeImage:(nullable UIImage *)image
         imageData:(nullable NSData *)imageData
            forKey:(nullable NSString *)key
            toDisk:(BOOL)toDisk
        completion:(nullable SDWebImageNoParamsBlock)completionBlock;
        
/*
根据指定key同步存储NSData类型的图片的数据到磁盘中
这是一个同步的方法,需要放在指定的ioQueue中执行,指定的ioQueue在下面会讲
imageData 图片的二进制数据即NSData类型的对象
key 图片的唯一ID,一般使用URL
*/
- (void)storeImageDataToDisk:(nullable NSData *)imageData forKey:(nullable NSString *)key;

上面几个方法是用来执行存储操作的,提供了内存缓存和磁盘缓存的不同存储方式方法,提供了不同的接口,但真正执行的方法只有一个,这样的设计方式值得我们学习。

#pragma mark - Query and Retrieve Ops

/*
异步方式根据指定的key查询磁盘中是否缓存了这个图片
key 图片的唯一ID,一般使用URL
completionBlock 查询完成后的回调块,这个回调块默认会在主线程中执行
*/
- (void)diskImageExistsWithKey:(nullable NSString *)key completion:(nullable SDWebImageCheckCacheCompletionBlock)completionBlock;

/**
 * Operation that queries the cache asynchronously and call the completion when done.
 *
 * @param key       The unique key used to store the wanted image
 * @param doneBlock The completion block. Will not get called if the operation is cancelled
 *
 * @return a NSOperation instance containing the cache op
 */
- (nullable NSOperation *)queryCacheOperationForKey:(nullable NSString *)key done:(nullable SDCacheQueryCompletedBlock)doneBlock;

/*
同步查询内存缓存中是否有ID为key的图片
key 图片的唯一ID,一般使用URL
*/
- (nullable UIImage *)imageFromMemoryCacheForKey:(nullable NSString *)key;

/*
同步查询磁盘缓存中是否有ID为key的图片
key 图片的唯一ID,一般使用URL
*/
- (nullable UIImage *)imageFromDiskCacheForKey:(nullable NSString *)key;

/*
同步查询内存缓存和磁盘缓存中是否有ID为key的图片
key 图片的唯一ID,一般使用URL
*/
- (nullable UIImage *)imageFromCacheForKey:(nullable NSString *)key;

上面几个方法是查询的方法,提供了丰富的根据图片key查找的功能。

#pragma mark - Remove Ops

/*
根据给定key异步方式删除缓存
key 图片的唯一ID,一般使用URL
completion 操作完成后的回调块
*/
- (void)removeImageForKey:(nullable NSString *)key withCompletion:(nullable SDWebImageNoParamsBlock)completion;

/*
根据给定key异步方式删除内存中的缓存
key 图片的唯一ID,一般使用URL
fromDisk 是否删除磁盘中的缓存,如果为YES那也会删除磁盘中的缓存
completion 操作完成后的回调块
*/
- (void)removeImageForKey:(nullable NSString *)key fromDisk:(BOOL)fromDisk withCompletion:(nullable SDWebImageNoParamsBlock)completion;

#pragma mark - Cache clean Ops

//删除所有的内存缓存,即NSCache中的removeAllObjects
- (void)clearMemory;

/*
异步方式清空磁盘中的所有缓存
completion 删除完成后的回调块
*/
- (void)clearDiskOnCompletion:(nullable SDWebImageNoParamsBlock)completion;

/*
异步删除磁盘缓存中所有超过缓存最大时间的图片,即前面属性中的maxCacheAge
completionBlock 删除完成后的回调块
*/
- (void)deleteOldFilesWithCompletionBlock:(nullable SDWebImageNoParamsBlock)completionBlock;

上面几个方法是用来删除缓存中图片的方法,以及清空内存缓存的方法。

#pragma mark - Cache Info

//获取磁盘缓存占用的存储空间大小,单位是字节
- (NSUInteger)getSize;

//获取磁盘缓存了多少张图片
- (NSUInteger)getDiskCount;

/*
异步方式计算磁盘缓存占用的存储空间大小,单位是字节
completionBlock 计算完成后的回调块
*/
- (void)calculateSizeWithCompletionBlock:(nullable SDWebImageCalculateSizeBlock)completionBlock;

上面几个方法提供了查询磁盘缓存占用内存大小以及缓存图片个数的功能。

#pragma mark - Cache Paths

/*
根据图片的key以及一个存储文件夹路径,构造一个在本地的图片的路径
key 图片的唯一ID,一般使用URL
inPath 本地存储图片的文件夹的路径
比如:图片URL是http:www.baidu.com/test.png inPath是/usr/local/,那么图片存储到本地后的路径为:/usr/local/test.png
*/
- (nullable NSString *)cachePathForKey:(nullable NSString *)key inPath:(nonnull NSString *)path;

/*
根据图片的key获取一个默认的缓存在本地的路径
key 图片的唯一ID,一般使用URL
*/
- (nullable NSString *)defaultCachePathForKey:(nullable NSString *)key;

@end

上面几个方法是用来构造图片保存到磁盘中的路径的功能。

完整阅读完上述代码后,可以发现SDImageCache提供了缓存图片的增删查功能,并提供了磁盘缓存路径相关的一系列功能函数。上面的代码中的注释已经详细解释了每个函数的功能,这里不再赘述了,接下来看一下具体的实现代码:

@interface AutoPurgeCache : NSCache
@end

@implementation AutoPurgeCache

- (nonnull instancetype)init {
    self = [super init];
    if (self) {
#if SD_UIKIT
        //收到系统内存警告后直接调用 removeAllObjects 删除所有缓存对象
        [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(removeAllObjects) name:UIApplicationDidReceiveMemoryWarningNotification object:nil];
#endif
    }
    return self;
}

- (void)dealloc {
#if SD_UIKIT
    [[NSNotificationCenter defaultCenter] removeObserver:self name:UIApplicationDidReceiveMemoryWarningNotification object:nil];
#endif
}

@end

首先它定义了一个AutoPurgeCache的类,这个类继承自NSCache,它只重写了initdealloc方法,在这两个方法中进行了添加和删除系统内存警告通知的监听操作,目的就是为了在收到系统内存警告后进行内存的清理工作。

继续看下面的代码:

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
}

上面定义了内联函数,用于计算每个图片的cost值,由于不同平台图片编码的方式不同,在iOSwatchOS上一张图片的实际大小与一个scale缩放值相关,所以需要使用image.size.height * image.scale来计算真正的图片高度,同理也需要计算真正的宽度,这样就可以计算出图片的实际大小,而在macOS下图片的大小就是实际的宽高乘积。所以,这里使用的cost就是图片的大小,也即图片的像素个数。

继续看下面的代码:

@interface SDImageCache ()

#pragma mark - Properties
//缓存对象
@property (strong, nonatomic, nonnull) NSCache *memCache;
//磁盘缓存的路径
@property (strong, nonatomic, nonnull) NSString *diskCachePath;
//自定义缓存查询路径,即前面add*方法添加的路径,都添加到这个数组中
@property (strong, nonatomic, nullable) NSMutableArray<NSString *> *customPaths;
/*
专门用来执行IO操作的队列,这是一个串行队列
使用串行队列就解决了很多问题,串行队列依次执行就不需要加锁释放锁操作来防止多线程下的异常问题
*/
@property (SDDispatchQueueSetterSementics, nonatomic, nullable) dispatch_queue_t ioQueue;

@end

@implementation SDImageCache {
    //文件操作的类
    NSFileManager *_fileManager;
}

这里使用extension又添加了几个属性,最重要的就是memCache了,一个NSCache类或子类的对象,真正进行内存缓存的对象。

继续看下面的代码:

#pragma mark - Singleton, init, dealloc
//类方法,返回一个单例对象
+ (nonnull instancetype)sharedImageCache {
    //使用GCD构造单例对象
    static dispatch_once_t once;
    static id instance;
    dispatch_once(&once, ^{
        //调用默认构造函数
        instance = [self new];
    });
    return instance;
}

/*
默认构造函数,调用initWithNamespace:执行初始化操作
默认的namespace为default,这个属性就是用来创建一个磁盘缓存存储文件夹
*/
- (instancetype)init {
    return [self initWithNamespace:@"default"];
}

//根据指定的namespace构造一个磁盘缓存的存储路径后调用initWithNamespace:diskCacheDirectory方法完成后续的初始化操作
- (nonnull instancetype)initWithNamespace:(nonnull NSString *)ns {
    /*
    makeDiskCachePath的目的是为了创建一个磁盘缓存存储图片的文件夹
    获取一个系统沙盒的cache目录下名称为ns的文件夹的路径
    比如:/usr/local/cache/default
    所以namespace的作用就是为了在沙盒的cache目录下创建一个文件夹时作为它的名称,以后去磁盘中查找时就有路径了
    */
    NSString *path = [self makeDiskCachePath:ns];
    return [self initWithNamespace:ns diskCacheDirectory:path];
}

/*
真正执行初始化操作的构造函数
ns 即namespace
directory 即磁盘缓存存储图片的文件夹路径
*/
- (nonnull instancetype)initWithNamespace:(nonnull NSString *)ns
                       diskCacheDirectory:(nonnull NSString *)directory {
    if ((self = [super init])) {
        //构造一个全限定名的namespace
        NSString *fullNamespace = [@"com.hackemist.SDWebImageCache." stringByAppendingString:ns];
        
        // Create IO serial queue
        //创建一个串行的专门执行IO操作的队列
        _ioQueue = dispatch_queue_create("com.hackemist.SDWebImageCache", DISPATCH_QUEUE_SERIAL);
        
        //构造一个SDImageCacheConfig对象
        _config = [[SDImageCacheConfig alloc] init];
        
        // Init the memory cache
        //创建一个AutoPurgeCache对象,即NSCache的子类
        _memCache = [[AutoPurgeCache alloc] init];
        //指定这个缓存对象的名称为前面的全限定名
        _memCache.name = fullNamespace;

        // Init the disk cache
        //如果传入的磁盘缓存的文件夹路径不为空
        if (directory != nil) {
            //在文件夹路径后面再创建一个文件夹,名称为全限定名名称
            _diskCachePath = [directory stringByAppendingPathComponent:fullNamespace];
        } else {
            //如果传入的磁盘缓存文件夹路径是空的就根据传入的ns获取一个沙盒cache目录下名称为ns的文件夹路径
            NSString *path = [self makeDiskCachePath:ns];
            _diskCachePath = path;
        }
        //同步方法在这个IO队列上进行fileManager的创建工作
        dispatch_sync(_ioQueue, ^{
            _fileManager = [NSFileManager new];
        });

#if SD_UIKIT
        // Subscribe to app events
        //监听收到系统内存警告的通知,收到后执行clearMemory方法
        [[NSNotificationCenter defaultCenter] addObserver:self
                                                 selector:@selector(clearMemory)
                                                     name:UIApplicationDidReceiveMemoryWarningNotification
                                                   object:nil];
         
         //监听程序即将终止的通知,收到后执行deleteOldFiles方法

        [[NSNotificationCenter defaultCenter] addObserver:self
                                                 selector:@selector(deleteOldFiles)
                                                     name:UIApplicationWillTerminateNotification
                                                   object:nil];
        
        //监听程序进入后台的通知,收到后执行backgroundDeleteOldFiles方法

        [[NSNotificationCenter defaultCenter] addObserver:self
                                                 selector:@selector(backgroundDeleteOldFiles)
                                                     name:UIApplicationDidEnterBackgroundNotification
                                                   object:nil];
#endif
    }

    return self;
}

//析构函数
- (void)dealloc {
    //移除所有通知的监听器
    [[NSNotificationCenter defaultCenter] removeObserver:self];
    //释放ioQueue
    SDDispatchQueueRelease(_ioQueue);
}

//检查当前执行队列是否为ioQueue
- (void)checkIfQueueIsIOQueue {
    //GCD都是C API所以需要使用C字符串
    const char *currentQueueLabel = dispatch_queue_get_label(DISPATCH_CURRENT_QUEUE_LABEL);
    const char *ioQueueLabel = dispatch_queue_get_label(self.ioQueue);
    if (strcmp(currentQueueLabel, ioQueueLabel) != 0) {
        NSLog(@"This method should be called from the ioQueue");
    }
}

上面的构造函数和析构函数都很好理解,主要功能就是创建了NSCache对象以及构造了一个磁盘缓存存储图片的文件夹路径并监听了一些通知用于清除缓存的操作。

接下里继续看代码:

//添加只读的用户自行添加的缓存搜索路径
- (void)addReadOnlyCachePath:(nonnull NSString *)path {
    //如果这个路径集合为空就创建一个
    if (!self.customPaths) {
        self.customPaths = [NSMutableArray new];
    }
    //如果路径集合中不包含这个新的路径就添加
    if (![self.customPaths containsObject:path]) {
        [self.customPaths addObject:path];
    }
}

/*
根据指定的图片的key和指定文件夹路径获取图片存储的绝对路径
首先通过cachedFileNameForKey:方法根据URL获取一个MD5值作为这个图片的名称
接着在这个指定路径path后面添加这个MD5名称作为这个图片在磁盘中的绝对路径
*/
- (nullable NSString *)cachePathForKey:(nullable NSString *)key inPath:(nonnull NSString *)path {
    NSString *filename = [self cachedFileNameForKey:key];
    return [path stringByAppendingPathComponent:filename];
}

/*
该方法与上面的方法一样,内部调用上面的方法
不过它使用默认的磁盘缓存路径diskCachePath,就是在构造函数中获取的沙盒cache下的一个文件夹的路径
*/
- (nullable NSString *)defaultCachePathForKey:(nullable NSString *)key {
    return [self cachePathForKey:key inPath:self.diskCachePath];
}

/*
根据图片的key,即URL构造一个MD5串,添加原来的后缀后作为这个图片在磁盘中存储时的名称
MD5算法保证了不同URL散列出的值不同,也就保证了不同URL图片的名称不同
具体算法不在本篇博客的讲述范围,有兴趣的读者自行查阅
*/
- (nullable NSString *)cachedFileNameForKey:(nullable NSString *)key {
    const char *str = key.UTF8String;
    if (str == NULL) {
        str = "";
    }
    unsigned char r[CC_MD5_DIGEST_LENGTH];
    CC_MD5(str, (CC_LONG)strlen(str), r);
    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], [key.pathExtension isEqualToString:@""] ? @"" : [NSString stringWithFormat:@".%@", key.pathExtension]];

    return filename;
}

/*
根据给定的fullNamespace构造一个磁盘缓存存储图片的路径
首先获取了沙盒下的cache目录
然后将fullNamespace添加进这个路径作为cache下的一个文件夹名称
*/
- (nullable NSString *)makeDiskCachePath:(nonnull NSString*)fullNamespace {
    NSArray<NSString *> *paths = NSSearchPathForDirectoriesInDomains(NSCachesDirectory, NSUserDomainMask, YES);
    return [paths[0] stringByAppendingPathComponent:fullNamespace];
}

上面的一系列方法提供了构造图片存储在磁盘中的绝对路径的功能,主要就是使用MD5算法散列图片的URL来创建图片存储在磁盘的文件名,并且根据namespace构造一个沙盒cache目录下的一个路径。上述函数很简单,就不再赘述了。

接下来继续看代码:

#pragma mark - Store Ops
//存储图片到缓存,直接调用下面的下面的方法
- (void)storeImage:(nullable UIImage *)image
            forKey:(nullable NSString *)key
        completion:(nullable SDWebImageNoParamsBlock)completionBlock {
     //使用该方法默认会缓存到磁盘中
    [self storeImage:image imageData:nil forKey:key toDisk:YES completion:completionBlock];
}

//存储图片到缓存,直接调用下面的方法
- (void)storeImage:(nullable UIImage *)image
            forKey:(nullable NSString *)key
            toDisk:(BOOL)toDisk
        completion:(nullable SDWebImageNoParamsBlock)completionBlock {
     //该方法是否缓存到磁盘由用户指定
    [self storeImage:image imageData:nil forKey:key toDisk:toDisk completion:completionBlock];
}

/*
真正执行存储操作的方法
*/
- (void)storeImage:(nullable UIImage *)image
         imageData:(nullable NSData *)imageData
            forKey:(nullable NSString *)key
            toDisk:(BOOL)toDisk
        completion:(nullable SDWebImageNoParamsBlock)completionBlock {
    //如果image为nil或image的URL为空直接返回即不执行保存操作
    if (!image || !key) {
        //如果回调块存在就执行完成回调块
        if (completionBlock) {
            completionBlock();
        }
        return;
    }
    // if memory cache is enabled
    //如果缓存策略指明要进行内存缓存
    if (self.config.shouldCacheImagesInMemory) {
        //根据前面的内联函数计算图片的大小作为cost
        NSUInteger cost = SDCacheCostForImage(image);
        //向memCache中添加图片对象,key即图片的URL,cost为上面计算的
        [self.memCache setObject:image forKey:key cost:cost];
    }
    
    //如果要保存到磁盘中
    if (toDisk) {
        //异步提交任务到串行的ioQueue中执行
        dispatch_async(self.ioQueue, ^{
            //进行磁盘存储的具体的操作,使用@autoreleasepool包围,执行完成后自动释放相关对象
            //我猜测这么做是为了尽快释放产生的局部变量,释放内存
            @autoreleasepool {
                NSData *data = imageData;
                //如果传入的imageData为空,图片不为空
                if (!data && image) {
                    // If we do not have any data to detect image format, use PNG format
                    //调用编码方法,获取NSData对象
                    //图片编码为NSData不在本文的讲述范围,可自行查阅
                    data = [[SDWebImageCodersManager sharedInstance] encodedDataWithImage:image format:SDImageFormatPNG];
                }
                //调用下面的方法用于磁盘存储操作
                [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 {
    //判断图片NSData数据以及图片key是否为空,如果为空直接返回
    if (!imageData || !key) {
        return;
    }
    //检查当前执行队列是否为ioQueue,如果不是会提示开发者
    [self checkIfQueueIsIOQueue];
    
    //如果构造函数中构造的磁盘缓存存储图片路径的文件夹不存在
    if (![_fileManager fileExistsAtPath:_diskCachePath]) {
        //那就根据这个路径创建需要的文件夹
        [_fileManager createDirectoryAtPath:_diskCachePath withIntermediateDirectories:YES attributes:nil error:NULL];
    }
    
    // get cache Path for image key
    // 根据key获取默认磁盘缓存存储路径下的MD5文件名的文件的绝对路径
    // 感觉有点绕口。。就是获取图片二进制文件在磁盘中的绝对路径,名称就是前面使用MD5散列的,路径就是构造函数默认构造的那个路径
    NSString *cachePathForKey = [self defaultCachePathForKey:key];
    // transform to NSUrl
    // 根据这个绝对路径创建一个NSURL对象
    NSURL *fileURL = [NSURL fileURLWithPath:cachePathForKey];
    //使用NSFileManager创建一个文件,文件存储的数据就是imageData
    //到此,图片二进制数据就存储在了磁盘中了
    [_fileManager createFileAtPath:cachePathForKey contents:imageData attributes:nil];
    
    // disable iCloud backup
    if (self.config.shouldDisableiCloud) {
        [fileURL setResourceValue:@YES forKey:NSURLIsExcludedFromBackupKey error:nil];
    }
}

上面就是图片缓存存储的核心方法了,其实看下来感觉也蛮简单的,如果要进行内存缓存就直接添加到memCache对象中,如果要进行磁盘缓存,就构造一个路径,构造一个文件名,然后存储起来就好了。这里面有几个重要的点,首先就是@autoreleasepool的使用,其实这里不添加这个autoreleasepool同样会自动释放内存,但添加后在这个代码块结束后就会立即释放,不会占用太多内存。其次,对于磁盘写入的操作是通过一个指定的串行队列实现的,这样不管执行多少个磁盘存储的操作,都必须一个一个的存储,这样就可以不用编写加锁的操作,可能有读者会疑惑为什么要进行加锁,因为并发情况下这些存储操作都不是线程安全的,很有可能会把路径修改掉或者产生其他异常行为,但使用了串行队列就完全不需要考虑加锁释放锁,一张图片存储完成才可以进行下一张图片存储的操作,这一点值得学习。

接下来继续看其他源码:

#pragma mark - Query and Retrieve Ops

//异步方式根据key判断磁盘缓存中是否存储了这个图片,查询完成后执行回调块
- (void)diskImageExistsWithKey:(nullable NSString *)key completion:(nullable SDWebImageCheckCacheCompletionBlock)completionBlock {
    //查询操作是异步,也放在指定的串行ioQueue中查询
    dispatch_async(_ioQueue, ^{
        /*
        调用defualtCachePathForKey:方法获取图片如果在本地存储时的绝对路径
        使用NSFileManager查询这个绝对路径的文件是否存在
        */
        BOOL exists = [_fileManager fileExistsAtPath:[self defaultCachePathForKey:key]];

        // fallback because of https://github.com/rs/SDWebImage/pull/976 that added the extension to the disk file name
        // checking the key with and without the extension
        //如果不存在
        if (!exists) {
            //再次去掉后缀名查询,这个问题可以自行查看上面git的问题
            exists = [_fileManager fileExistsAtPath:[self defaultCachePathForKey:key].stringByDeletingPathExtension];
        }
        
        //查询完成后,如果存在回调块,就在主线程执行回调块并传入exists
        if (completionBlock) {
            dispatch_async(dispatch_get_main_queue(), ^{
                completionBlock(exists);
            });
        }
    });
}

//查询内存缓存中是否有指定key的缓存数据
- (nullable UIImage *)imageFromMemoryCacheForKey:(nullable NSString *)key {
    //直接调用NSCache的objectForKey:方法查询
    return [self.memCache objectForKey:key];
}

//根据指定的key获取磁盘缓存的图片构造并返回UIImage对象
- (nullable UIImage *)imageFromDiskCacheForKey:(nullable NSString *)key {
    //调用diskImageForKey:方法查询,这个方法下面会讲
    UIImage *diskImage = [self diskImageForKey:key];
    //如果找到了,并且缓存策略使用了内存缓存
    if (diskImage && self.config.shouldCacheImagesInMemory) {
        //计算cost并且将磁盘中获取的图片放入到内存缓存中
        NSUInteger cost = SDCacheCostForImage(diskImage);
        //调用NSCache的setObject:forKey:cost方法设置要缓存的对象
        //之所以要设置是因为如果是第一次从磁盘中拿出此时内存缓存中还没有
        //还有可能是内存缓存中的对象被删除了,然后在磁盘中找到了,此时也需要设置一下
        //setObject:forKey:cost方法的时间复杂度是常量的,所以哪怕内存中有也无所谓
        [self.memCache setObject:diskImage forKey:key cost:cost];
    }

    return diskImage;
}

//查找内存缓存和磁盘缓存中是否有指定key的图片
- (nullable UIImage *)imageFromCacheForKey:(nullable NSString *)key {
    // First check the in-memory cache...
    //首先检查内存缓存中是否有,有就返回,调用了上面的那个方法
    //实际就是执行了NSCache的 objectForKey:方法
    UIImage *image = [self imageFromMemoryCacheForKey:key];
    if (image) {
        return image;
    }
    
    // Second check the disk cache...
    //如果内存缓存中没有再去磁盘中查找
    image = [self imageFromDiskCacheForKey:key];
    return image;
}

//在磁盘中所有的保存路径,包括用户添加的路径中搜索key对应的图片数据
- (nullable NSData *)diskImageDataBySearchingAllPathsForKey:(nullable NSString *)key {
    //首先在默认存储路径中查找,如果有就直接返回
    NSString *defaultPath = [self defaultCachePathForKey:key];
    NSData *data = [NSData dataWithContentsOfFile:defaultPath];
    if (data) {
        return data;
    }

    // fallback because of https://github.com/rs/SDWebImage/pull/976 that added the extension to the disk file name
    // checking the key with and without the extension
    //同样的去掉后缀再次查找,找到就返回
    data = [NSData dataWithContentsOfFile:defaultPath.stringByDeletingPathExtension];
    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];
        if (imageData) {
            return imageData;
        }

        // fallback because of https://github.com/rs/SDWebImage/pull/976 that added the extension to the disk file name
        // checking the key with and without the extension
        //去掉后缀再次查找
        imageData = [NSData dataWithContentsOfFile:filePath.stringByDeletingPathExtension];
        if (imageData) {
            return imageData;
        }
    }
    //没找到返回nil
    return nil;
}

//在磁盘中查找指定key的图片数据,然后转换为UIImage对象返回
- (nullable UIImage *)diskImageForKey:(nullable NSString *)key {
    //调用上面的方法查找所有路径下是否存在对应key的图片数据
    NSData *data = [self diskImageDataBySearchingAllPathsForKey:key];
    //如果有就解码解压缩后返回UIImage对象
    if (data) {
        UIImage *image = [[SDWebImageCodersManager sharedInstance] decodedImageWithData:data];
        image = [self scaledImageForKey:key image:image];
        if (self.config.shouldDecompressImages) {
            image = [[SDWebImageCodersManager sharedInstance] decompressedImageWithImage:image data:&data options:@{SDWebImageCoderScaleDownLargeImagesKey: @(NO)}];
        }
        return image;
    } else {
        return nil;
    }
}

//在iOS watchOS下图片的真实大小与scale有关,这里做一下缩放处理
- (nullable UIImage *)scaledImageForKey:(nullable NSString *)key image:(nullable UIImage *)image {
    return SDScaledImageForKey(key, image);
}

/*
在缓存中查找指定key的图片是否存在,完成后执行回调块
返回一个NSOperation,调用者可以随时取消查询
提供这个功能主要是因为在磁盘中查找真的很耗时,调用者可能在一段时间后就不查询了
这个NSOperation更像是一个标记对象,标记调用者是否取消了查询操作,完美的利用了NSOperation的cancel方法
*/
- (nullable NSOperation *)queryCacheOperationForKey:(nullable NSString *)key done:(nullable SDCacheQueryCompletedBlock)doneBlock {
    //如果key为空执行回调块返回nil
    if (!key) {
        if (doneBlock) {
            //SDImageCacheTypeNone表示没有缓存数据
            doneBlock(nil, nil, SDImageCacheTypeNone);
        }
        return nil;
    }

    // First check the in-memory cache...
    //查找内存缓存中是否存在,调用了前面的方法
    UIImage *image = [self imageFromMemoryCacheForKey:key];
    //如果存在,就在磁盘中查找对应的二进制数据,然后执行回调块
    if (image) {
        NSData *diskData = nil;
        if (image.images) {
            diskData = [self diskImageDataBySearchingAllPathsForKey:key];
        }
        if (doneBlock) {
            //SDImageCacheTypeMemory表示图片在内存缓存中查找到
            doneBlock(image, diskData, SDImageCacheTypeMemory);
        }
        //NSOperation为nil
        return nil;
    }
    
    //接下来就需要在磁盘中查找了,由于耗时构造一个NSOperation对象
    //下面是异步方式在ioQueue上进行查询操作,所以直接就返回了NSOperation对象
    NSOperation *operation = [NSOperation new];
    //异步在ioQueue上查询
    dispatch_async(self.ioQueue, ^{
        //ioQueue是串行的,而且磁盘操作很慢,有可能还没开始查询调用者就取消查询
        //如果在开始查询后调用者再取消就没有用了,只有在查询前取消才有用
        if (operation.isCancelled) {
            // do not call the completion if cancelled
            //如果是调用者取消查询不执行回调块
            return;
        }
        //同理创建一个自动释放池,
        @autoreleasepool {
            //在磁盘中查找图片二进制数据,和UIImage对象
            NSData *diskData = [self diskImageDataBySearchingAllPathsForKey:key];
            UIImage *diskImage = [self diskImageForKey:key];
            //找到并且需要内存缓存就设置一下
            if (diskImage && self.config.shouldCacheImagesInMemory) {
                NSUInteger cost = SDCacheCostForImage(diskImage);
                [self.memCache setObject:diskImage forKey:key cost:cost];
            }
            //在主线程中执行回调块
            if (doneBlock) {
                dispatch_async(dispatch_get_main_queue(), ^{
                    //SDImageCacheTypeDisk表示在磁盘中找到
                    doneBlock(diskImage, diskData, SDImageCacheTypeDisk);
                });
            }
        }
    });

    return operation;
}

上面的方法提供了内存缓存和磁盘缓存中查找的功能,比较精明的设计就是返回NSOperation对象,这个对象并不代表一个任务,仅仅利用了它的cancel方法和isCancelled属性,来取消磁盘查询。上面的代码也很简单,就不再赘述啦!

接下来继续看:

#pragma mark - Remove Ops
//删除缓存总指定key的图片,删除完成后的回调块completion
//该方法也直接调用了下面的方法,默认也删除磁盘的数据
- (void)removeImageForKey:(nullable NSString *)key withCompletion:(nullable SDWebImageNoParamsBlock)completion {
    [self removeImageForKey:key fromDisk:YES withCompletion:completion];
}

//根据指定key删除图片数据
- (void)removeImageForKey:(nullable NSString *)key fromDisk:(BOOL)fromDisk withCompletion:(nullable SDWebImageNoParamsBlock)completion {
    //图片key为nil直接返回
    if (key == nil) {
        return;
    }
    //先判断缓存策略是否有内存缓存,有就删除内存缓存
    if (self.config.shouldCacheImagesInMemory) {
        //调用NSCache的removeObjectForKey方法
        [self.memCache removeObjectForKey:key];
    }
    //如果要删除磁盘数据
    if (fromDisk) {
        //异步方式在ioQueue上执行删除操作
        dispatch_async(self.ioQueue, ^{
            //使用key构造一个默认路径下的文件存储的绝对路径
            //调用NSFileManager删除该路径的文件
            [_fileManager removeItemAtPath:[self defaultCachePathForKey:key] error:nil];
            //有回调块就在主线程中执行
            if (completion) {
                dispatch_async(dispatch_get_main_queue(), ^{
                    completion();
                });
            }
        });
     //不需要删除磁盘数据并且有回调块就直接执行
    } else if (completion){
        completion();
    }
    
}

上面的删除操作也很好理解,内存缓存就直接删除NSCache对象的数据,磁盘缓存就直接获取文件的绝对路径后删除即可。

继续看代码:

# pragma mark - Mem Cache settings

- (void)setMaxMemoryCost:(NSUInteger)maxMemoryCost {
    self.memCache.totalCostLimit = maxMemoryCost;
}

- (NSUInteger)maxMemoryCost {
    return self.memCache.totalCostLimit;
}

- (NSUInteger)maxMemoryCountLimit {
    return self.memCache.countLimit;
}

- (void)setMaxMemoryCountLimit:(NSUInteger)maxCountLimit {
    self.memCache.countLimit = maxCountLimit;
}

memCache就是AutoPurgeCache对象,即NSCache的子类,相关设置值的方法直接就设置了NSCache的相关属性。

继续看:

#pragma mark - Cache clean Ops
//清除缓存的操作,在收到系统内存警告通知时执行
- (void)clearMemory {
    //调用NSCache方法删除所有缓存对象
    [self.memCache removeAllObjects];
}

//清空磁盘的缓存,完成后的回调块completion
- (void)clearDiskOnCompletion:(nullable SDWebImageNoParamsBlock)completion {
    //使用异步提交在ioQueue中执行
    dispatch_async(self.ioQueue, ^{
        //获取默认的图片存储路径然后使用NSFileManager删除这个路径的所有文件及文件夹
        [_fileManager removeItemAtPath:self.diskCachePath error:nil];
        //删除以后再创建一个空的文件夹
        [_fileManager createDirectoryAtPath:self.diskCachePath
                withIntermediateDirectories:YES
                                 attributes:nil
                                      error:NULL];
        //完成后有回调块就在主线程中执行
        if (completion) {
            dispatch_async(dispatch_get_main_queue(), ^{
                completion();
            });
        }
    });
}

//删除磁盘中老的即超过缓存最长时限maxCacheAge的图片,直接调用下面的方法
- (void)deleteOldFiles {
    [self deleteOldFilesWithCompletionBlock:nil];
}

//删除磁盘中老的即超过缓存最长时限maxCacheAge的图片,完成后回调块completionBlock
- (void)deleteOldFilesWithCompletionBlock:(nullable SDWebImageNoParamsBlock)completionBlock {
    //异步方式在ioQueue上执行
    dispatch_async(self.ioQueue, ^{
        //获取磁盘缓存存储图片的路径构造为NSURL对象
        NSURL *diskCacheURL = [NSURL fileURLWithPath:self.diskCachePath isDirectory:YES];
        //后面会用到,查询文件的属性
        NSArray<NSString *> *resourceKeys = @[NSURLIsDirectoryKey, NSURLContentModificationDateKey, NSURLTotalFileAllocatedSizeKey];

        // This enumerator prefetches useful properties for our cache files.
        //构造一个存储图片目录的迭代器,使用了上面的文件属性
        NSDirectoryEnumerator *fileEnumerator = [_fileManager enumeratorAtURL:diskCacheURL
                                                   includingPropertiesForKeys:resourceKeys
                                                                      options:NSDirectoryEnumerationSkipsHiddenFiles
                                                                 errorHandler:NULL];
        //构造过期日期,即当前时间往前maxCacheAge秒的日期
        NSDate *expirationDate = [NSDate dateWithTimeIntervalSinceNow:-self.config.maxCacheAge];
        //缓存的文件的字典
        NSMutableDictionary<NSURL *, NSDictionary<NSString *, id> *> *cacheFiles = [NSMutableDictionary dictionary];
        //当前缓存大小
        NSUInteger currentCacheSize = 0;

        // Enumerate all of the files in the cache directory.  This loop has two purposes:
        //
        //  1. Removing files that are older than the expiration date.
        //  2. Storing file attributes for the size-based cleanup pass.
        //需要删除的图片的文件URL
        NSMutableArray<NSURL *> *urlsToDelete = [[NSMutableArray alloc] init];
        //遍历上面创建的那个目录迭代器
        for (NSURL *fileURL in fileEnumerator) {
            NSError *error;
            //根据resourcesKeys获取文件的相关属性
            NSDictionary<NSString *, id> *resourceValues = [fileURL resourceValuesForKeys:resourceKeys error:&error];

            // Skip directories and errors.
            //有错误,然后属性为nil或者路径是个目录就continue
            if (error || !resourceValues || [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[fileURL] = resourceValues;
        }
        //遍历要删除的过期的图片文件URL集合,并删除文件
        for (NSURL *fileURL in urlsToDelete) {
            [_fileManager removeItemAtURL:fileURL error:nil];
        }

        // If our remaining disk cache exceeds a configured maximum size, perform a second
        // size-based cleanup pass.  We delete the oldest files first.
        //如果缓存策略配置了最大缓存大小,并且当前缓存的大小大于这个值则需要清理
        if (self.config.maxCacheSize > 0 && currentCacheSize > self.config.maxCacheSize) {
            // Target half of our maximum cache size for this cleanup pass.
            //清理到只占用最大缓存大小的一半
            const NSUInteger desiredCacheSize = self.config.maxCacheSize / 2;

            // Sort the remaining cache files by their last modification time (oldest first).
            //根据文件创建的日期排序
            NSArray<NSURL *> *sortedFiles = [cacheFiles keysSortedByValueWithOptions:NSSortConcurrent
                                                                     usingComparator:^NSComparisonResult(id obj1, id obj2) {
                                                                         return [obj1[NSURLContentModificationDateKey] compare:obj2[NSURLContentModificationDateKey]];
                                                                     }];

            // Delete files until we fall below our desired cache size.
            //按创建的先后顺序遍历,然后删除,直到缓存大小是最大值的一半
            for (NSURL *fileURL in sortedFiles) {
                if ([_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();
            });
        }
    });
}

#if SD_UIKIT
//在ios下才会有的函数
//写不动了,就是在后台删除。。。自己看看吧。。。唉
- (void)backgroundDeleteOldFiles {
    Class UIApplicationClass = NSClassFromString(@"UIApplication");
    if(!UIApplicationClass || ![UIApplicationClass respondsToSelector:@selector(sharedApplication)]) {
        return;
    }
    UIApplication *application = [UIApplication performSelector:@selector(sharedApplication)];
    __block UIBackgroundTaskIdentifier bgTask = [application beginBackgroundTaskWithExpirationHandler:^{
        // Clean up any unfinished task business by marking where you
        // stopped or ending the task outright.
        [application endBackgroundTask:bgTask];
        bgTask = UIBackgroundTaskInvalid;
    }];

    // Start the long-running task and return immediately.
    [self deleteOldFilesWithCompletionBlock:^{
        [application endBackgroundTask:bgTask];
        bgTask = UIBackgroundTaskInvalid;
    }];
}
#endif

上面就是删除磁盘中过期的图片,以及当缓存大小大于配置的值时,进行缓存清理。

继续吧:

#pragma mark - Cache Info
//计算磁盘缓存占用空间大小
- (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<NSString *, id> *attrs = [[NSFileManager defaultManager] attributesOfItemAtPath:filePath error:nil];
            size += [attrs fileSize];
        }
    });
    return size;
}

//计算磁盘缓存图片的个数
- (NSUInteger)getDiskCount {
    __block NSUInteger count = 0;
    dispatch_sync(self.ioQueue, ^{
        NSDirectoryEnumerator *fileEnumerator = [_fileManager enumeratorAtPath:self.diskCachePath];
        count = fileEnumerator.allObjects.count;
    });
    return count;
}

//同时计算磁盘缓存图片占用空间大小和缓存图片的个数,然后调用回调块,传入相关参数
- (void)calculateSizeWithCompletionBlock:(nullable SDWebImageCalculateSizeBlock)completionBlock {
    NSURL *diskCacheURL = [NSURL fileURLWithPath:self.diskCachePath isDirectory:YES];
    //异步在ioQueue中执行
    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);
            });
        }
    });
}

@end

上面的方法就是用来计算磁盘中缓存图片的数量和占用磁盘空间大小。

整个SDWebImage的缓存模块到此就结束了,阅读完后可以发现,整个代码很好理解,但是设计的也很巧妙,各种情况都考虑的很周全,这些都值得我们学习,尤其是所有IO操作使用一个串行队列来执行,避免加锁释放锁的复杂,还有就是使用NSOperation作为一个标识用来取消耗时的磁盘查询任务。整个代码简洁易懂,接口设计的很完善,是我们学习的榜样。

补充

最近还研究了一下YYCache的源码,YYCache包括了内存缓存和磁盘缓存两部分。

对于内存缓存可以说作者为了提升性能无所不用其极,使用Core Foundation提供的C字典CFMutableDictionaryRef来存储封装的缓存对象,并构造了一个双向链表,维护链表并使用LRU淘汰算法来剔除超过限制的缓存对象,使用pthread_mutext互斥锁来保证线程安全,包括释放对象使用了一个小技巧使得可以在子线程中释放,而不需要在主线程中执行,直接访问ivar而不使用getter/setter,一系列的优化方法使得YYCache的内存缓存效率超过了NSCache及其他第三方库。

对于磁盘缓存,作者参考了NSURLCache的实现及其他第三方的实现,采用文件系统结合SQLite的实现方式,实验发现对于20KB以上的数据,文件系统的读写速度高于SQLite,所以当数据大于20KB时直接将数据保存在文件系统中,在数据库中保存元数据,并添加索引,数据小于20KB时直接保存在数据库中,这样,就能够快速统计相关数据来实现淘汰。SDWebImage的磁盘缓存使用的只有文件系统。

读了YYCache源码让我明白了,不能一味的迷信苹果为我们提供的类,为了追求更极致的性能需要做大量的对比试验来确定技术方案。

读者可以参考YYCache作者的博客YYCache设计思路,有兴趣的读者可以研究一下其源码,值得学习的有很多。

备注

由于作者水平有限,难免出现纰漏,如有问题还请不吝赐教。