第50条:构建缓存时选用NSCache而非NSDictionary

  本条要点:(作者总结)

  •  实现缓存时应选用 NSCache 而非 NSDictionary 对象。因为 NSCache 可以提供优雅的自动删减功能,而且是“线程安全的”,此外,它与字典不同,并不会拷贝键。
  • 可以给 NSCache 对象设置上限,用以限制缓存中的对象总个数及“总成本”,而这些尺度则定义了缓存删减其中对象的时机。但是绝对不要把这些尺度当成可靠的“硬限制”(hard limit),它们仅对NSCache 起指导作用。
  • 将 NSPurgeableData 与 NSCache 搭配使用,可实现自动清除数据的功能,也就是说,当 NSPurgeableData 对象所占内存为系统所丢弃时,该对象自身也会从缓存中移除。
  • 如果缓存使用得当,那么应用程序的响应速度就能提高。只有那种 “重新计算起来很费事的”数据,才值得放入缓存,比如那些需要从网络获取或从磁盘读取的数据。

  开发 Mac OS X 或 iOS 应用程序时,经常会遇到一个问题,那就是从因特网下载的图片应如何来缓存。首先能想到的好办法就是把内存中的图片保存到字典里,这样的话,稍后使用时就无须再次下载了。有些程序员会不假思索,直接使用 NSDictionary 来做(准确来说,是使用其可变版本),因为这个类很常用。其实,NSCache 类更好,它是 Foundation 框架专为处理这种任务而设计的。

  NSCache 胜过 NSDictionary 之处在于,当系统资源将要耗尽时,它可以自动删减缓存。如果采用普通的字典,那么就要自己编写挂钩,在系统发出“低内存”(low memory)通知时手工删减缓存。而 NSCache 则会自动删减,由于其是 Foundation 框架的一部分,所以与开发者相比,它能在更深的层面上插入挂钩。此外,NSCache 还会先行删减 “最久未使用的”(lease recently used)对象。若想自己编写代码来为字典添加此功能,则会十分复杂。

  NSCache 并不会“拷贝”键,而是会 “保留”它。此行为用NSDictionary 也可以实现,然而需要编写相当复杂的代码。NSCache 对象不拷贝键的原因在于:很多时候,键都是由不支持拷贝操作的对象来充当的。因此,NSCache 不会拷贝键,所以说,在键不支持拷贝操作的情况下,该类用起来比字典更方便。另外,NSCache 是线程安全的。而 NSDictionary 则绝对不具备此优势,意思就是:在开发者自己不编写加锁代码的前提下,多个线程便可以同时访问 NSCache。对缓存来说,线程安全通常很重要,因为开发者可能要在某个线程中读取数据,此时如果发现缓存里找不到指定的键,那么就要下载该键所对应的数据了。而下载完数据之后所要执行的回调函数,有可能会放在背景线程中执行,这样的话,就等于是用另外一个线程来写入缓存了。

  开发者可以操控缓存删减其内容的时机。有两个与系统资源相关的尺度可供调整,其一是缓存中的对象总数,其二是所有对象的“总开销”(overall cost)。开发者在将对象加入缓存时,可为其指定“开销值”。当对象总数或总开销超过上限时,缓存就可能会删减其中的对象了,在可用的系统资源趋于紧张时,也会这么做。然而要注意,“可能”会删减某个对象,并不意味着“一定”会删减这个对象。删减对象时所遵照的顺序,由具体实现来定。这尤其说明:想通过调整“开销值” 来迫使缓存优先删减某对象,不是个好主意。

  向缓存中添加对象时,只有在能很快计算出“开销值”的情况下,才应该考虑采用这个尺度。若计算过程很复杂,那么照这种方式来使用缓存就达不到最佳效果了,因为每次向缓存中放入对象时,还要专门花时间来计算这个附加因素的值。而缓存的本意则是要增加应用程序响应用户操作的速度。比方说,如果计算“开销值”时必须访问磁盘才能确定文件大小,或是必须访问数据库才能决定具体取值,那就不太好了。然而,如果要加入缓存中的是NSData 对象,那么就不妨指定“开销值”了,可以把数据大小当作“开销值”来用。因为NSData 对象的数据大小是已知的,所以计算 “开销值”的过程只不过是读取一项属性。

  下面这段代码演示了缓存的用法:

  #import <Foundation/Foundation.h>

  // Network fetcher class

  typedef void (^EOCNetworkFetcherCompletionHandler)(NSData *data);

  @interface EOCNetworkFetcher : NSObject 

  - (id)initWithURL:(NSURL *)url;

  - (void)startWithCompletionHandler:(EOCNetworkFetcherCompletionHandler)handler;

  @end

  // Calss that uses the network fetcher and cache results

  @interface EOCClass : NSObject

  @end

  @implementation EOCClass {

    NSCache *_cache;

  }

  - (id)init {

    if ((self = [super init])) {

      _cache = [NSCache new];

      // Cache a maximum of 100 URLs

      _cache.countLimit = 100;

      /**

      * The Size in bytes of data is used as the cost,

      * so this sets a cost limit of 5MB.

      */

      _cache.totalCostLimit = 5 * 1024 * 1024;

    }

    return self;

  }

  - (void)downloadDataForURL:(NSURL *)url {

    NSData *cachedData = [_cache objectForKey:url];

    if (cachedData) {

      // Cache hit

      [self useData:cacheData];

     } else {

      // Cache miss

      EOCNetworkFetcher *fetcher = [[EOCNetworkFetcher alloc] initWithURL:url];

      [fetcher startWithCompletionHandler:^(NSData *data) {

        [_cache setObject:data forKey:url cost:data.length];

        [self useData:data];

      }];

     }

    }

  @end

  在本例中,下载数据所用的 URL,就是缓存的键。若缓存未命中(cache miss)(意思是指缓存中没有访问者所需的数据),则下载数据并将其放入缓存。而数据的“开销值”则设为其长度。创建 NSCache 时,将其中可缓存的总对象数目上限设为 100,将“总开销”上限设为 5MB,不过,由于“开销值”以“字节”为单位,所以要通过算式将 MB 换算成字节。

  还有个类叫做 NSPurgeableData,和 NSCache 搭配起来用,效果很好,此类是NSMutableData 的子类,而且实现了 NSDiscardableContent 协议。如果某个对象所占的内存能够根据需要随时丢弃,那么就可以实现该协议所定义的接口。这就是说,当系统资源紧张时,可以把保存 NSPurgeableData 对象的那块内存释放掉。NSDiscardableContent 协议里定义了名为 isContentDiscarded 的方法,可用来查询相关内存是否已释放。

  如果需要访问某个 NSPurgeableData 对象。可以调用其 beginContentAccess 方法,告诉它现在还不应丢弃自己所占据的内存。用完之后,调用 endContentAccess 方法,告诉它在必要时可以丢弃自己所占据的内存了。这些调用可以嵌套,所以说,它们就像递增与递减引用计数所用的方法那样。只有对象的 “引用计数”为0 时才可以丢弃。

  如果将 NSPurgeableData 对象加入NSCache,那么当该对象为系统所丢弃时,也会自动从缓存中移除。通过 NSCache 的 evictsObjectsWithDiscardedContent 属性,可以开启或关闭此功能。

  刚才那个例子可用 NSPurgeableData 改写如下:

  - (void)downloadDataForURL:(NSURL *)url {

    NSPurgeableData *cachedData = [_cache objectForKey:url];

    if (cachedData) {

      // Stop the data being purged

      [cacheData beginContentAccess];

      // Use the cached data

      [self useData:cachedData];

      // Mark that the data may be purged again

      [cacheData endContentAccess];

    } else {

      // Cache miss

      EOCNetworkFetcher *fetcher = [[EOCNetworkFetcher alloc] initWithURL:url];

      [fetcher startWithCompletionHandler:^(NSData *data) {

        NSPurgeableData *purgeableData = [NSPurgeableData dataWithData:data];

        [_cache setObject:purgeableData forKey:url cost: purgeableData.length];

        // Don't need to beginContentAccess as it begins

        // with access already marked

        // Use the retrieved data

        [self useData:data];

        // Mark that the data may be purged now

        [purgeableData endContentAccess];

       }];

    }    

  }

  注意,创建好 NSPurgeableData 对象之后,其 “purge 引用计数” 会多1,所以无须再调用 beginContentAccess 了,然而其后必须调用 endContentAccess,将多出来的这个 "1" 抵消掉。

  END  

posted @ 2017-09-02 12:18  鳄鱼不怕牙医不怕  阅读(441)  评论(0编辑  收藏  举报