iOS NSCache

3,101 阅读5分钟

原文地址

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))

关注公众号,阅读更多技术好文