From c43de6d4bd283f4cf2d561be809f392adbeeb09b Mon Sep 17 00:00:00 2001 From: Daryll Herberger <daryllh@vertigo.com> Date: Wed, 16 Jul 2014 14:17:46 -0700 Subject: [PATCH 1/4] Adding selectors to allow disk-only cache insertion; this is handy for pre-caching objects from network responses that may not be immediately used --- SAMCache/SAMCache.h | 26 ++++++++++++++++++++++++-- SAMCache/SAMCache.m | 38 ++++++++++++++++++++++++-------------- 2 files changed, 48 insertions(+), 16 deletions(-) diff --git a/SAMCache/SAMCache.h b/SAMCache/SAMCache.h index 5221b9e..40e5903 100755 --- a/SAMCache/SAMCache.h +++ b/SAMCache/SAMCache.h @@ -105,7 +105,7 @@ ///---------------------------------------- /** - Synchronously set an object in the cache for a given key. + Synchronously set an object in the cache for a given key. Uses both memory and disk cache. @param object The object to store in the cache. @@ -113,6 +113,17 @@ */ - (void)setObject:(id <NSCoding>)object forKey:(NSString *)key; +/** + Synchronously set an object in the cache for a given key. + + @param object The object to store in the cache. + + @param key The key of the object. + + @param useDiskCacheOnly A value indicating whether or not to store the object in memory or only write object to disk cache location. + */ +- (void)setObject:(id <NSCoding>)object forKey:(NSString *)key diskCacheOnly:(BOOL)useDiskCacheOnly; + /** Remove an object from the cache. @@ -203,7 +214,7 @@ - (void)imageForKey:(NSString *)key usingBlock:(void (^)(UIImage *image))block; /** - Synchronously store a PNG representation of an image in the cache for a given key. + Synchronously store a PNG representation of an image in the cache for a given key. Uses both memory and disk cache. @param image The image to store in the cache. @@ -211,6 +222,17 @@ */ - (void)setImage:(UIImage *)image forKey:(NSString *)key; +/** + Synchronously store a PNG representation of an image in the cache for a given key. + + @param image The image to store in the cache. + + @param key The key of the image. + + @param useDiskCacheOnly A value indicating whether or not to store the object in memory or only write object to disk cache location. + */ +- (void)setImage:(UIImage *)image forKey:(NSString *)key diskCacheOnly:(BOOL)useDiskCacheOnly; + /** Synchronously check if an image exists in the cache without retriving it. diff --git a/SAMCache/SAMCache.m b/SAMCache/SAMCache.m index 0c54b2e..4f5ef48 100755 --- a/SAMCache/SAMCache.m +++ b/SAMCache/SAMCache.m @@ -166,24 +166,29 @@ - (BOOL)objectExistsForKey:(NSString *)key { #pragma mark - Adding and Removing Cached Values - (void)setObject:(id <NSCoding>)object forKey:(NSString *)key { - NSParameterAssert(key); + [self setObject:object forKey:key diskCacheOnly:NO]; +} +- (void)setObject:(id <NSCoding>)object forKey:(NSString *)key diskCacheOnly:(BOOL)useDiskCacheOnly { + NSParameterAssert(key); + // If there's no object, delete the key. if (!object) { [self removeObjectForKey:key]; return; } - - // Save to memory cache - [self.cache setObject:object forKey:key]; - + + if (useDiskCacheOnly == NO) { + // Save to memory cache + [self.cache setObject:object forKey:key]; + } + dispatch_async(self.diskQueue, ^{ // Save to disk cache [NSKeyedArchiver archiveRootObject:object toFile:[self _pathForKey:key]]; }); } - - (void)removeObjectForKey:(NSString *)key { NSParameterAssert(key); @@ -319,28 +324,33 @@ - (void)imageForKey:(NSString *)key usingBlock:(void (^)(UIImage *image))block { - (void)setImage:(UIImage *)image forKey:(NSString *)key { - NSParameterAssert(key); + [self setImage:image forKey:key diskCacheOnly:NO]; +} +- (void)setImage:(UIImage *)image forKey:(NSString *)key diskCacheOnly:(BOOL)useDiskCacheOnly { + NSParameterAssert(key); + // If there's no image, delete the key. if (!image) { [self removeObjectForKey:key]; return; } - + key = [[self class] _keyForImageKey:key]; - + dispatch_async(self.diskQueue, ^{ NSString *path = [self _pathForKey:key]; - - // Save to memory cache - [self.cache setObject:image forKey:key]; - + + if (useDiskCacheOnly == NO) { + // Save to memory cache + [self.cache setObject:image forKey:key]; + } + // Save to disk cache [UIImagePNGRepresentation(image) writeToFile:path atomically:YES]; }); } - - (BOOL)imageExistsForKey:(NSString *)key { NSParameterAssert(key); return [self objectExistsForKey:[[self class] _keyForImageKey:key]]; From d3bdbea3badafa3b71e966c63efc70c0000f2be9 Mon Sep 17 00:00:00 2001 From: Daryll Herberger <daryllh@vertigo.com> Date: Wed, 16 Jul 2014 18:32:16 -0700 Subject: [PATCH 2/4] Removing call to delete cache directory for a SAMCache named instance -- calling this selector appears to remove the cache directory entirely, resulting in failure to make subsequent insertions into the properly named subfolder on disk --- SAMCache/SAMCache.m | 1 - 1 file changed, 1 deletion(-) diff --git a/SAMCache/SAMCache.m b/SAMCache/SAMCache.m index c007286..f1fb33e 100644 --- a/SAMCache/SAMCache.m +++ b/SAMCache/SAMCache.m @@ -202,7 +202,6 @@ - (void)removeAllObjects { for (NSString *path in [self.fileManager contentsOfDirectoryAtPath:self.directory error:nil]) { [self.fileManager removeItemAtPath:[self.directory stringByAppendingPathComponent:path] error:nil]; } - [self.fileManager removeItemAtPath:self.directory error:nil]; }); } From 6d7098c50d726f943bca9f64f4c199d217ddf790 Mon Sep 17 00:00:00 2001 From: Daryll Herberger <daryllh@vertigo.com> Date: Mon, 6 Oct 2014 13:53:56 -0700 Subject: [PATCH 3/4] Fixing comments for setObject selectors to clarify that the disk cache saving operation is asynchronous, adding selector to allow for disk queue writing completion callback --- SAMCache/SAMCache.h | 19 ++++++++++++++++--- SAMCache/SAMCache.m | 10 +++++++++- 2 files changed, 25 insertions(+), 4 deletions(-) diff --git a/SAMCache/SAMCache.h b/SAMCache/SAMCache.h index 40e5903..3ae8313 100755 --- a/SAMCache/SAMCache.h +++ b/SAMCache/SAMCache.h @@ -105,7 +105,7 @@ ///---------------------------------------- /** - Synchronously set an object in the cache for a given key. Uses both memory and disk cache. + Synchronously set an object in the memory cache for a given key, while asynchronously writing to the disk cache. Uses both memory and disk cache. @param object The object to store in the cache. @@ -114,16 +114,29 @@ - (void)setObject:(id <NSCoding>)object forKey:(NSString *)key; /** - Synchronously set an object in the cache for a given key. + Synchronously set an object in the memory cache for a given key, while asynchronously writing to the disk cache. Has an option to only write asynchronously to the disk cache. @param object The object to store in the cache. @param key The key of the object. - @param useDiskCacheOnly A value indicating whether or not to store the object in memory or only write object to disk cache location. + @param useDiskCacheOnly A value indicating whether or not to store the object in memory or only write object to disk cache location in an asynchronous fashion. */ - (void)setObject:(id <NSCoding>)object forKey:(NSString *)key diskCacheOnly:(BOOL)useDiskCacheOnly; +/** + Synchronously set an object in the memory cache for a given key, while asynchronously writing to the disk cache. Has an option to only write asynchronously to the disk cache, along with a completion block for the asynchronous write operation. + + @param object The object to store in the cache. + + @param key The key of the object. + + @param useDiskCacheOnly A value indicating whether or not to store the object in memory or only write object to disk cache location in an asynchronous fashion. + + @param withCompletion A callback block that indicates that all operations (synchronous and asynchronous) have been completed, with an indication of success for the disk wrigin operation. + */ +- (void)setObject:(id <NSCoding>)object forKey:(NSString *)key diskCacheOnly:(BOOL)useDiskCacheOnly withCompletion:(void (^)(BOOL didSave))block; + /** Remove an object from the cache. diff --git a/SAMCache/SAMCache.m b/SAMCache/SAMCache.m index 2a80ee4..1e0d133 100755 --- a/SAMCache/SAMCache.m +++ b/SAMCache/SAMCache.m @@ -170,6 +170,10 @@ - (void)setObject:(id <NSCoding>)object forKey:(NSString *)key { } - (void)setObject:(id <NSCoding>)object forKey:(NSString *)key diskCacheOnly:(BOOL)useDiskCacheOnly { + [self setObject:object forKey:key diskCacheOnly:NO withCompletion:nil]; +} + +- (void)setObject:(id <NSCoding>)object forKey:(NSString *)key diskCacheOnly:(BOOL)useDiskCacheOnly withCompletion:(void (^)(BOOL didSave))block { NSParameterAssert(key); // If there's no object, delete the key. @@ -185,7 +189,11 @@ - (void)setObject:(id <NSCoding>)object forKey:(NSString *)key diskCacheOnly:(BO dispatch_async(self.diskQueue, ^{ // Save to disk cache - [NSKeyedArchiver archiveRootObject:object toFile:[self _pathForKey:key]]; + BOOL didSave = [NSKeyedArchiver archiveRootObject:object toFile:[self _pathForKey:key]]; + + if (block) { + block(didSave); + } }); } From b534939bf4a89c59d6147a9c3d7994468c151c49 Mon Sep 17 00:00:00 2001 From: Daryll Herberger <daryllh@vertigo.com> Date: Thu, 20 Nov 2014 11:36:24 -0800 Subject: [PATCH 4/4] Adding selector for 'flushMemoryCache' -- useful for clearing the memory cache while keeping the disk cache intact, added unit test coverage for the new selectors --- SAMCache/SAMCache.h | 13 +++++++++---- SAMCache/SAMCache.m | 19 +++++++++++++------ Tests/SAMCacheTests.m | 36 ++++++++++++++++++++++++++++++++++++ 3 files changed, 58 insertions(+), 10 deletions(-) diff --git a/SAMCache/SAMCache.h b/SAMCache/SAMCache.h index 3ae8313..649ee7d 100755 --- a/SAMCache/SAMCache.h +++ b/SAMCache/SAMCache.h @@ -6,7 +6,7 @@ // Copyright (c) 2011-2014 Sam Soffes. All rights reserved. // -#import <Foundation/Foundation.h> +@import Foundation; @interface SAMCache : NSObject @@ -133,9 +133,9 @@ @param useDiskCacheOnly A value indicating whether or not to store the object in memory or only write object to disk cache location in an asynchronous fashion. - @param withCompletion A callback block that indicates that all operations (synchronous and asynchronous) have been completed, with an indication of success for the disk wrigin operation. + @param completionBlock A callback block that indicates that all operations (synchronous and asynchronous) have been completed, with an indication of success for the disk wrigin operation. */ -- (void)setObject:(id <NSCoding>)object forKey:(NSString *)key diskCacheOnly:(BOOL)useDiskCacheOnly withCompletion:(void (^)(BOOL didSave))block; +- (void)setObject:(id <NSCoding>)object forKey:(NSString *)key diskCacheOnly:(BOOL)useDiskCacheOnly withCompletion:(void (^)(BOOL didSave))completionBlock; /** Remove an object from the cache. @@ -149,6 +149,11 @@ */ - (void)removeAllObjects; +/** + Removes all cached objects from the memory cache. Does not remove disk-cache + entries. + */ +- (void)flushMemoryCache; ///------------------------------- /// @name Accessing the Disk Cache @@ -195,7 +200,7 @@ #if TARGET_OS_IPHONE -#import <UIKit/UIImage.h> +@import UIKit.UIImage; @interface SAMCache (UIImageAdditions) diff --git a/SAMCache/SAMCache.m b/SAMCache/SAMCache.m index 1e0d133..8334df5 100755 --- a/SAMCache/SAMCache.m +++ b/SAMCache/SAMCache.m @@ -170,10 +170,10 @@ - (void)setObject:(id <NSCoding>)object forKey:(NSString *)key { } - (void)setObject:(id <NSCoding>)object forKey:(NSString *)key diskCacheOnly:(BOOL)useDiskCacheOnly { - [self setObject:object forKey:key diskCacheOnly:NO withCompletion:nil]; + [self setObject:object forKey:key diskCacheOnly:useDiskCacheOnly withCompletion:nil]; } -- (void)setObject:(id <NSCoding>)object forKey:(NSString *)key diskCacheOnly:(BOOL)useDiskCacheOnly withCompletion:(void (^)(BOOL didSave))block { +- (void)setObject:(id <NSCoding>)object forKey:(NSString *)key diskCacheOnly:(BOOL)useDiskCacheOnly withCompletion:(void (^)(BOOL didSave))completionBlock { NSParameterAssert(key); // If there's no object, delete the key. @@ -187,12 +187,16 @@ - (void)setObject:(id <NSCoding>)object forKey:(NSString *)key diskCacheOnly:(BO [self.cache setObject:object forKey:key]; } + __weak SAMCache *weakSelf = self; + dispatch_async(self.diskQueue, ^{ + __strong SAMCache *strongSelf = weakSelf; + // Save to disk cache - BOOL didSave = [NSKeyedArchiver archiveRootObject:object toFile:[self _pathForKey:key]]; + BOOL didSave = [NSKeyedArchiver archiveRootObject:object toFile:[strongSelf _pathForKey:key]]; - if (block) { - block(didSave); + if (completionBlock) { + completionBlock(didSave); } }); } @@ -218,6 +222,9 @@ - (void)removeAllObjects { }); } +- (void)flushMemoryCache { + [self.cache removeAllObjects]; +} #pragma mark - Accessing the Disk Cache @@ -274,7 +281,7 @@ - (NSString *)_pathForKey:(NSString *)key { #if TARGET_OS_IPHONE -#import <UIKit/UIScreen.h> +@import UIKit.UIScreen; @implementation SAMCache (UIImageAdditions) diff --git a/Tests/SAMCacheTests.m b/Tests/SAMCacheTests.m index e021ba6..22a0fb4 100644 --- a/Tests/SAMCacheTests.m +++ b/Tests/SAMCacheTests.m @@ -69,4 +69,40 @@ - (void)testReadingWithSubscript { XCTAssertEqualObjects(@"subread", self.cache[@"subscriptRead"], @"Reading an object with a subscript"); } +- (void)testAddingToDiskCacheOnly { + [self.cache setObject:@42 forKey:@"answer" diskCacheOnly:YES]; + + XCTAssertNil([self.cache.cache objectForKey:@"answer"]); + + XCTAssertEqualObjects(@42, [self.cache objectForKey:@"answer"], @"Reading from disk cache"); + + XCTAssertNotNil([self.cache.cache objectForKey:@"answer"]); +} + +- (void)testAddingToDiskCacheOnlyWithCallback { + XCTestExpectation *diskWriteExpectation = [self expectationWithDescription:@"object written to disk"]; + + __weak SAMCacheTests *weakSelf = self; + + [weakSelf.cache setObject:@42 forKey:@"answer" diskCacheOnly:YES withCompletion:^(BOOL didSave) { + XCTAssert(didSave); + [diskWriteExpectation fulfill]; + }]; + + [self waitForExpectationsWithTimeout:1 handler:nil]; +} + +- (void)testFlushMemoryCache { + [self.cache setObject:@42 forKey:@"answer"]; + XCTAssertEqualObjects(@42, [self.cache objectForKey:@"answer"], @"Reading from memory cache"); + + [self.cache flushMemoryCache]; + + XCTAssertNil([self.cache.cache objectForKey:@"answer"]); + + XCTAssertEqualObjects(@42, [self.cache objectForKey:@"answer"], @"Reading from disk cache"); + + XCTAssertNotNil([self.cache.cache objectForKey:@"answer"]); +} + @end