你要知道的NSCache都在这里
转载请注明出处 http://blog.youkuaiyun.com/u014205968/article/details/78356828
本篇文章首先会详细讲解NSCache
的基本使用,NSCache
是Foundation
框架提供的缓存类的实现,使用方式类似于可变字典,由于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
,因为在添加后面的对象时前面的被删除了,所以,当我们从缓存中获取对象时一定要判断是否为空,我们无法保证缓存中的某个对象不会被删除。
接着调用了NSCache
的removeAllObjects
方法,一旦调用该方法,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
的源码看看它是怎样进行图片的缓存操作的。由于篇幅的问题,本文将源码中的英文注释删掉了,有需要的读者可以对照着注释源码查阅本文章。本节内容包括了GCD
、NSOperation
等多线程相关的知识,有疑问的读者可以查阅本博客iOS多线程——你要知道的GCD都在这里 以及 iOS多线程——你要知道的NSOperation都在这里 相关内容,本文就不再赘述了。
首先看一下官方给的设置图片后执行时序图:
整个执行流程非常清晰明了,本篇文章的重点在第四步、第五步和第八步,关于网络下载,以后会在讲解NSURLSession
后进行相关的源码分析。查看SDWebImage
的源码,与缓存有关的一共有四个文件SDImageCacheConfig
和SDImageCache
,首先看一下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 *