NSCache 是苹果官方提供的缓存类,它用于临时存储键对儿,使用和 NSMutableDictionary 类似。其定义如下:
@interface NSCache<__covariant KeyType, __covariant ObjectType> : NSObject
NSCache类结合了各种自动释放策略,确保缓存不会占用系统太多内存如果其他应用程序需要内存,这些策略将从缓存中删除一些项,从而将其内存占用降到最低。
NSCache 是线程安全的,你可以从不同线程添加,移除和查询内容,而不用加锁。
与 NSMutableDictionary 不同,NSCache 不会 copy 放入其中的 key 对象,而只是对 key 对象进行 Strong 引用,因此 key 不需要实现 NSCoping 协议。
通常使用 NSCache 来临时存储数据对象,这些对象的创建成本很高。重用这些对象可以提升性能。但是这些对象对应用程序来说不是必须的,因为这些对象在内存不足时可能会被丢弃。如果被丢弃,则在需要时要重新获取它的值。
NSCache 的使用
NSCache 的 API
NSCache 的 API 如下:
@property (copy) NSString *name;
@property (nullable, assign) id<NSCacheDelegate> delegate;
- (nullable ObjectType)objectForKey:(KeyType)key;
- (void)setObject:(ObjectType)obj forKey:(KeyType)key; // 0 cost
- (void)setObject:(ObjectType)obj forKey:(KeyType)key cost:(NSUInteger)g;
- (void)removeObjectForKey:(KeyType)key;
- (void)removeAllObjects;
@property NSUInteger totalCostLimit; // limits are imprecise/not strict
@property NSUInteger countLimit; // limits are imprecise/not strict
@property BOOL evictsObjectsWithDiscardedContent;
属性
- name:缓存的名字。
- countLimit:能够缓存对象的最大数量。默认是0,没有限制。
- totalCostLimit:缓存的大小。当超过这个限制时,NSCa车会进行内存修剪工作。默认值为0,表示没有限制。
- evictsObjectsWithDiscardedContent:标识缓存是否回收废弃的内容。
方法
- 获取与指定键名关联的值。
(nullable ObjectType)objectForKey:(KeyType)key;
- 设置键值对儿,0成本。
- (void)setObject:(ObjectType)obj forKey:(KeyType)key;
- 设置键值对儿,并指定该键值对儿成本。
- (void)setObject:(ObjectType)obj forKey:(KeyType)keycost:(NSUInteger)g;
- 删除缓存中指定键名的对象。
- (void)removeObjectForKey:(KeyType)key;
- 删除缓存中所有对象。
- (void)removeAllObjects;
NSCacheDelegate
@protocol NSCacheDelegate <NSObject>
@optional
- (void)cache:(NSCache *)cache willEvictObject:(id)obj;
@end
当对象要从缓存中移除时,会调用此回调。
例子
- (void)viewDidLoad {
[super viewDidLoad];
// Do any additional setup after loading the view.
_cache = [[NSCache alloc] init];
_cache.countLimit = 5;
_cache.delegate = self;
for (int i = 0; i < 10; i++) {
[_cache setObject:[NSString stringWithFormat:@"value_%d",i] forKey:[NSString stringWithFormat:@"key_%d",i]];
}
}
- (void)cache:(NSCache *)cache willEvictObject:(id)obj {
NSLog(@"obj:%@ 即将被销毁",obj);
}
log 日志如下:
2019-11-06 00:06:12.278219+0800 CacheDemo[2026:46091] obj:value_0 即将被销毁
2019-11-06 00:06:12.278317+0800 CacheDemo[2026:46091] obj:value_1 即将被销毁
2019-11-06 00:06:12.278392+0800 CacheDemo[2026:46091] obj:value_2 即将被销毁
2019-11-06 00:06:12.278469+0800 CacheDemo[2026:46091] obj:value_3 即将被销毁
2019-11-06 00:06:12.278545+0800 CacheDemo[2026:46091] obj:value_4 即将被销毁
这里把 countLimit 缓存数量设置为 5 时 , 后续继续添加缓存时 , NSCache 对象会释放之前存储的内容 , 然后在设置新的内容。
那么在 APP 收到内存警告时,会释放其中的内容吗?
我们改写一下上面的代码:
- (void)viewDidLoad {
[super viewDidLoad];
// Do any additional setup after loading the view.
_cache = [[NSCache alloc] init];
_cache.countLimit = 5;
_cache.delegate = self;
for (int i = 0; i < 5; i++) {
[_cache setObject:[NSString stringWithFormat:@"value_%d",i] forKey:[NSString stringWithFormat:@"key_%d",i]];
}
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(didReceiveMemoryWaring:) name:UIApplicationDidReceiveMemoryWarningNotification object:nil];
}
- (void)didReceiveMemoryWaring:(NSNotification *)notify {
NSLog(@"notification:%@", notify);
}
- (void)cache:(NSCache *)cache willEvictObject:(id)obj {
NSLog(@"obj:%@ 即将被销毁",obj);
}
然后通过模拟器 Debug 选项来模拟内存警告,其 log 如下:
2019-11-06 00:14:28.789151+0800 CacheDemo[2248:52737] notification:NSConcreteNotification 0x600001db4d20 {name = UIApplicationDidReceiveMemoryWarningNotification; object = <UIApplication: 0x7fc22ba00180>}
2019-11-06 00:14:28.791933+0800 CacheDemo[2248:52888] obj:value_0 即将被销毁
2019-11-06 00:14:28.792180+0800 CacheDemo[2248:52888] obj:value_1 即将被销毁
2019-11-06 00:14:28.792305+0800 CacheDemo[2248:52888] obj:value_2 即将被销毁
NSCache 的实现原理
首先 NSCache 是会持有一个 NSMutableDictionary。
- (id) init
{
if (nil == (self = [super init]))
{
return nil;
}
_objects = [NSMutableDictionary new];
_accesses = [NSMutableArray new];
return self;
}
然后设计一个 Cached 对象结构来保存一些额外的信息
@interface _GSCachedObject : NSObject {
@public
id object; //cache 的值
NSString *key; //设置 cache 的 key
int accessCount; //保存访问次数,用于自动清理
NSUInteger cost; //setObject:forKey:cost:
BOOL isEvictable; //线程安全
}
@end
在 Cache 读取的时候会对 _accesses 数组的添加、删除操作,通过 isEvictable 布尔值来保证线程安全操作。使用 Cached 对象里的 accessCount 属性进行 +1 操作为后面自动清理的条件判断做准备。实现如下:
- (id) objectForKey: (id)key {
_GSCachedObject *obj = [_objects objectForKey: key];
if (nil == obj) {
return nil;
}
if (obj->isEvictable) //保证添加删除操作线程安全
{
// 将 obj 移到 access list 末端
[_accesses removeObjectIdenticalTo: obj];
[_accesses addObject: obj];
}
obj->accessCount++;
_totalAccesses++;
return obj->object;
}
在每次 Cache 添加时会先去检查是否自动清理,会创建一个 Cached 对象将 key,object,cost 等信息记录下添加到 _accesses 数组和 _objects 字典里。
- (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
newObject->object = RETAIN(obj);
newObject->key = RETAIN(key);
newObject->cost = num;
if ([obj conformsToProtocol: @protocol(NSDiscardableContent)]) {
newObject->isEvictable = YES;
[_accesses addObject: newObject];
}
[_objects setObject: newObject forKey: key];
RELEASE(newObject);
_totalCost += num;
}
自动清理内存是如何实现的呢?
既然是自动清理必定需要有触发时机和进入清理的条件判断,触发时机一个是发生在添加 Cache 内容时,一个是发生在内存警告时。条件判断代码如下:
// cost 在添加新 cache 值时指定的 cost
// _costLimit 是 totalCostLimit 属性值
if (_costLimit > 0 && _totalCost + cost > _costLimit) {
spaceNeeded = _totalCost + cost - _costLimit;
}
// 只有当 cost 大于人工限制时才会清理
// 或者 cost 设置为0不进行人工干预
if (count > 0 && (spaceNeeded > 0 || count >= _countLimit))
关注公众号,阅读更多技术好文