Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Cache Expiration Implementation using NSCache

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?

like image 788
Boon Avatar asked Apr 12 '12 21:04

Boon


1 Answers

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
    }
}
like image 60
Aaron Brager Avatar answered Oct 14 '22 18:10

Aaron Brager