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

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 NSCachewill 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 theNSData object, and so calculating it is as simple as reading a property.

The following is an example of using a cache:

#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 calledNSPurgeableData, 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 theNSDiscardableContent protocol, returns whether the memory has been freed.

If a purgeable data object needs to be accessed, you callbeginContentAccess 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’sevictsObjectsWithDiscardedContent property.

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

- (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 allocinitWithURL: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 callbeginContentAccess on it, but you must balance the +1 with a call toendContentAccess.

Things to Remember

Image 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.

Image 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.

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

Image 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.

展开阅读全文

没有更多推荐了,返回首页