Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Date based cache purge #29

Merged
merged 12 commits into from Aug 22, 2013
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
12 changes: 11 additions & 1 deletion OGImage/OGImageCache.h
Expand Up @@ -19,6 +19,8 @@ typedef void (^OGImageCacheCompletionBlock)(__OGImage *image);

+ (NSString *)filePathForKey:(NSString *)key;

+ (NSURL *)fileURLForKey:(NSString *)key;

/**
* Check in-memory and on-disk caches for image corresponding to `key`. `block`
* called on main queue when check is complete. If `image` parameter is `nil`,
Expand All @@ -30,7 +32,9 @@ typedef void (^OGImageCacheCompletionBlock)(__OGImage *image);

/**
* Remove all cached images from in-memory and on-disk caches. If `wait` is `YES`
* this will block the calling thread until the purge is complete.
* this will block the calling thread until the purge is complete. In either case,
* this method manages its own `UIBackgroundTaskIdentifier` — it's safe to call it
* from `applicationDidEnterBackground`
*/
- (void)purgeCache:(BOOL)wait;

Expand All @@ -46,5 +50,11 @@ typedef void (^OGImageCacheCompletionBlock)(__OGImage *image);
*/
- (void)purgeMemoryCacheForKey:(NSString *)key andWait:(BOOL)wait;

/**
* Remove cached images from disk that haven't been accessed since `date`
* This method manages its own `UIBackgroundTaskIdentifier` — it's safe to call it
* from `applicationDidEnterBackground`
*/
- (void)purgeDiskCacheOfImagesLastAccessedBefore:(NSDate *)date;

@end
73 changes: 57 additions & 16 deletions OGImage/OGImageCache.m
Expand Up @@ -12,20 +12,21 @@

static OGImageCache *OGImageCacheShared;

NSString *OGImageCachePath() {
NSURL *OGImageCacheURL() {
// generate the cache path: <app>/Library/Application Support/<bundle identifier>/OGImageCache,
// creating the directories as needed
NSArray *array = NSSearchPathForDirectoriesInDomains(NSApplicationSupportDirectory, NSUserDomainMask, YES);
NSArray *array = [[NSFileManager defaultManager] URLsForDirectory:NSApplicationSupportDirectory inDomains:NSUserDomainMask];
if (nil == array || 0 == [array count]) {
return nil;
}
NSString *cachePath = [[array[0] stringByAppendingPathComponent:[[NSBundle mainBundle] bundleIdentifier]] stringByAppendingPathComponent:@"OGImageCache"];
[[NSFileManager defaultManager] createDirectoryAtPath:cachePath withIntermediateDirectories:YES attributes:nil error:nil];
return cachePath;
NSURL *cacheURL = [[array[0] URLByAppendingPathComponent:[[NSBundle mainBundle] bundleIdentifier]] URLByAppendingPathComponent:@"OGImageCache"];
[[NSFileManager defaultManager] createDirectoryAtURL:cacheURL withIntermediateDirectories:YES attributes:nil error:nil];
return cacheURL;
}

@implementation OGImageCache {
NSCache *_memoryCache;
dispatch_queue_t _cacheFileReadQueue;
dispatch_queue_t _cacheFileTasksQueue;
}

Expand All @@ -49,16 +50,30 @@ + (NSString *)MD5:(NSString *)string {
}

+ (NSString *)filePathForKey:(NSString *)key {
return [OGImageCachePath() stringByAppendingPathComponent:[OGImageCache MD5:key]];
return [[OGImageCache fileURLForKey:key] path];
}

+ (NSURL *)fileURLForKey:(NSString *)key {
return [OGImageCacheURL() URLByAppendingPathComponent:[OGImageCache MD5:key]];
}

- (id)init {
self = [super init];
if (self) {
_memoryCache = [[NSCache alloc] init];
[_memoryCache setName:@"com.origamilabs.OGImageCache"];
/*
* We use the 'queue-jumping' pattern outlined in WWDC 2011 Session 201: "Mastering Grand Central Dispatch"
* We place lower-priority tasks (writing, purging) on a serial queue that has its
* target queue set to our high-priority (read) queue. Whenever we submit a high-priority
* block, we suspend the lower-priority queue for the duration of the block.
*
* This way, writes and purges never cause cache reads to wait in the queue.
*/
_cacheFileReadQueue = dispatch_queue_create("com.origamilabs.OGImageCache.read", DISPATCH_QUEUE_SERIAL);
dispatch_set_target_queue(_cacheFileReadQueue, dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0));
_cacheFileTasksQueue = dispatch_queue_create("com.origamilabs.OGImageCache.filetasks", DISPATCH_QUEUE_SERIAL);
dispatch_set_target_queue(_cacheFileTasksQueue, dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0));
dispatch_set_target_queue(_cacheFileTasksQueue, _cacheFileReadQueue);
}
return self;
}
Expand All @@ -71,10 +86,11 @@ - (void)imageForKey:(NSString *)key block:(OGImageCacheCompletionBlock)block {
block(image);
return;
}
dispatch_async(_cacheFileTasksQueue, ^{
dispatch_suspend(_cacheFileTasksQueue);
dispatch_async(_cacheFileReadQueue, ^{
// Check to see if the image is cached locally
NSString *cachePath = [OGImageCache filePathForKey:(key)];
__OGImage *image = [[__OGImage alloc] initWithDataAtURL:[NSURL fileURLWithPath:cachePath]];
NSURL *cacheURL = [OGImageCache fileURLForKey:(key)];
__OGImage *image = [[__OGImage alloc] initWithDataAtURL:cacheURL];
// if we have the image in the on-disk cache, store it to the in-memory cache
if (nil != image) {
[_memoryCache setObject:image forKey:key];
Expand All @@ -83,6 +99,7 @@ - (void)imageForKey:(NSString *)key block:(OGImageCacheCompletionBlock)block {
dispatch_async(dispatch_get_main_queue(), ^{
block(image);
});
dispatch_resume(_cacheFileTasksQueue);
});
}

Expand All @@ -91,7 +108,7 @@ - (void)setImage:(__OGImage *)image forKey:(NSString *)key {
NSParameterAssert(nil != key);
[_memoryCache setObject:image forKey:key];
dispatch_async(_cacheFileTasksQueue, ^{
NSURL *fileURL = [NSURL fileURLWithPath:[OGImageCache filePathForKey:key]];
NSURL *fileURL = [OGImageCache fileURLForKey:key];
NSError *error;
if(![image writeToURL:fileURL error:&error]) {
NSLog(@"[OGImageCache ERROR] failed to write image with error %@ %s %d", error, __FILE__, __LINE__);
Expand All @@ -105,11 +122,15 @@ - (void)setImage:(__OGImage *)image forKey:(NSString *)key {

- (void)purgeCache:(BOOL)wait {
[_memoryCache removeAllObjects];
UIBackgroundTaskIdentifier taskId = UIBackgroundTaskInvalid;
taskId = [[UIApplication sharedApplication] beginBackgroundTaskWithExpirationHandler:^{
[[UIApplication sharedApplication] endBackgroundTask:taskId];
}];
void (^purgeFilesBlock)(void) = ^{
NSString *cachePath = OGImageCachePath();
for (NSString *file in [[NSFileManager defaultManager] enumeratorAtPath:cachePath]) {
[[NSFileManager defaultManager] removeItemAtPath:[cachePath stringByAppendingPathComponent:file] error:nil];
for (NSURL *url in [[NSFileManager defaultManager] enumeratorAtURL:OGImageCacheURL() includingPropertiesForKeys:nil options:0 errorHandler:nil]) {
[[NSFileManager defaultManager] removeItemAtURL:url error:nil];
}
[[UIApplication sharedApplication] endBackgroundTask:taskId];
};
if (YES == wait) {
dispatch_sync(_cacheFileTasksQueue, purgeFilesBlock);
Expand All @@ -123,10 +144,10 @@ - (void)purgeCacheForKey:(NSString *)key andWait:(BOOL)wait {

[self purgeMemoryCacheForKey:key andWait:wait];

NSString *cachedFilePath = [[self class] filePathForKey:key];
NSURL *cachedFileURL = [[self class] fileURLForKey:key];

void (^purgeFileBlock)(void) =^{
[[NSFileManager defaultManager] removeItemAtPath:cachedFilePath error:nil];
[[NSFileManager defaultManager] removeItemAtURL:cachedFileURL error:nil];
};

if (YES == wait) {
Expand All @@ -142,4 +163,24 @@ - (void)purgeMemoryCacheForKey:(NSString *)key andWait:(BOOL)wait {
[_memoryCache removeObjectForKey:key];
}

- (void)purgeDiskCacheOfImagesLastAccessedBefore:(NSDate *)date {
UIBackgroundTaskIdentifier taskId = UIBackgroundTaskInvalid;
taskId = [[UIApplication sharedApplication] beginBackgroundTaskWithExpirationHandler:^{
[[UIApplication sharedApplication] endBackgroundTask:taskId];
}];
dispatch_async(_cacheFileTasksQueue, ^{
NSURL *cacheURL = OGImageCacheURL();
for (NSURL *fileURL in [[NSFileManager defaultManager] enumeratorAtURL:cacheURL includingPropertiesForKeys:@[NSURLContentAccessDateKey] options:NSDirectoryEnumerationSkipsHiddenFiles errorHandler:nil]) {
NSDate *accessDate;
if (NO == [fileURL getResourceValue:&accessDate forKey:NSURLContentAccessDateKey error:nil]) {
return;
}
if (NSOrderedDescending == [date compare:accessDate]) {
[[NSFileManager defaultManager] removeItemAtURL:fileURL error:nil];
}
}
[[UIApplication sharedApplication] endBackgroundTask:taskId];
});
}

@end
12 changes: 5 additions & 7 deletions OGImageDemo/OGImageDemo/OGAppDelegate.m
Expand Up @@ -10,6 +10,7 @@
#import "OGViewController.h"
#import "DDLog.h"
#import "DDTTYLogger.h"
#import "OGImageCache.h"

@implementation OGAppDelegate

Expand All @@ -28,25 +29,22 @@ - (void)setupLogging {
}

- (void)applicationWillResignActive:(UIApplication *)application {
// Sent when the application is about to move from active to inactive state. This can occur for certain types of temporary interruptions (such as an incoming phone call or SMS message) or when the user quits the application and it begins the transition to the background state.
// Use this method to pause ongoing tasks, disable timers, and throttle down OpenGL ES frame rates. Games should use this method to pause the game.
}

- (void)applicationDidEnterBackground:(UIApplication *)application {
// Use this method to release shared resources, save user data, invalidate timers, and store enough application state information to restore your application to its current state in case it is terminated later.
// If your application supports background execution, this method is called instead of applicationWillTerminate: when the user quits.
// purge the disk cache of any image that hasn't been
// accessed more recently than 2 minutes ago. This is obviously pretty contrived;
NSDate *before = [NSDate dateWithTimeIntervalSinceNow:-120.];
[[OGImageCache shared] purgeDiskCacheOfImagesLastAccessedBefore:before];
}

- (void)applicationWillEnterForeground:(UIApplication *)application {
// Called as part of the transition from the background to the inactive state; here you can undo many of the changes made on entering the background.
}

- (void)applicationDidBecomeActive:(UIApplication *)application {
// Restart any tasks that were paused (or not yet started) while the application was inactive. If the application was previously in the background, optionally refresh the user interface.
}

- (void)applicationWillTerminate:(UIApplication *)application {
// Called when the application is about to terminate. Save data if appropriate. See also applicationDidEnterBackground:.
}

@end
19 changes: 9 additions & 10 deletions OGImageDemo/OGImageDemo/OGImageTableViewCell.m
Expand Up @@ -32,9 +32,11 @@ - (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(N
if ([keyPath isEqualToString:@"scaledImage"]) {
self.imageView.image = self.image.scaledImage;
self.textLabel.text = [[self.image.url path] lastPathComponent];
self.detailTextLabel.text = [NSString stringWithFormat:@"%.2f", self.image.loadTime];
} else if ([keyPath isEqualToString:@"error"]) {

self.detailTextLabel.textColor = [UIColor redColor];
self.detailTextLabel.text = [NSString stringWithFormat:NSLocalizedString(@"%@", @""), [self.image.error localizedDescription]];
self.imageView.image = self.image.scaledImage;
[self setNeedsLayout];
}
}
else {
Expand All @@ -45,23 +47,20 @@ - (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(N
#pragma mark - Properties

- (void)setImage:(OGScaledImage *)image {
self.detailTextLabel.text = @"";
/*
* When the cell's image is set, we want to first make sure we're no longer listening
* for any KVO notifications on the cell's previous image.
*/
[_image removeObserver:self forKeyPath:@"error" context:&KVOContext];
[_image removeObserver:self forKeyPath:@"scaledImage" context:&KVOContext];
[_image removeObserver:self context:&KVOContext];
_image = image;
self.imageView.image = _image.scaledImage;
[_image addObserver:self context:&KVOContext];
self.textLabel.text = [[self.image.url path] lastPathComponent];
self.detailTextLabel.text = NSLocalizedString(@"Loading", @"");
[_image addObserver:self forKeyPath:@"error" options:NSKeyValueObservingOptionNew context:&KVOContext];
[_image addObserver:self forKeyPath:@"scaledImage" options:NSKeyValueObservingOptionNew context:&KVOContext];
self.imageView.image = _image.scaledImage;
}

- (void)dealloc {
[_image removeObserver:self forKeyPath:@"error" context:&KVOContext];
[_image removeObserver:self forKeyPath:@"scaledImage" context:&KVOContext];
[_image removeObserver:self context:&KVOContext];
}

@end
2 changes: 1 addition & 1 deletion OGImageDemo/OGImageDemo/OGViewController.m
Expand Up @@ -8,6 +8,7 @@

#import "OGViewController.h"
#import "OGScaledImage.h"
#import "OGImageCache.h"
#import "OGImageTableViewCell.h"

@interface OGViewController ()
Expand All @@ -21,7 +22,6 @@ @implementation OGViewController {
- (void)viewDidLoad {
[super viewDidLoad];
[self loadJSON];
// Do any additional setup after loading the view, typically from a nib.
}

- (void)didReceiveMemoryWarning {
Expand Down