Cache Expiration Implementation using NSCache
caching, cocoa-touch
Solution
Should the cache automatically invalidate itself by using a fixed interval timer?
This would be a bad solution, because you might add something seconds before the timer fires. The expiry should be based on the specific item's age. (It would, of course, be possible to conditionally invalidate items using a timer; see the comments on this answer.)
Here's an example. I thought about subclassing `NSCache`, but decided it was simpler to use composition.
Interface
//
// ExpiringCache.h
//
// Created by Aaron Brager on 10/23/13.
#import <Foundation/Foundation.h>
@protocol ExpiringCacheItem <NSObject>
@property (nonatomic, strong) NSDate *expiringCacheItemDate;
@end
@interface ExpiringCache : NSObject
@property (nonatomic, strong) NSCache *cache;
@property (nonatomic, assign) NSTimeInterval expiryTimeInterval;
- (id)objectForKey:(id)key;
- (void)setObject:(NSObject <ExpiringCacheItem> *)obj forKey:(id)key;
@end
Implementation
//
// ExpiringCache.m
//
// Created by Aaron Brager on 10/23/13.
#import "ExpiringCache.h"
@implementation ExpiringCache
- (instancetype) init {
self = [super init];
if (self) {
self.cache = [[NSCache alloc] init];
self.expiryTimeInterval = 3600; // default 1 hour
}
return self;
}
- (id)objectForKey:(id)key {
@try {
NSObject <ExpiringCacheItem> *object = [self.cache objectForKey:key];
if (object) {
NSTimeInterval timeSinceCache = fabs([object.expiringCacheItemDate timeIntervalSinceNow]);
if (timeSinceCache > self.expiryTimeInterval) {
[self.cache removeObjectForKey:key];
return nil;
}
}
return object;
}
@catch (NSException *exception) {
return nil;
}
}
- (void)setObject:(NSObject <ExpiringCacheItem> *)obj forKey:(id)key {
obj.expiringCacheItemDate = [NSDate date];
[self.cache setObject:obj forKey:key];
}
@end
Notes
- Assumes you're using ARC.
- I didn't implement `setObject:forKey:cost:` since the NSCache documentation all but tells you not to use it.
- I use a @try/@catch block, since technically you could add an object to the cache that doesn't respond to `expiringCacheItemDate`. I thought about using `respondsToSelector:` for this, but you could add an object that doesn't respond to that too, since NSCache takes `id` and not `NSObject`.
Sample code
#import "ExpiringCache.h"
@property (nonatomic, strong) ExpiringCache *accountsCache;
- (void) doSomething {
if (!self.accountsCache) {
self.accountsCache = [[ExpiringCache alloc] init];
self.accountsCache.expiryTimeInterval = 7200; // 2 hours
}
// add an object to the cache
[self.accountsCache setObject:newObj forKey:@"some key"];
// get an object
NSObject *cachedObj = [self.accountsCache objectForKey:@"some key"];
if (!cachedObj) {
// create a new one, this one is expired or we've never gotten it
}
}
Problem
I am using NSCache to implement caching in my app. I want to add expiration to it so it will obtain new data after some time. What are the options and what's the best approach? Should I look at the timestamp when the cache is accessed and invalidate it then? Should the cache automatically invalidate itself by using a fixed interval timer?