Skip to content

Commit

Permalink
[core] Refactor the in memory image cache to use pixels as the counte…
Browse files Browse the repository at this point in the history
…r instead of "memory". Also fleshed out nil handling and corresponding unit tests.
  • Loading branch information
jverkoey committed Jul 3, 2011
1 parent 4b69f64 commit ba7b72f
Show file tree
Hide file tree
Showing 4 changed files with 133 additions and 46 deletions.
8 changes: 8 additions & 0 deletions src/core/src/NIDataStructures.m
Expand Up @@ -16,6 +16,8 @@

#import "NIDataStructures.h"

#import "NIDebuggingTools.h"


///////////////////////////////////////////////////////////////////////////////////////////////////
///////////////////////////////////////////////////////////////////////////////////////////////////
Expand Down Expand Up @@ -226,6 +228,12 @@ - (id)lastObject {

///////////////////////////////////////////////////////////////////////////////////////////////////
- (NILinkedListLocation *)addObject:(id)object {
// nil objects can not be added to a linked list.
NIDASSERT(nil != object);
if (nil == object) {
return nil;
}

struct NILinkedListNode* node = malloc(sizeof(struct NILinkedListNode));
memset(node, 0, sizeof(struct NILinkedListNode));
node->object = [object retain];
Expand Down
35 changes: 17 additions & 18 deletions src/core/src/NIInMemoryCache.h
Expand Up @@ -150,53 +150,52 @@


/**
* An in-memory cache for storing images with a least-recently-used memory cap.
* An in-memory cache for storing images with caps on the total number of pixels.
*
* When reduceMemoryUsage is called, the least recently used images are removed from the cache
* until the totalMemoryUsage is below maxTotalLowMemoryUsage.
* until the numberOfPixels is below maxNumberOfPixelsUnderStress.
*
* When an image is added to the cache that causes the memory usage to pass the max, the
* least recently used images are removed from the cache until totalMemoryUsage is below
* maxTotalMemoryUsage.
* least recently used images are removed from the cache until the numberOfPixels is below
* maxNumberOfPixels.
*
* By default the image memory cache has no upper bound on its memory. You must explicitly
* By default the image memory cache has no limit to its pixel count. You must explicitly
* set this value in your application.
*
* @attention If the cache is too small to fit the newly added image, then all images
* will end up being removed including the one being added.
*
* @remark The way memory is calculated isn't completely accurate, so do not use it
* as a means of measuring the exact number of bytes used in the cache.
* @remark Memory is calculated in total number of pixels.
*
* @see Nimbus::globalImageMemoryCache
* @see Nimbus::setGlobalImageMemoryCache:
*/
@interface NIImageMemoryCache : NIMemoryCache {
@private
NSUInteger _totalMemoryUsage;
NSUInteger _numberOfPixels;

NSUInteger _maxTotalMemoryUsage;
NSUInteger _maxTotalLowMemoryUsage;
NSUInteger _maxNumberOfPixels;
NSUInteger _maxNumberOfPixelsUnderStress;
}

/**
* The total amount of memory being used.
* The total number of pixels being stored in the cache.
*/
@property (nonatomic, readonly, assign) NSUInteger totalMemoryUsage;
@property (nonatomic, readonly, assign) NSUInteger numberOfPixels;

/**
* The maximum amount of memory this cache may ever use.
* The maximum number of pixels this cache may ever store.
*
* Defaults to 0, which is special cased to represent an unbounded cache size.
* Defaults to 0, which is special cased to represent an unlimited number of pixels.
*/
@property (nonatomic, readwrite, assign) NSUInteger maxTotalMemoryUsage;
@property (nonatomic, readwrite, assign) NSUInteger maxNumberOfPixels;

/**
* The maximum amount of memory this cache may use after a call to reduceMemoryUsage.
* The maximum number of pixels this cache may store after a call to reduceMemoryUsage.
*
* Defaults to 0, which is special cased to represent an unbounded cache size.
* Defaults to 0, which is special cased to represent an unlimited number of pixels.
*/
@property (nonatomic, readwrite, assign) NSUInteger maxTotalLowMemoryUsage;
@property (nonatomic, readwrite, assign) NSUInteger maxNumberOfPixelsUnderStress;

@end

Expand Down
66 changes: 49 additions & 17 deletions src/core/src/NIInMemoryCache.m
Expand Up @@ -128,6 +128,10 @@ - (id)initWithCapacity:(NSUInteger)capacity {

///////////////////////////////////////////////////////////////////////////////////////////////////
- (void)updateAccessTimeForInfo:(NIMemoryCacheInfo *)info {
NIDASSERT(nil != info);
if (nil == info) {
return;
}
info.lastAccessTime = [NSDate date];

[_lruCacheObjects removeObjectAtLocation:info.lruLocation];
Expand All @@ -143,6 +147,11 @@ - (NIMemoryCacheInfo *)cacheInfoForName:(NSString *)name {

///////////////////////////////////////////////////////////////////////////////////////////////////
- (void)setCacheInfo:(NIMemoryCacheInfo *)info forName:(NSString *)name {
NIDASSERT(nil != name);
if (nil == name) {
return;
}

// Storing in the cache counts as an access of the object, so we update the access time.
[self updateAccessTimeForInfo:info];

Expand All @@ -155,6 +164,11 @@ - (void)setCacheInfo:(NIMemoryCacheInfo *)info forName:(NSString *)name {

///////////////////////////////////////////////////////////////////////////////////////////////////
- (void)removeCacheInfoForName:(NSString *)name {
NIDASSERT(nil != name);
if (nil == name) {
return;
}

[self willRemoveObject:[self cacheInfoForName:name].object withName:name];
[_cacheMap removeObjectForKey:name];
}
Expand Down Expand Up @@ -192,6 +206,11 @@ - (void)storeObject:(id)object withName:(NSString *)name {

///////////////////////////////////////////////////////////////////////////////////////////////////
- (void)storeObject:(id)object withName:(NSString *)name expiresAfter:(NSDate *)expirationDate {
// Don't store nil objects in the cache.
if (nil == object) {
return;
}

if (nil != expirationDate && [[NSDate date] timeIntervalSinceDate:expirationDate] >= 0) {
// The object being stored is already expired so remove the object from the cache altogether.
[self removeObjectWithName:name];
Expand Down Expand Up @@ -229,8 +248,10 @@ - (id)objectWithName:(NSString *)name {
return nil;
}

// Update the access time whenever we fetch an object from the cache.
[self updateAccessTimeForInfo:info];
if (nil != info) {
// Update the access time whenever we fetch an object from the cache.
[self updateAccessTimeForInfo:info];
}

return info.object;
}
Expand Down Expand Up @@ -309,24 +330,34 @@ - (BOOL)hasExpired {
@end


///////////////////////////////////////////////////////////////////////////////////////////////////
///////////////////////////////////////////////////////////////////////////////////////////////////
///////////////////////////////////////////////////////////////////////////////////////////////////
@interface NIImageMemoryCache()

// Internally only.
@property (nonatomic, readwrite, assign) NSUInteger numberOfPixels;

@end


///////////////////////////////////////////////////////////////////////////////////////////////////
///////////////////////////////////////////////////////////////////////////////////////////////////
///////////////////////////////////////////////////////////////////////////////////////////////////
@implementation NIImageMemoryCache

@synthesize totalMemoryUsage = _totalMemoryUsage;
@synthesize maxTotalMemoryUsage = _maxTotalMemoryUsage;
@synthesize maxTotalLowMemoryUsage = _maxTotalLowMemoryUsage;
@synthesize numberOfPixels = _numberOfPixels;
@synthesize maxNumberOfPixels = _maxNumberOfPixels;
@synthesize maxNumberOfPixelsUnderStress = _maxNumberOfPixelsUnderStress;


///////////////////////////////////////////////////////////////////////////////////////////////////
- (NSUInteger)numberOfBytesUsedByImage:(UIImage *)image {
// This is a rough estimate.
NSUInteger numberOfBytesUsed = (NSUInteger)(image.size.width * image.size.height * 4);
- (NSUInteger)numberOfPixelsUsedByImage:(UIImage *)image {
NSUInteger numberOfPixels = (NSUInteger)(image.size.width * image.size.height);
if ([image respondsToSelector:@selector(scale)]) {
numberOfBytesUsed *= image.scale;
numberOfPixels *= image.scale;
}
return numberOfBytesUsed;
return numberOfPixels;
}


Expand All @@ -335,9 +366,9 @@ - (void)reduceMemoryUsage {
// Remove all expired images first.
[super reduceMemoryUsage];

if (_maxTotalLowMemoryUsage > 0) {
if (self.maxNumberOfPixelsUnderStress > 0) {
// Remove the least recently used images by iterating over the linked list.
while (_totalMemoryUsage > _maxTotalLowMemoryUsage) {
while (self.numberOfPixels > self.maxNumberOfPixelsUnderStress) {
NIMemoryCacheInfo* info = [self.lruCacheObjects firstObject];
[self removeCacheInfoForName:info.name];
}
Expand All @@ -351,12 +382,13 @@ - (void)willSetObject:(id)object withName:(NSString *)name previousObject:(id)pr
if (![object isKindOfClass:[UIImage class]]) {
return;
}
_totalMemoryUsage -= [self numberOfBytesUsedByImage:previousObject];
_totalMemoryUsage += [self numberOfBytesUsedByImage:object];
self.numberOfPixels -= [self numberOfPixelsUsedByImage:previousObject];
self.numberOfPixels += [self numberOfPixelsUsedByImage:object];

if (_maxTotalMemoryUsage > 0) {
if (self.maxNumberOfPixels > 0) {
// Remove least recently used images until we satisfy our memory constraints.
while (_totalMemoryUsage > _maxTotalMemoryUsage) {
while (self.numberOfPixels > self.maxNumberOfPixels
&& [self.lruCacheObjects count] > 0) {
NIMemoryCacheInfo* info = [self.lruCacheObjects firstObject];
[self removeCacheInfoForName:info.name];
}
Expand All @@ -374,7 +406,7 @@ - (void)willRemoveObject:(id)object withName:(NSString *)name {
NIMemoryCacheInfo* info = [self cacheInfoForName:name];
[self.lruCacheObjects removeObjectAtLocation:info.lruLocation];

_totalMemoryUsage -= [self numberOfBytesUsedByImage:object];
self.numberOfPixels -= [self numberOfPixelsUsedByImage:object];
}


Expand Down
70 changes: 59 additions & 11 deletions src/core/unittests/NIMemoryCacheTests.m
Expand Up @@ -300,12 +300,60 @@ - (void)testImageCacheNoLimit {
}


///////////////////////////////////////////////////////////////////////////////////////////////////
- (void)testImageCacheNils {
NIImageMemoryCache* cache = [[[NIImageMemoryCache alloc] init] autorelease];

[cache storeObject: nil
withName: @"obj1"];
STAssertEquals([cache count], (NSUInteger)0, @"No objects should have been stored in the cache.");

[cache storeObject: nil
withName: nil];
STAssertEquals([cache count], (NSUInteger)0, @"No objects should have been stored in the cache.");

[cache storeObject: [NSDictionary dictionary]
withName: nil];
STAssertEquals([cache count], (NSUInteger)0, @"No objects should have been stored in the cache.");

[cache storeObject: [NSDictionary dictionary]
withName: nil
expiresAfter: nil];
STAssertEquals([cache count], (NSUInteger)0, @"No objects should have been stored in the cache.");

[cache storeObject: [NSDictionary dictionary]
withName: nil
expiresAfter: [NSDate dateWithTimeIntervalSinceNow:1]];
STAssertEquals([cache count], (NSUInteger)0, @"No objects should have been stored in the cache.");

[cache storeObject: nil
withName: @"obj1"
expiresAfter: nil];
STAssertEquals([cache count], (NSUInteger)0, @"No objects should have been stored in the cache.");

[cache storeObject: nil
withName: nil
expiresAfter: [NSDate dateWithTimeIntervalSinceNow:1]];
STAssertEquals([cache count], (NSUInteger)0, @"No objects should have been stored in the cache.");

[cache storeObject: nil
withName: nil
expiresAfter: nil];
STAssertEquals([cache count], (NSUInteger)0, @"No objects should have been stored in the cache.");

STAssertNil([cache objectWithName:nil], @"The result should be nil");
[cache removeObjectWithName:nil];

STAssertEquals([cache count], (NSUInteger)0, @"No objects should have been stored in the cache.");
}


///////////////////////////////////////////////////////////////////////////////////////////////////
- (void)testImageCacheStoreTooMuch {
NIImageMemoryCache* cache = [[[NIImageMemoryCache alloc] init] autorelease];

static const NSUInteger numberOfBytesInOneImage = 100 * 100 * 4;
cache.maxTotalMemoryUsage = numberOfBytesInOneImage;
static const NSUInteger numberOfPixelsInOneImage = 100 * 100;
cache.maxNumberOfPixels = numberOfPixelsInOneImage;

UIImage* img1 = [self emptyImageWithSize:CGSizeMake(100, 100)];
UIImage* img2 = [self emptyImageWithSize:CGSizeMake(100, 100)];
Expand All @@ -327,9 +375,9 @@ - (void)testImageCacheStoreTooMuch {
- (void)testImageCacheReduceMemoryUsage {
NIImageMemoryCache* cache = [[[NIImageMemoryCache alloc] init] autorelease];

static const NSUInteger numberOfBytesInOneImage = 100 * 100 * 4;
cache.maxTotalMemoryUsage = numberOfBytesInOneImage * 2;
cache.maxTotalLowMemoryUsage = numberOfBytesInOneImage;
static const NSUInteger numberOfPixelsInOneImage = 100 * 100;
cache.maxNumberOfPixels = numberOfPixelsInOneImage * 2;
cache.maxNumberOfPixelsUnderStress = numberOfPixelsInOneImage;

UIImage* img1 = [self emptyImageWithSize:CGSizeMake(100, 100)];
UIImage* img2 = [self emptyImageWithSize:CGSizeMake(100, 100)];
Expand All @@ -356,9 +404,9 @@ - (void)testImageCacheReduceMemoryUsage {
- (void)testImageCacheReduceMemoryUsageWithAccess {
NIImageMemoryCache* cache = [[[NIImageMemoryCache alloc] init] autorelease];

static const NSUInteger numberOfBytesInOneImage = 100 * 100 * 4;
cache.maxTotalMemoryUsage = numberOfBytesInOneImage * 2;
cache.maxTotalLowMemoryUsage = numberOfBytesInOneImage;
static const NSUInteger numberOfPixelsInOneImage = 100 * 100;
cache.maxNumberOfPixels = numberOfPixelsInOneImage * 2;
cache.maxNumberOfPixelsUnderStress = numberOfPixelsInOneImage;

UIImage* img1 = [self emptyImageWithSize:CGSizeMake(100, 100)];
UIImage* img2 = [self emptyImageWithSize:CGSizeMake(100, 100)];
Expand Down Expand Up @@ -387,9 +435,9 @@ - (void)testImageCacheReduceMemoryUsageWithAccess {
- (void)testImageCacheReduceMemoryUsageWithThrashingAccess {
NIImageMemoryCache* cache = [[[NIImageMemoryCache alloc] init] autorelease];

static const NSUInteger numberOfBytesInOneImage = 100 * 100 * 4;
cache.maxTotalMemoryUsage = numberOfBytesInOneImage * 2;
cache.maxTotalLowMemoryUsage = numberOfBytesInOneImage;
static const NSUInteger numberOfPixelsInOneImage = 100 * 100;
cache.maxNumberOfPixels = numberOfPixelsInOneImage * 2;
cache.maxNumberOfPixelsUnderStress = numberOfPixelsInOneImage;

UIImage* img1 = [self emptyImageWithSize:CGSizeMake(100, 100)];
UIImage* img2 = [self emptyImageWithSize:CGSizeMake(100, 100)];
Expand Down

0 comments on commit ba7b72f

Please sign in to comment.