NSCache详解

2019-09-26  本文已影响0人  沉江小鱼

Tips:NSCache是Foundation框架提供的缓存类的实现,使用方式类似于可变字典。由于NSMutableDictionary的存在,很多人在实现缓存时都会使用可变字典,但是NSCache在实现缓存功能时比可变字典更方便,最重要的它是线程安全的,而NSmutableDictionary不是线程安全的,在多线程环境下使用NSCache是更好的选择。

下面是官方文档的翻译:

NSCache
一个可变集合,用于临时存储在资源不足时容易被收回的临时键值对数据。

特点:

通常使用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等于0时,如果内存在那个时间点吃紧,就可以丢弃当前对象。为了丢弃内容,在对象上调用discardContentIfPossible,如果counter等于0,那么它将释放关联的内存。

Foundation框架包括了一个NSPurgeableData类,该类默认实现了这个协议。

下面我们看一下这个协议中的方法:

@protocol NSDiscardableContent
@required
- (BOOL)beginContentAccess; // counter+1
- (void)endContentAccess; // counter-1
- (void)discardContentIfPossible;
- (BOOL)isContentDiscarded;
@end

上面就是对于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];
    }
}

上一篇下一篇

猜你喜欢

热点阅读