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 touse 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
NSCacheover
an
NSDictionaryis
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,
NSCacheoffers
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
NSCachedoes
not copy keys but rather retains them. This is something that can be controlled on
NSDictionarybut 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
NSCachedoesn’t
copy by default, it makes it an easier class to work with in these situations. Also,
NSCacheis thread safe. This is certainly not true of an
NSDictionary, which
means that you can poke away at an
NSCachefrom 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
NSDataobjects 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
NSDataobject, 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
NSCacheis called
NSPurgeableData,
an
NSMutableDatasubclass 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
NSPurgeableDatais freed when system resources
are getting low. The method called
isContentDiscarded, part of the
NSDiscardableContentprotocol, returns whether the memory has been freed.
If a purgeable data object needs to be accessed, you call
beginContentAccessto
tell it that it should not be discarded now. When you are done with it, you call
endContentAccessto 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
NSPurgeableDataobjects 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
evictsObjectsWithDiscardedContentproperty.
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
beginContentAccesson
it, but you must balance the +1 with a call to
endContentAccess.
Things to Remember
Consider using
NSCachein
the place of
NSDictionaryobjects 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
NSPurgeableDataobjects
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.
相关文章推荐
- 5 Reasons to Use Protocol Buffers Instead of JSON For Your Next Service
- Effective Objective-C 2.0: Item 45: Use dispatch_once for Thread-Safe Single-Time Code Execution
- 使用GetItemById出错“Detected use of SPRequest for previously closed SPWeb object.”
- #("The POSIX name for this item is deprecated. Instead, use the ISO C++ conformant name:")
- Item 1:Always Use Properties Instead of Accessible Date Members
- Item 1: 尽可能的使用属性代替可访问的数据成员(Always Use Properties Instead of Accessible Data Members)
- Issue 71 - pymssql - Undefined symbols on Mac, CentOS, Redhat with pre-compiled build - A fast MS SQL Server client library for Python directly using C API instead of ODBC. It is Python DB-API 2.0 compliant. Works on Linux, *BSD, Solaris, Mac OS X and Win
- Item 1: Use Properties Instead of Accessible Data Members(Effective C#)
- Use parameters instead of string concatenation for forming SQL queries,用参数方式来生成sql语句,而不是用连接字符串的方式
- error C4996 The POSIX name for this item is deprecated. Instead, use the ISO C and C++ conformant name
- Item 4:Use Conditional Attributes Instead of #if
- Item 4: Use Conditional Attributes Instead of #if
- 编译trimesh2遇到问题:The POSIX name for this item is deprecated. Instead, use the ISO C++ conformant names
- Item 1: Use Properties Instead of Accessible Data Members
- error C4996: 'getpid': The POSIX name for this item is deprecated. Instead, use the ISO C++ conforma
- Use sp_MSForEachDB instead of your own loop
- Effective Objective-C 2.0: Item 44: Use Dispatch Groups to Take Advantage of Platform Scaling
- Item 4: Use Conditional Attributes Instead of #if(Effective C#)
- use instead of trigger for blocking delete
- #("The POSIX name for this item is deprecated. Instead, use the ISO C++ conformant name:")