您的位置:首页 > 移动开发 > Objective-C

Effective Objective-C 2.0:Item 50: Use NSCache Instead of NSDictionary for Caches

2013-12-15 18:38 549 查看


Item 50: Use NSCache Instead of NSDictionary for Caches

A common problem encountered when developing a Mac OS X or an iOS application that downloads images from the Internet is deciding what to do about caching them. A good first approach is to
use a dictionary to store in memory images that have been downloaded, such that they don’t need to be downloaded again if they are requested later. A naïve developer will simply use an
NSDictionary
(or
rather a mutable one) because that’s a commonly used class. However, an even better class, called
NSCache
, is also part of the Foundation framework and has been designed
exactly for this task.

The benefit of
NSCache
over
an
NSDictionary
is
that as system memory becomes full, the cache is automatically pruned. When using a dictionary, you often end up having to write pruning code yourself by hooking into system notifications for low memory. However,
NSCache
offers
this automatically; because it is part of the Foundation framework, it will be able to hook in deeper to the system than you could yourself. An
NSCache
will also prune
the least recently used objects first. Writing the code to support this yourself with a dictionary would be quite complex.

Also, an
NSCache
does
not copy keys but rather retains them. This is something that can be controlled on
NSDictionary
but requires more complex code (see Item
49). A cache usually would rather not copy the keys because often, the key will be an object that does not support copying. Since
NSCache
doesn’t
copy by default, it makes it an easier class to work with in these situations. Also,
NSCache
is thread safe. This is certainly not true of an
NSDictionary
, which
means that you can poke away at an
NSCache
from multiple threads at the same time without having to introduce any locks of your own. This is usually useful for a cache
because you may want to read from it in one thread, and, if a certain key doesn’t exist, you may download the data for that key. The callbacks for downloading may be in a background thread, so you end up adding to the cache in this other thread.

You can control when a cache will prune its contents. Two user-controllable metrics alongside the system resources are a limit on both
the number of objects in the cache and the overall “cost” of the objects. Each object can optionally be given a cost when added to the cache. When the total number of objects exceeds the count limit or the total cost exceeds the cost limit, the cache may evict
objects, just as it does when the available system memory becomes tight. However, it is important to note that it may evict rather than it will evict. The order in which objects are evicted is implementation
specific. In particular, this means that manipulating the cost metric in order to force eviction in a certain order is a bad idea.

The cost metric should be used only when adding an object to the cache if calculating the cost is very cheap. If calculating it is expensive, you may find that using the cache becomes suboptimal,
since you are having to calculate this additional factor each time an object is cached. After all, caches are meant to help with making an application more responsive. For example, having to go to the disk to find the size of a file or to a database to determine
the cost would be bad ideas. However, an example of a good cost to use is if
NSData
objects are added to the cache; in that case, you can use the size of that data as the
cost. This is already known to the
NSData
object, and so calculating it is as simple as reading a property.

The following is an example of using a cache:

Click here to view code image

#import <Foundation/Foundation.h>

// Network fetcher class

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

@interface EOCNetworkFetcher : NSObject

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

- (void)startWithCompletionHandler:

(EOCNetworkFetcherCompletionHandler)handler;

@end

// Class that uses the network fetcher and caches 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:cachedData];

} 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

In this example, the URL to be retrieved is used as the cache key. When there is a cache miss, the data is downloaded and added to the cache. The cost is calculated as the data’s length. When
the cache is created, the total number of objects that can be cached is set to 100, and the overall cost is set to a value that equates to 5MB because the unit of cost is the size in bytes.

Another class that can be used effectively alongside
NSCache
is called
NSPurgeableData
,
an
NSMutableData
subclass that implements a protocol called
NSDiscardableContent
.
This protocol defines an interface for objects whose memory can be discarded, if required. This means that the memory backing
NSPurgeableData
is freed when system resources
are getting low. The method called
isContentDiscarded
, part of the
NSDiscardableContent
protocol, returns whether the memory has been freed.

If a purgeable data object needs to be accessed, you call
beginContentAccess
to
tell it that it should not be discarded now. When you are done with it, you call
endContentAccess
to tell it that it is free to be discarded, if desired. These calls can be
nested, so you can think of them as just like a reference count being incremented and decremented. Only when the reference count is zero can the object be discarded.

If
NSPurgeableData
objects are added to an
NSCache
, a purgeable data object that is purged
is automatically removed from the cache. This can optionally be turned on or off through the cache’s
evictsObjectsWithDiscardedContent
property.

The preceding example could therefore be changed to make use of purgeable data, like so:

Click here to view code image

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

}];

}

}

Note that when a purgeable data object is created, it is returned with a +1 purge reference count, so you do not need to specifically call
beginContentAccess
on
it, but you must balance the +1 with a call to
endContentAccess
.


Things to Remember


Consider using
NSCache
in
the place of
NSDictionary
objects being used as caches. Caches provide optimal pruning behavior, thread safety, and don’t copy keys, unlike a dictionary.


Use the
count limit and cost limits to manipulate the metrics that define when objects are pruned from the cache. But never rely on those metrics to be hard limits; they are purely guidance for the cache.


Use
NSPurgeableData
objects
with a cache to provide autopurging data that is also automatically removed from the cache when purged.


Caches will
make your applications more responsive if used correctly. Cache only data that is expensive to recalculate, such as data that needs to be fetched from the network or read from disk.
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: 
相关文章推荐