缓存和可清除内存

对于那些正在处理着需要大量内存及计算时间的对象的应用程序开发者而言,缓存及可清除内存是至关重要的。同样对于那些因内存变满而不得不频繁与硬盘进行页交换操作从而导致性能下降的应用程序而言也是非常重要的。

缓存概述

缓存就是一组能够极大提升应用程序性能的对象或者数据的集合。

为什么使用缓存

开发者往往使用缓存去存储一些临时的计算代价高昂的却又被频繁访问的对象。重用这些对象可以带来性能提升。因为这些对象的值不用一次次的被重新计算。但是这些对象对于应用而言又不是必不可缺的,如果内存紧张的话,那么它们会被销毁,如果这些对象被销毁,那么当再次需要将它们加载至内存时,它们的值需要被重新计算出来。

使用缓存可能引发的问题

尽管使用缓存可以带来巨大的性能收益,但是同样因为使用缓存也会带来一些问题。最严重的就是,缓存会占用大量的内存空间。当缓存占用大量内存空间后,可能就不会留给应用程序以及其它对象足够的内存空间,计算机会疲于在物理内存和硬盘之间做页交换操作以释放足够的内存页,从而导致系统性能下降。

解决办法

Cocoa提供了一种 NSCache 对象来帮助你实现缓存。NSCache类与NSDictionary类非常相似,它们均存储键值对。然而NScache类是一个 反应式的缓存 ,也就是说当内存够用时,它就缓存所有传给它的数据。但是,当内存不足时,它会自动释放其中的一部分数据从而减少内存占用。最后,如果这些被释放的数据又需要被加载回来后,它们的值还需要重新计算。

NSCache提供了两个有用的 “限制”特性 :

  • 可以限制NSCache缓存对象的数量,可以采用函数setCountLimit。例如你设置该NSCache中缓存数量限制为10,你放入了11个对象,那么NSCache就会自动释放其中的某一个对象。
  • 可以限制NSCache缓存对象代价的总和,及总代价限制,可以采用函数setTotalCostLimit。你可以为每个对象设置一个代价值,如果添加对象的总代价值大于你设置的代价值上限,那么NSCache就会自动释放其中的某些对象使得NSCache中的总代价降至代价上限之下。这个释放过程不确定,也许是立即释放也会会过一会才释放,一切都依赖于Cache的实现机制。如果你的缓存中的对象均不需要计算代价,那么传值0进去即可,或者使用setObject:forKey函数,该函数不需要传代价值进去。

注意:缓存数量限制以及缓存总代价限制并不是严格要求执行的,也就是说当缓存打破了上述的某一个或者全部限制,缓存中的某些对象可能会被立即释放也许会过一段时间才释放,甚至有可能不会释放,这全部取决于缓存内部实现细节。

使用可清除内存

为了确保你的应用程序不会使用过多内存,Cocoa框架同样也提供了 NSPurgeableData 类帮助你解决此问题。NSPurgeableData类遵循 NSDiscardableContent 协议,任何实现该协议的对象类都允许在其他对象使用完毕该对象实例后释放内存。当你创建了很多可以随意处置的子控件对象时,你应该为这些对象实现该协议。此外,NSPurgeableData类没必要非得和NSCache类一起使用,你也可以单独使用它实现释放内存的特性。

使用可清除内存的优点

使用 purgeable memory ,你可以使得系统迅速从低内存状态恢复回来,从而提高系统的性能。被标记为可清除标签的的内存是不会被页出至硬盘中的,因为 分页(paging) 操作是相当耗时的,取而代之的是直接将该内存块数据销毁,如果后面还需要使用它,那么还需要重新计算该对象的相关数值。

在使用 purgeable memory 时需要注意一点,它所占用的那部分内存在访问之前是锁定状态的。这个锁定机制十分有必要,因为它能确保在不会因为其它自动回收机制释放了你正在访问的数据。同样的,该锁定机制也会保证虚拟内存系统没有销毁该数据。NSPurgebleData实现了一个非常简单的锁定机制来保证当它被访问时数据的安全性(数据不会在被访问时销毁)。

如何实现可清除内存

NSPurgeableData类使用起来非常简单,因为该类仅仅实现了NSDiscardableContent协议。对于NSDiscardableContent对象的生命周期而言,“计数器”是其核心。当该对象进行读操作时,计数器数值将会大于等于1.当它不再被使用可以被销毁时,计数器数值为0。

当计数器值为0时,如果系统内存紧张的话那么该内存块可能会被销毁。为了销毁该内存块数据,会调用 discardContentIfPossible函数,该函数会释放数据所在的相关计数器数值为0的内存区域。

默认情况下,当一个NSPurgeableData对象初始化的时候,它的引用计数变量值为1,并且可以被安全的访问。为了访问这个purgeable memory,调用 beginContentAccess方法即可。这个方法首先会检查这个对象的数据是否被销毁。如果这个数据仍然在,它将会增加这个对象指向的内存的引用计数,并且返回YES。如果这个对象的数据已经被销毁了,这个方法就会返回NO。当我们完成对这个对象的访问后,调用endContentAccess方法即可,这个方法会减少这块内存区域的引用计数,并允许系统在内存紧张时释放它。只有当 beginContentAccess方法返回YES时,我们才可以去访问这个对象所指向的内存空间。

当系统可用内存减少时,系统或客户端对象通过调用discardContentIfPossible方法来销毁purgeable数据,当对象所指向的内存引用计数为0时,这个方法仅仅会销毁上面的数据,然后不再做其它任何操作。如果这个内存被销毁了,那么 isContentDiscarded方法会返回YES。下面是一个使用NSPurgeableData对象的例子:

  1. NSPurgeableData * data = [[NSPurgeableData alloc] init];
  2. [data endContentAccess]; //Don't necessarily need data right now, so mark as
  3. discardable.
  4. //Maybe put data into a cache if you anticipate you will need it later.
  5. ...
  6. if([data beginContentAccess])
  7. { //YES if data has not been discarded and counter variable has been incremented
  8. ...Some operation on the data...
  9. [data endContentAccess] //done with the data, so mark as discardable
  10. }
  11. else
  12. {
  13. Caching and Purgeable Memory Using Purgeable Memory
  14. //data has been discarded, so recreate data and complete operation
  15. data = ...
  16. [data endContentAccess]; //done with data
  17. }
  18. //data is able to be discarded at this point if memory is tight  

Purgeable Memory 和 NSCache

当一个实现了 NSDiscardableContent协议的对象放在NSCache对象中去时,cache对象就维持了一个对该对象的强引用指针。如果这个对象的内容已经被销毁(discard)了,这个缓存的evictsObjectsWith DiscardedContent的值将会被设置为YES,然后这个对象会自动从缓存中移除。  

关于使用Purgeable Memory的一些警告

值得注意的是Purgeable memory是针对那些大对象或大内存块才可以直接使用的内存。Purgeable API也是作用于多页大小虚拟内存对象,这就使得我们很难把一个很小的缓存元素标记为purgeable。cache API会做一些必要的记录使得较小的缓存元素也可以使用purgeable memory。同样的,有些情况下,非要通过cache API为某些缓存元素分配内存空间也是不合适的,当我们可以十分方便的创建一个对象,或这个对象已经在不同层(layer)分配内存控件了,并且这个层(layer)已经对它做了缓存。在这些情况下,就没必要使用purgeable memory。

什么时候使用Purgeable Memory

当清除对象的开销小于分页(paging)的开销时,我们可以考虑采用Purgeable Memory,即分页(paging)的开销远大于再次使用这个对象时重新计算相关数据值的性能开销。很多时候缓存没没有遵循上面的规律,它们的一些缓存项很有可能以后都不会再次使用。同样的,那些能够轻松计算出数据的缓存项可以考虑使用purgeable memory,因为重新计算这些数据并不会耗费应用程序多少性能。