NSCache详解
Tips:NSCache是Foundation框架提供的缓存类的实现,使用方式类似于可变字典。由于NSMutableDictionary的存在,很多人在实现缓存时都会使用可变字典,但是NSCache在实现缓存功能时比可变字典更方便,最重要的它是线程安全的,而NSmutableDictionary不是线程安全的,在多线程环境下使用NSCache是更好的选择。
下面是官方文档的翻译:
NSCache
一个可变集合,用于临时存储在资源不足时容易被收回的临时键值对数据。
特点:
- 使用方便,类似字典
- 内存不足,NSCache会自动释放存储对象
1、当我们设置countLimit
2、手动调用remove
3、App进入后台之后
Tips:收到内存警告的时候, 不会释放哦 - 可以从不同的线程添加、删除和查询缓存中的项,而不必自己锁定缓存。(线程安全)
- 与NSMutableDictionary对象不同,NSCache的key不会被拷贝,不需要实现Copying协议。
通常使用NSCache对象来临时存储具有临时数据的对象,这些临时数据的创建成本很高。重用这些对象可以提供性能优势,因为它们的值不必重新计算。但是,这些对象对于应用程序来说并不重要,如果内存紧张,可以丢弃它们,如果被丢弃,则必须在需要时重新计算它们的值。
如果一个对象可以在不使用时丢弃,可以采用实现NSDiscardableContent协议来改进缓存回收行为。默认情况下,如果缓冲中的NSDiscardableContent对象的内容被丢弃,那么它们被自动删除,不过这个自动删除策略可以更改。如果将NSDiscardableContent对象放入缓存,则缓存在删除该对象时,调用discardContentlfPossible方法。
NSCache提供的属性和相关方法:
// 缓存的名称
@property (copy) NSString *name;
// NSCacheDelegate 代理
@property (nullable, assign) id<NSCacheDelegate> delegate;
// 通过key获取value,类似于字典中通过key取value的操作。
- (nullable ObjectType)objectForKey:(KeyType)key;
// 设置keyValue
- (void)setObject:(ObjectType)obj forKey:(KeyType)key; // 0 cost
// 设置key value ,cost表示 key 和 obj 这个关联键值对的成本
// cost 值用于计算包含缓存中所有对象的成本的总和。当内存有限或缓存的总成本超过允许的最大总成本时,缓存可以开始一个清除过程来删除它的一些元素。
// 但是这个清除过程并不是保证顺序的,所以一般情况下使用上面那个方法就行了。
- (void)setObject:(ObjectType)obj forKey:(KeyType)key cost:(NSUInteger)g;
- (void)removeObjectForKey:(KeyType)key;
- (void)removeAllObjects;
// 缓存在开始清除对象之前所能容纳的最大总成本。
// 如果将对象添加到缓存中,可以传入对象的指定成本,如果导致缓存的总成本高于totalCostLimit,则缓存可能会自动删除对象,不保证缓存清除对象的顺序。(淘汰策略)
@property NSUInteger totalCostLimit; // limits are imprecise/not strict
// 缓存能保存的对象的最大数量。
// 并不严格,如果缓存超过了这个限制,缓存中的对象可能会立即、稍后或永远被清除,取决于缓存的实现细节。
@property NSUInteger countLimit; // limits are imprecise/not strict
// 缓存是否会自动 清除内容已被丢弃 的 可丢弃内容对象 的标志。
// 如果是,缓存将在其内容被丢弃后清除可丢弃内容对象。如果没有,就不会。默认值是YES。
@property BOOL evictsObjectsWithDiscardedContent;
@end
@protocol NSCacheDelegate <NSObject>
@optional
//上述协议只有这一个方法,缓存中的一个对象即将被删除时的回调
- (void)cache:(NSCache *)cache willEvictObject:(id)obj;
@end
举个例子:
@interface LGCacheIOP : NSObject <NSCacheDelegate>
@end
@implementation LGCacheIOP
/// 将要被移除时会调用该方法
- (void)cache:(NSCache *)cache willEvictObject:(id)obj{
NSLog(@"cache willEvictObject cache:%@ obj:%@",cache,obj);
}
@end
@interface ViewController ()
@property (nonatomic, strong) NSCache *cache;
@property (nonatomic, strong) LGCacheIOP *cacheIOP;
@end
@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
_cache = [NSCache new];
_cacheIOP = [LGCacheIOP new];
_cache.countLimit = 5;
_cache.delegate = _cacheIOP;
[self addCacheObject];
[self getCacheObject];
}
- (void)getCacheObject{
for (int i = 0; i < 10; i++) {
NSLog(@"Cache object:%@, at index: %d",[_cache objectForKey:[NSString stringWithFormat:@"DD%d",i]],i);
}
}
- (void)addCacheObject{
for (int i = 0; i < 10; i++) {
[_cache setObject:[NSString stringWithFormat:@"Object%d",i] forKey:[NSString stringWithFormat:@"DD%d",i]];
}
}
@end
运行输出结果:
2019-09-22 17:05:37.339522+0800 DDPerson[15669:1415737] cache willEvictObject cache:<NSCache: 0x600003563540> obj:Object0
2019-09-22 17:05:37.339750+0800 DDPerson[15669:1415737] cache willEvictObject cache:<NSCache: 0x600003563540> obj:Object1
2019-09-22 17:05:37.339942+0800 DDPerson[15669:1415737] cache willEvictObject cache:<NSCache: 0x600003563540> obj:Object2
2019-09-22 17:05:37.340107+0800 DDPerson[15669:1415737] cache willEvictObject cache:<NSCache: 0x600003563540> obj:Object3
2019-09-22 17:05:37.340245+0800 DDPerson[15669:1415737] cache willEvictObject cache:<NSCache: 0x600003563540> obj:Object4
2019-09-22 17:05:37.340376+0800 DDPerson[15669:1415737] Cache object:(null), at index: 0
2019-09-22 17:05:37.340519+0800 DDPerson[15669:1415737] Cache object:(null), at index: 1
2019-09-22 17:05:37.340630+0800 DDPerson[15669:1415737] Cache object:(null), at index: 2
2019-09-22 17:05:37.340733+0800 DDPerson[15669:1415737] Cache object:(null), at index: 3
2019-09-22 17:05:37.340844+0800 DDPerson[15669:1415737] Cache object:(null), at index: 4
2019-09-22 17:05:37.340948+0800 DDPerson[15669:1415737] Cache object:Object5, at index: 5
2019-09-22 17:05:37.341051+0800 DDPerson[15669:1415737] Cache object:Object6, at index: 6
2019-09-22 17:05:37.341205+0800 DDPerson[15669:1415737] Cache object:Object7, at index: 7
2019-09-22 17:05:37.341480+0800 DDPerson[15669:1415737] Cache object:Object8, at index: 8
2019-09-22 17:05:37.346467+0800 DDPerson[15669:1415737] Cache object:Object9, at index: 9
我们可以看到当我们_cache.countLimit 设置为5的时候,添加第6-10个的时候,前面5个就会被移除了。
之后我们在把应用退出到后台,会发现,后面5个也会被移除了:
2019-09-22 17:08:43.518213+0800 DDPerson[15669:1415737] cache willEvictObject cache:<NSCache: 0x600003563540> obj:Object5
2019-09-22 17:08:43.518814+0800 DDPerson[15669:1415737] cache willEvictObject cache:<NSCache: 0x600003563540> obj:Object7
2019-09-22 17:08:43.519441+0800 DDPerson[15669:1415737] cache willEvictObject cache:<NSCache: 0x600003563540> obj:Object9
2019-09-22 17:08:43.520190+0800 DDPerson[15669:1415737] cache willEvictObject cache:<NSCache: 0x600003563540> obj:Object6
2019-09-22 17:08:43.520569+0800 DDPerson[15669:1415737] cache willEvictObject cache:<NSCache: 0x600003563540> obj:Object8
上文中,我们提到( 如果一个对象可以在不使用时丢弃,可以采用实现NSDiscardableContent协议来改进缓存回收行为。)并且NSCache中也有一个属性是 evictsObjectsWithDiscardedContent,那么我们可以稍微了解一下关于 NSDiscardableContent 这个协议的描述:
当一个有内容的类的对象可以在内容不使用时丢弃,可以实现此协议,从而使应用程序占用更小的内存,这样可以提高缓存的淘汰。
默认情况下,内存不足时,当前系统会把内存中的一部分缓存,置换到磁盘上,所以我们使用NSDiscardableContent这个协议,把数据标记成可清除的,而不用被置换的,当没有内容的时候,直接被清除就行了。
实现NSDiscardableContent的对象的生命周期依赖于一个"counter"变量。
实现NSDiscardableContent的对象是一个可清除的内存块,它会跟踪当前对象是否被其它对象使用。
- 当这个对象内存被读取或仍然需要的时候,它的counter将大于或等于1。
- 当它不被使用,并且可以被丢弃时,counter将= 0;
当counter等于0时,如果内存在那个时间点吃紧,就可以丢弃当前对象。为了丢弃内容,在对象上调用discardContentIfPossible,如果counter等于0,那么它将释放关联的内存。
Foundation框架包括了一个NSPurgeableData类,该类默认实现了这个协议。
下面我们看一下这个协议中的方法:
@protocol NSDiscardableContent
@required
- (BOOL)beginContentAccess; // counter+1
- (void)endContentAccess; // counter-1
- (void)discardContentIfPossible;
- (BOOL)isContentDiscarded;
@end
-
beginContentAccess;
返回一个布尔值,该值指示可丢弃的内容是否仍然可用并已成功访问。
如果需要或即将使用对象的内存,请调用这个方法。这个方法会递增计数器变量,从而保护对象的内存不被丢弃。
实现类可能会决定,如果内容已经被丢弃,则此方法将重新尝试创建它们;如果创建成功则返回YES。如果没有调用beginContentAccess方法,就使用NSDiscardableContent对象,则此协议的实现者应该会引发异常。 -
endContentAccess
如果不再访问可丢弃的内容,则调用。
该方法将对象的计数器变量递减,这通常会将计数器变量的值降低到0,从而允许在必要时丢弃对象的可丢弃内容。 -
discardContentIfPossible
如果被访问计数器的值为0,则调用此函数以丢弃改对象的内容。
如果访问的计数器的值为0,此方法只应丢弃对象的内容。否则,它应该什么都不做。 -
isContentDiscarded
返回一个布尔值,该值指示内容是否已被丢弃。
上面就是对于NSDiscardableContent 协议的介绍,Foundation框架中提供了一个默认实现该协议的类:
NSPurgeableData
// 一个可变的数据对象,其中包含可以在不再需要时丢弃的字节。
@interface NSPurgeableData : NSMutableData <NSDiscardableContent> {
@private
NSUInteger _length;
int32_t _accessCount;
uint8_t _private[32];
void *_reserved;
}
@end
可以单独使用这个对象,并不一定和NSCache结合使用。
比如我们生成了NSPurgeableData这样的一个实例,并且存入到了NSCache中,然后调用endContentAccess方法,将counter设置为0,当收到内存警告的时候,NSPurgeableData的实例对象,就会被清除了。
我们使用GNUStep来看下NSCache的实现:
@interface GS_GENERIC_CLASS(NSCache, KeyT, ValT) : NSObject
{
#if GS_EXPOSE(NSCache)
@private
/** The maximum total cost of all cache objects. */
NSUInteger _costLimit;
/** Total cost of currently-stored objects. */
NSUInteger _totalCost;
/** The maximum number of objects in the cache. */
NSUInteger _countLimit;
/** The delegate object, notified when objects are about to be evicted. */
id _delegate;
// 表示当前对象是否实现了NSDiscardedContent协议
BOOL _evictsObjectsWithDiscardedContent;
/** Name of this cache. */
NSString *_name;
// 使用NSMapTable存储缓存对象
NSMapTable *_objects;
/** LRU ordering of all potentially-evictable objects in this cache. */
GS_GENERIC_CLASS(NSMutableArray, ValT) *_accesses;
/** Total number of accesses to objects */
int64_t _totalAccesses;
#endif
#if GS_NONFRAGILE
#else
/* Pointer to private additional data used to avoid breaking ABI
* when we don't have the non-fragile ABI available.
* Use this mechanism rather than changing the instance variable
* layout (see Source/GSInternal.h for details).
*/
@private id _internal GS_UNUSED_IVAR;
#endif
}
我们直接到setObject:forKey:方法的实现:
- (void) setObject: (id)obj forKey: (id)key cost: (NSUInteger)num
{
// 先取出旧的值
_GSCachedObject *oldObject = [_objects objectForKey: key];
_GSCachedObject *newObject;
// 移除旧的
if (nil != oldObject)
{
[self removeObjectForKey: oldObject->key];
}
// 缓存的淘汰
[self _evictObjectsToMakeSpaceForObjectWithCost: num];
newObject = [_GSCachedObject new];
// Retained here, released when obj is dealloc'd
// 对于key 和 object 都是 RETAIN
newObject->object = RETAIN(obj);
newObject->key = RETAIN(key);
newObject->cost = num;
if ([obj conformsToProtocol: @protocol(NSDiscardableContent)])
{
newObject->isEvictable = YES;
[_accesses addObject: newObject];
}
// 设置到NSMapTable当中
[_objects setObject: newObject forKey: key];
RELEASE(newObject);
// 加上这个对象的内存消耗
_totalCost += num;
}
我们看到对象在缓存时是用的_GSCachedObject:
@interface _GSCachedObject : NSObject
{
@public
id object; // 当前对象
NSString *key; // 对应的key
int accessCount; // 当前对象的访问次数
NSUInteger cost; // 对象的消耗
BOOL isEvictable; // 当前对象是否能被移除
}
@end
num也就是当前对象所占用的内存的消耗,默认是0,下面这个方法里面,就是缓存的淘汰,我们可以看下这个方法是如何依赖num来实现具体的缓存策略:
LRU(Least recently used,最近最少使用)算法根据数据的历史访问记录来进行淘汰数据,其核心思想是“如果数据最近被访问过,那么将来被访问的几率也更高”。
[self _evictObjectsToMakeSpaceForObjectWithCost: num];
====> 具体实现
- (void)_evictObjectsToMakeSpaceForObjectWithCost: (NSUInteger)cost
{
NSUInteger spaceNeeded = 0;
NSUInteger count = [_objects count];
if (_costLimit > 0 && _totalCost + cost > _costLimit)
{
///////计算需要清除的空间 = 现有的总的 + 要添加的 - 总的限制
spaceNeeded = _totalCost + cost - _costLimit;
}
// Only evict if we need the space.
// 要清除那些访问比较少的对象
if (count > 0 && (spaceNeeded > 0 || count >= _countLimit))
{
NSMutableArray *evictedKeys = nil;
// Round up slightly. 平均的访问次数
// _totalAccesses 所有对象的访问次数的总和
// count 所有对象的数量
// 下面这个公式就是大概计算需要清除访问的平均数 *0.2 是拿到少的那一部分 + 1 是为了避免为0
NSUInteger averageAccesses = ((_totalAccesses / (double)count) * 0.2) + 1;
NSEnumerator *e = [_accesses objectEnumerator];
_GSCachedObject *obj;
if (_evictsObjectsWithDiscardedContent)
{
evictedKeys = [[NSMutableArray alloc] init];
}
while (nil != (obj = [e nextObject]))
{
// Don't evict frequently accessed objects.
// 当前对象的访问次数小于平均的访问次数 是否可被移除
if (obj->accessCount < averageAccesses && obj->isEvictable)
{
// 发送消息
[obj->object discardContentIfPossible];
if ([obj->object isContentDiscarded])
{
NSUInteger cost = obj->cost;
// Evicted objects have no cost.
obj->cost = 0;
// Don't try evicting this again in future; it's gone already.
obj->isEvictable = NO;
// Remove this object as well as its contents if required
if (_evictsObjectsWithDiscardedContent)
{
[evictedKeys addObject: obj->key];
}
_totalCost -= cost;
// If we've freed enough space, give up
if (cost > spaceNeeded)
{
break;
}
spaceNeeded -= cost;
}
}
}
// Evict all of the objects whose content we have discarded if required
if (_evictsObjectsWithDiscardedContent)
{
NSString *key;
e = [evictedKeys objectEnumerator];
while (nil != (key = [e nextObject]))
{
// 最后的remove
[self removeObjectForKey: key];
}
}
[evictedKeys release];
}
}