Cache Expiration Implementation using NSCache

2019-04-05 14:37发布

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?

2条回答
Animai°情兽
2楼-- · 2019-04-05 15:00

Another solution would be to set an expire time when setting an object and compare against the expire time for an object.

For example:

Usage

#import "PTCache.h"

NSInteger const PROFILE_CACHE_EXPIRE = 3600;

- (void) cacheSomething: (id) obj
                 forKey: (NSString*) key
{
     [PTCache sharedInstance] setObject: obj
                                 forKey: key
                                 expire: PROFILE_CACHE_EXPIRE
     ];

}

Interface

#import <Foundation/Foundation.h>

@interface PTCache : NSCache

+ (PTCache *) sharedInstance;

- (void) setObject: (id) obj
            forKey: (NSString *) key
            expire: (NSInteger) seconds;

- (id) objectForKey: (NSString *) key;

@end

Implementation

#import "PTCache.h"

@implementation PTCache
{
    NSMutableArray * expireKeys;
}


+ (PTCache *) sharedInstance
{
    static dispatch_once_t predicate = 0;
    __strong static id sharedObject = nil;
    dispatch_once(&predicate, ^{
        sharedObject = [[self alloc] init];
    });

    return sharedObject;
}

- (id) init
{
    if ( self = [super init])
    {
        expireKeys = [[NSMutableArray alloc] init];
    }

    return self;
}

/**
 * Get Object
 *
 * @param NSString * key
 * @return id obj
 *
 **/

- (id) objectForKey: (NSString *) key
{
    id obj = [super objectForKey: key];

    if( obj == nil)
    {
        return nil;
    }

    BOOL expired = [self hasExpired: key];

    if( expired)
    {
        [super removeObjectForKey: key];
        return nil;
    }

    return obj;
}

/**
 * Set Object
 *
 * @param id obj
 * @param NSString * key
 * @param NSInteger seconds
 *
 */
- (void) setObject: (id) obj
            forKey: (NSString *) key
            expire: (NSInteger) seconds
{
    [super setObject: obj forKey: key];

    [self updateExpireKey: key expire: seconds];
}


/**
 * Update Expire Time for Key and Seconds to Expire
 *
 * @param NSString * key
 * @param NSInteger seconds
 *
 **/
- (void) updateExpireKey: (NSString *) key
                  expire: (NSInteger) seconds
    __block NSInteger index = -1;

    [expireKeys enumerateObjectsUsingBlock: ^(id obj, NSUInteger idx, BOOL *stop) {
        if([obj[@"key"] isEqualToString: key])
        {
            index = idx;
            *stop = YES;
            return;
        }
    }];

    NSNumber * expires = [NSNumber numberWithFloat: ([[NSDate date] timeIntervalSince1970] + seconds)];

    if( index > -1)
    {
        [[expireKeys objectAtIndex: index] setObject: expires forKey: key];
    }
    else
    {
        NSMutableDictionary * element = [[NSMutableDictionary alloc] init];
        [element setObject: key forKey: @"key"];
        [element setObject: expires forKey: @"expire"];

        [expireKeys addObject: element];
    }

}

/**
 * Has Expired for Key
 *
 **/
- (BOOL) hasExpired: (NSString *) key
{
    NSNumber * expiredObj = [self getExpireTime: key];

    NSDate * current = [NSDate date];

    NSDate * expireDate = [NSDate dateWithTimeIntervalSince1970: [expiredObj doubleValue]];

    return [current compare: expireDate] == NSOrderedDescending;
}

/**
 * Get Expire Time
 *
 * @param NSString * key
 * @param NSInteger
 *
 **/

- (NSNumber *) getExpireTime: (NSString *) key
{
    __block NSNumber * expire = nil;

    [expireKeys enumerateObjectsUsingBlock: ^(id obj, NSUInteger idx, BOOL *stop) {
        if([obj[@"key"] isEqualToString: key])
        {
            expire = obj[@"expire"];
            *stop = YES;
            return;
        }
    }];

    return expire;
}



@end
查看更多
forever°为你锁心
3楼-- · 2019-04-05 15:06

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
    }
}
查看更多
登录 后发表回答