美文网首页
NSCache详解

NSCache详解

作者: 沉江小鱼 | 来源:发表于2019-09-26 10:00 被阅读0次

    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];
        }
    }
    
    

    相关文章

      网友评论

          本文标题:NSCache详解

          本文链接:https://www.haomeiwen.com/subject/bapuyctx.html