iOS缓存 NSCache详解及SDWebImage缓存策略源码分析

Posted 这个名字到底好不好

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了iOS缓存 NSCache详解及SDWebImage缓存策略源码分析相关的知识,希望对你有一定的参考价值。

你要知道的NSCache都在这里

转载请注明出处 http://blog.csdn.net/u014205968/article/details/78356828

本篇文章首先会详细讲解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都在这里 相关内容,本文就不再赘述了。

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

整个执行流程非常清晰明了,本篇文章的重点在第四步、第五步和第八步,关于网络下载,以后会在讲解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];
                
                //调用下面的方法用于磁盘存储操作
  

以上是关于iOS缓存 NSCache详解及SDWebImage缓存策略源码分析的主要内容,如果未能解决你的问题,请参考以下文章

深入源码理解YYCache SDWebImageAFNetworkingNSCache 缓存方式与对比

深入源码理解YYCache SDWebImageAFNetworkingNSCache 缓存方式与对比

iOS缓存策略之NSCache的简单使用

iOS开发-NSCache

NSCache

说说NSCache优于NSDictionary的几点