diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..7e11480 --- /dev/null +++ b/.gitignore @@ -0,0 +1,22 @@ +# Mac OS X Finder +.DS_Store + +# Xcode +build/* +*.pbxuser +!default.pbxuser +*.mode1v3 +!default.mode1v3 +*.mode2v3 +!default.mode2v3 +*.perspectivev3 +!default.perspectivev3 +*.xcworkspace +!default.xcworkspace +xcuserdata +profile +*.moved-aside +DerivedData + +# Demo Images +FastImageCacheDemo/Demo Images/*.jpg diff --git a/FastImageCache/FICEntity.h b/FastImageCache/FICEntity.h new file mode 100644 index 0000000..fec135d --- /dev/null +++ b/FastImageCache/FICEntity.h @@ -0,0 +1,77 @@ +// +// FICEntity.h +// FastImageCache +// +// Copyright (c) 2013 Path, Inc. +// See LICENSE for full license agreement. +// + +#import "FICImports.h" + +typedef void (^FICEntityImageDrawingBlock)(CGContextRef context, CGSize contextSize); + +/** + `FICEntity` is a protocol that classes interacting with the image cache must conform to. An entity uniquely identifies entries in image tables, which are instances of ``. + */ +@protocol FICEntity + +@required + +/** + A string that uniquely identifies this entity. + + @discussion Within each image table, each entry is identified by an entity's UUID. Ideally, this value should never change for an entity. For example, if your entity class is a person + model, its UUID might be an API-assigned, unchanging, unique user ID. No matter how the properties of the person change, its user ID should never change. + */ +@property (nonatomic, copy, readonly) NSString *UUID; + +/** + A string that uniquely identifies an entity's source image. + + @discussion While `` should be unchanging, a source image UUID might change. For example, if your entity class is a person model, its source image UUID might change every time the + person changes their profile photo. In this case, the source image UUID might be a hash of the profile photo URL (assuming each image is given a unique URL). + */ +@property (nonatomic, copy, readonly) NSString *sourceImageUUID; + +/** + Returns the source image URL associated with a specific format name. + + @param formatName The name of the image format that identifies which image table is requesting the source image. + + @return A URL representing the requested source image. + + @discussion Fast Image Cache operates on URLs when requesting source images. Typically, these URLs will point to remote image resources that must be downloaded from the Internet. While the + URL returned by this method must be a valid instance of `NSURL`, it does not need to point to an actual remote resource. The URL might point to a file path on disk or be composed of a custom + URL scheme of your choosing. The image cache's delegate is prompted to provide a source image for a particular entity and format name when it cannot find the requested image. It only uses the + URL returned by this method to key image cache requests. No network or file operations are performed by the image cache. + + An example of when this method might return different source image URLs for the same entity is if you have defined several image formats for different thumbnail sizes and styles. For very + large thumbnails, the source image URL might be the original image. For smaller thumbnails, the source image URL might point to a downscaled version of the original image. + + @see FICImageFormat + @see [FICImageCacheDelegate imageCache:wantsSourceImageForEntity:withFormatName:completionBlock:] + */ +- (NSURL *)sourceImageURLWithFormatName:(NSString *)formatName; + +/** + Returns the drawing block for a specific image and format name. + + @param image The cached image that represents this entity. + + @param formatName The name of the image format that identifies which image table is requesting the source image. + + @return The drawing block used to draw the image data to be stored in the image table. + + The drawing block's type is defined as follows: + + typedef void (^FICEntityImageDrawingBlock)(CGContextRef context, CGSize contextSize) + + @discussion Each entity is responsible for drawing its own source image into the bitmap context provided by the image table that will store the image data. Often it is sufficient to simply + draw the image into the bitmap context. However, if you wish to apply any additional graphics processing to the source image before it is stored (such as clipping the image to a roundect rect), + you may use this block to do so. + + @note This block will always be called from the serial dispatch queue used by the image cache. + */ +- (FICEntityImageDrawingBlock)drawingBlockForImage:(UIImage *)image withFormatName:(NSString *)formatName; + +@end diff --git a/FastImageCache/FICImageCache+FICErrorLogging.h b/FastImageCache/FICImageCache+FICErrorLogging.h new file mode 100644 index 0000000..f8b00f0 --- /dev/null +++ b/FastImageCache/FICImageCache+FICErrorLogging.h @@ -0,0 +1,32 @@ +// +// FICImageCache+FICErrorLogging.h +// FastImageCache +// +// Copyright (c) 2013 Path, Inc. +// See LICENSE for full license agreement. +// + +#import "FICImageCache.h" + +/** + This category on `` simply exposes its private logging mechanism to other classes. + */ +@interface FICImageCache (FICErrorLogging) + +///----------------------------- +/// @name Logging Error Messages +///----------------------------- + +/** + Passes an error message to the image cache. + + @param message A string representing the error message. + + @discussion Rather than logging directly to standard output, Fast Image Cache classes pass all error logging to the shared `` instance. `` then allows its delegate to handle the + message. + + @see [FICImageCacheDelegate imageCache:errorDidOccurWithMessage:] + */ +- (void)_logMessage:(NSString *)message; + +@end diff --git a/FastImageCache/FICImageCache.h b/FastImageCache/FICImageCache.h new file mode 100644 index 0000000..e36375b --- /dev/null +++ b/FastImageCache/FICImageCache.h @@ -0,0 +1,262 @@ +// +// FICImageCache.h +// FastImageCache +// +// Copyright (c) 2013 Path, Inc. +// See LICENSE for full license agreement. +// + +#import "FICImports.h" +#import "FICImageFormat.h" +#import "FICEntity.h" + +@protocol FICEntity; +@protocol FICImageCacheDelegate; + +typedef void (^FICImageCacheCompletionBlock)(id entity, NSString *formatName, UIImage *image); +typedef void (^FICImageRequestCompletionBlock)(UIImage *sourceImage); + +/** + `FICImageCache` is the primary class for managing and interacting with the image cache. Applications using the image cache create one or more `` + objects. These formats effectively act as logical groupings for image data stored in the image cache. An `` object is created for each format defined by + your application to allow for efficient storage and retrieval of image data. Image data is keyed off of objects conforming to the `` protocol as well as an + image format name. + */ +@interface FICImageCache : NSObject + +///---------------------------- +/// @name Managing the Delegate +///---------------------------- + +/** + The delegate of the image cache. + + @discussion The delegate is responsible for asynchronously providing the source image for an entity. Optionally, the delegate can require that all formats in a format + family for a particular entity be processed. Any errors that occur in the image cache are also communicated back to the delegate. + */ +@property(nonatomic, assign) id delegate; + +///--------------------------------------- +/// @name Accessing the Shared Image Cache +///--------------------------------------- + +/** + Convenience accessor to retrieve a shared image cache instance. + */ ++ (instancetype)sharedImageCache; + +///--------------------------------- +/// @name Working with Image Formats +///--------------------------------- + +/** + Sets the image formats to be used by the image cache. + + @param formats An array of `` objects. + + @note Once the image formats have been set, subsequent calls to this method will do nothing. + */ +- (void)setFormats:(NSArray *)formats; + +/** + Returns an image format previously associated with the image cache. + + @param formatName The name of the image format to return. + + @return An image format with the name `formatName` or `nil` if no format with that name exists. + */ +- (FICImageFormat *)formatWithName:(NSString *)formatName; + +/** + Returns all the image formats of the same family previously associated with the image cache. + + @param family The name of the family of image formats to return. + + @return An array of `` objects whose family is `family` or `nil` if no format belongs to that family. + */ +- (NSArray *)formatsWithFamily:(NSString *)family; + +///----------------------------------------------- +/// @name Storing, Retrieving, and Deleting Images +///----------------------------------------------- + +/** + Manually sets the the image to be used by the image cache for a particular entity and format name. + + @discussion Usually the image cache's delegate is responsible for lazily providing the source image for a given entity. This source image is then processed according + to the drawing block defined by an entity for a given image format. This method allows the sender to explicitly set the image data to be stored in the image cache. + After the image has been processed by the image cache, the completion block is called asynchronously on the main queue. + + @param image The image to store in the image cache. + + @param entity The entity that uniquely identifies the source image. + + @param formatName The format name that uniquely identifies which image table to look in for the cached image. + + @param completionBlock The completion block that is called after the image has been processed or if an error occurs. + + The completion block's type is defined as follows: + + typedef void (^FICImageCacheCompletionBlock)(id entity, NSString *formatName, UIImage *image) + */ +- (void)setImage:(UIImage *)image forEntity:(id )entity withFormatName:(NSString *)formatName completionBlock:(FICImageCacheCompletionBlock)completionBlock; + +/** + Attempts to synchronously retrieve an image from the image cache. + + @param entity The entity that uniquely identifies the source image. + + @param formatName The format name that uniquely identifies which image table to look in for the cached image. + + @param completionBlock The completion block that is called when the requested image is available or if an error occurs. + + The completion block's type is defined as follows: + + typedef void (^FICImageCacheCompletionBlock)(id entity, NSString *formatName, UIImage *image) + + If the requested image already exists in the image cache, then the completion block is immediately called synchronously on the current thread. If the requested image + does not already exist in the image cache, then the completion block will be called asynchronously on the main thread as soon as the requested image is available. + + @return `YES` if the requested image already exists in the image case, `NO` if the image needs to be provided to the image cache by its delegate. + + @discussion Even if you make a synchronous image retrieval request, if the image does not yet exist in the image cache, the delegate will be asked to provide a source + image, and it will be processed. This always occurs asynchronously. In this case, the return value from this method will be `NO`, and the image will be available in the + completion block. + + @note You can always rely on the completion block being called. If an error occurs for any reason, the `image` parameter of the completion block will be `nil`. See + <[FICImageCacheDelegate imageCache:errorDidOccurWithMessage:]> for information about being notified when errors occur. + */ +- (BOOL)retrieveImageForEntity:(id )entity withFormatName:(NSString *)formatName completionBlock:(FICImageCacheCompletionBlock)completionBlock; + +/** + Asynchronously retrieves an image from the image cache. + + @param entity The entity that uniquely identifies the source image. + + @param formatName The format name that uniquely identifies which image table to look in for the cached image. + + @param completionBlock The completion block that is called when the requested image is available or if an error occurs. + + The completion block's type is defined as follows: + + typedef void (^FICImageCacheCompletionBlock)(id entity, NSString *formatName, UIImage *image) + + Unlike its synchronous counterpart, this method will always call its completion block asynchronously on the main thread, even if the request image is already in the + image cache. + + @return `YES` if the requested image already exists in the image case, `NO` if the image needs to be provided to the image cache by its delegate. + + @note You can always rely on the completion block being called. If an error occurs for any reason, the `image` parameter of the completion block will be `nil`. See + <[FICImageCacheDelegate imageCache:errorDidOccurWithMessage:]> for information about being notified when errors occur. + + @see [FICImageCache retrieveImageForEntity:withFormatName:completionBlock:] + */ +- (BOOL)asynchronouslyRetrieveImageForEntity:(id )entity withFormatName:(NSString *)formatName completionBlock:(FICImageCacheCompletionBlock)completionBlock; + +/** + Deletes an image from the image cache. + + @param entity The entity that uniquely identifies the source image. + + @param formatName The format name that uniquely identifies which image table to look in for the cached image. + */ +- (void)deleteImageForEntity:(id )entity withFormatName:(NSString *)formatName; + +///----------------------------------- +/// @name Checking for Image Existence +///----------------------------------- + +/** + Returns whether or not an image exists in the image cache. + + @param entity The entity that uniquely identifies the source image. + + @param formatName The format name that uniquely identifies which image table to look in for the cached image. + + @return `YES` if an image exists in the image cache for a given entity and format name. Otherwise, `NO`. + */ +- (BOOL)imageExistsForEntity:(id )entity withFormatName:(NSString *)formatName; + +///-------------------------------- +/// @name Resetting the Image Cache +///-------------------------------- + +/** + Resets the image cache by deleting all image tables and their contents. + + @note Resetting an image cache does not reset its image formats. + */ +- (void)reset; + +@end + +/** + `FICImageCacheDelegate` defines the required and optional actions that an image cache's delegate can perform. + */ +@protocol FICImageCacheDelegate + +@required + +/** + This method is called on the delegate when the image cache needs a source image. + + @param imageCache The image cache that is requesting the source image. + + @param entity The entity that uniquely identifies the source image. + + @param formatName The format name that uniquely identifies which image table to look in for the cached image. + + @param completionBlock The completion block that the receiver must call when it has a source image ready. + + The completion block's type is defined as follows: + + typedef void (^FICImageRequestCompletionBlock)(UIImage *sourceImage) + + The completion block must always be called on the main thread. + + @discussion A source image is usually the original, full-size image that represents an entity. This source image is processed for every unique format to create the + actual image data to be stored in the image cache. This method is an asynchronous data provider, so nothing is actually returned to the sender. Instead, the delegate's + implementation is expected to call the completion block once an image is available. + + Fast Image Cache is architected under the typical design pattern whereby model objects provide a URL to certain image assets and allow the client to actually retrieve + the images via network requests only when needed. As a result, the implementation of this method will usually involve creating an asynchronous network request using + the URL returned by <[FICEntity sourceImageURLWithFormatName:]>, deserializing the image data when the request completes, and finally calling this method's completion + block to provide the image cache with the source image. + */ +- (void)imageCache:(FICImageCache *)imageCache wantsSourceImageForEntity:(id )entity withFormatName:(NSString *)formatName completionBlock:(FICImageRequestCompletionBlock)completionBlock; + +@optional + +/** + This method is called on the delegate to determine whether or not all formats in a family should be processed right now. + + @note If this method is not implemented by the delegate, the default value is `YES`. + + @param imageCache The image cache that is requesting the source image. + + @param formatFamily The name of a format family. + + @param entity The entity that uniquely identifies the source image. + + @return `YES` if all formats in a format family should be processed. Otherwise, `NO`. + + @discussion This method is called whenever new image data is stored in the image cache. Because format families are used to group multiple different formats together, + typically the delegate will want to return `YES` here so that other formats in the same family can be processed. + + For example, if your image cache has defined several different thumbnail sizes and styles for a person model, and if a person changes their profile photo, you would + want every thumbnail size and style is updated with the new source image. + */ +- (BOOL)imageCache:(FICImageCache *)imageCache shouldProcessAllFormatsInFamily:(NSString *)formatFamily forEntity:(id )entity; + +/** + This method is called on the delegate whenever the image cache has an error message to log. + + @param imageCache The image cache that is requesting the source image. + + @param errorMessage The error message generated by the image cache. + + @discussion Fast Image Cache will not explicitly log any messages to standard output. Instead, it allows the delegate to handle (or ignore) any error output. + */ +- (void)imageCache:(FICImageCache *)imageCache errorDidOccurWithMessage:(NSString *)errorMessage; + +@end diff --git a/FastImageCache/FICImageCache.m b/FastImageCache/FICImageCache.m new file mode 100644 index 0000000..93a0a27 --- /dev/null +++ b/FastImageCache/FICImageCache.m @@ -0,0 +1,405 @@ +// +// FICImageCache.m +// FastImageCache +// +// Copyright (c) 2013 Path, Inc. +// See LICENSE for full license agreement. +// + +#import "FICImageCache.h" +#import "FICEntity.h" +#import "FICImageTable.h" +#import "FICImageFormat.h" + +#pragma mark Internal Definitions + +static void _FICAddCompletionBlockForEntity(NSString *formatName, NSMutableDictionary *entityRequestsDictionary, id entity, FICImageCacheCompletionBlock completionBlock); + +static NSString *const FICImageCacheFormatKey = @"FICImageCacheFormatKey"; +static NSString *const FICImageCacheCompletionBlocksKey = @"FICImageCacheCompletionBlocksKey"; +static NSString *const FICImageCacheEntitiesKey = @"FICImageCacheEntitiesKey"; +static NSString *const FICImageCacheEntityKey = @"FICImageCacheEntityKey"; + +#pragma mark - Class Extension + +@interface FICImageCache () { + NSMutableDictionary *_formats; + NSMutableDictionary *_imageTables; + NSMutableDictionary *_requests; + id _delegate; + + BOOL _delegateImplementsShouldProcessAllFormatsInFamilyForEntity; + BOOL _delegateImplementsErrorDidOccurWithMessage; +} + +@end + +#pragma mark + +@implementation FICImageCache + +@synthesize delegate = _delegate; + +#pragma mark - Property Accessors + +- (void)setDelegate:(id)delegate { + if (delegate != _delegate) { + _delegate = delegate; + + _delegateImplementsShouldProcessAllFormatsInFamilyForEntity = [_delegate respondsToSelector:@selector(imageCache:shouldProcessAllFormatsInFamily:forEntity:)]; + _delegateImplementsErrorDidOccurWithMessage = [_delegate respondsToSelector:@selector(imageCache:errorDidOccurWithMessage:)]; + } +} + +static FICImageCache *__imageCache = nil; +static dispatch_queue_t __imageCacheDispatchQueue = NULL; + +#pragma mark - Object Lifecycle + ++ (instancetype)sharedImageCache { + if (__imageCache == nil) { + static dispatch_once_t onceToken; + dispatch_once(&onceToken, ^{ + __imageCache = [[[self class] alloc] init]; + }); + } + + return __imageCache; +} + +- (id)init { + self = [super init]; + + if (self != nil) { + static dispatch_once_t onceToken; + dispatch_once(&onceToken, ^{ + __imageCacheDispatchQueue = dispatch_queue_create("com.path.FastImageCacheQueue", NULL); + }); + + _formats = [[NSMutableDictionary alloc] init]; + _imageTables = [[NSMutableDictionary alloc] init]; + _requests = [[NSMutableDictionary alloc] init]; + } + return self; +} + +- (void)dealloc { + [_formats release]; + [_imageTables release]; + [_requests release]; + + [super dealloc]; +} + +#pragma mark - Working with Formats + +- (void)setFormats:(NSArray *)formats { + if ([_formats count] > 0) { + [self _logMessage:[NSString stringWithFormat:@"*** FIC Error: %s FICImageCache has already been configured with its image formats.", __PRETTY_FUNCTION__]]; + } else { + NSMutableSet *imageTableFiles = [NSMutableSet set]; + FICImageFormatDevices currentDevice = [[UIDevice currentDevice] userInterfaceIdiom] == UIUserInterfaceIdiomPad ? FICImageFormatDevicePad : FICImageFormatDevicePhone; + for (FICImageFormat *imageFormat in formats) { + NSString *formatName = [imageFormat name]; + FICImageFormatDevices devices = [imageFormat devices]; + if (devices & currentDevice) { + // Only initialize an image table for this format if it is needed on the current device. + FICImageTable *imageTable = [[FICImageTable alloc] initWithFormat:imageFormat]; + [_imageTables setObject:imageTable forKey:formatName]; + [imageTable release]; + [_formats setObject:imageFormat forKey:formatName]; + + [imageTableFiles addObject:[[imageTable tableFilePath] lastPathComponent]]; + [imageTableFiles addObject:[[imageTable metadataFilePath] lastPathComponent]]; + } + } + + // Remove any extraneous files in the image tables directory + NSFileManager *fileManager = [NSFileManager defaultManager]; + NSString *directoryPath = [FICImageTable directoryPath]; + NSArray *fileNames = [fileManager contentsOfDirectoryAtPath:directoryPath error:nil]; + for (NSString *fileName in fileNames) { + if ([imageTableFiles containsObject:fileName] == NO) { + // This is an extraneous file, which is no longer needed. + NSString* filePath = [directoryPath stringByAppendingPathComponent:fileName]; + [fileManager removeItemAtPath:filePath error:nil]; + } + } + } +} + +- (FICImageFormat *)formatWithName:(NSString *)formatName { + return [_formats objectForKey:formatName]; +} + +- (NSArray *)formatsWithFamily:(NSString *)family { + NSMutableArray *formats = nil; + for (FICImageFormat *format in [_formats allValues]) { + if ([[format family] isEqualToString:family]) { + if (formats == nil) { + formats = [NSMutableArray array]; + } + + [formats addObject:format]; + } + } + + return [[formats copy] autorelease]; +} + +#pragma mark - Retrieving Images + +- (BOOL)retrieveImageForEntity:(id )entity withFormatName:(NSString *)formatName completionBlock:(FICImageCacheCompletionBlock)completionBlock { + return [self _retrieveImageForEntity:entity withFormatName:formatName loadSynchronously:YES completionBlock:completionBlock]; +} + +- (BOOL)asynchronouslyRetrieveImageForEntity:(id )entity withFormatName:(NSString *)formatName completionBlock:(FICImageCacheCompletionBlock)completionBlock { + return [self _retrieveImageForEntity:entity withFormatName:formatName loadSynchronously:NO completionBlock:completionBlock]; +} + +- (BOOL)_retrieveImageForEntity:(id )entity withFormatName:(NSString *)formatName loadSynchronously:(BOOL)loadSynchronously completionBlock:(FICImageCacheCompletionBlock)completionBlock { + BOOL imageExists = NO; + + FICImageTable *imageTable = [_imageTables objectForKey:formatName]; + NSString *entityUUID = [entity UUID]; + NSString *sourceImageUUID = [entity sourceImageUUID]; + + if (loadSynchronously == NO && [imageTable entryExistsForEntityUUID:entityUUID sourceImageUUID:sourceImageUUID]) { + imageExists = YES; + + dispatch_async(__imageCacheDispatchQueue, ^{ + UIImage *image = [imageTable newImageForEntityUUID:entityUUID sourceImageUUID:sourceImageUUID]; + + if (completionBlock != nil) { + dispatch_async(dispatch_get_main_queue(), ^{ + completionBlock(entity, formatName, image); + }); + } + + [image release]; // Already retained by the block + }); + } else { + UIImage *image = [imageTable newImageForEntityUUID:entityUUID sourceImageUUID:sourceImageUUID]; + imageExists = image != nil; + + dispatch_block_t completionBlockCallingBlock = ^{ + if (completionBlock != nil) { + if (loadSynchronously) { + completionBlock(entity, formatName, image); + } else { + dispatch_async(dispatch_get_main_queue(), ^{ + completionBlock(entity, formatName, image); + }); + } + } + + [image release]; // Already retained by the block + }; + + if (image == nil && _delegate != nil) { + // No image for this UUID exists in the image table. We'll need to ask the delegate to retrieve the source asset. + NSURL *sourceImageURL = [entity sourceImageURLWithFormatName:formatName]; + + if (sourceImageURL != nil) { + // We check to see if this image is already being fetched. + NSMutableDictionary *requestDictionary = [_requests objectForKey:sourceImageURL]; + if (requestDictionary == nil) { + // If we're here, then we aren't currently fetching this image. + NSMutableDictionary *entityRequestsDictionary = [NSMutableDictionary dictionary]; + requestDictionary = [NSMutableDictionary dictionaryWithObjectsAndKeys: entityRequestsDictionary, FICImageCacheEntitiesKey, nil]; + [_requests setObject:requestDictionary forKey:sourceImageURL]; + + _FICAddCompletionBlockForEntity(formatName, entityRequestsDictionary, entity, completionBlock); + [_delegate imageCache:self wantsSourceImageForEntity:entity withFormatName:formatName completionBlock:^(UIImage *sourceImage) { + [self _imageDidLoad:sourceImage forURL:sourceImageURL]; + }]; + } else { + // We have an existing request dictionary, which means this URL is currently being fetched. + NSMutableDictionary *entityRequestsDictionary = [requestDictionary objectForKey:FICImageCacheEntitiesKey]; + _FICAddCompletionBlockForEntity(formatName, entityRequestsDictionary, entity, completionBlock); + } + } else { + NSString *message = [NSString stringWithFormat:@"*** FIC Error: %s entity %@ returned a nil source image URL for image format %@.", __PRETTY_FUNCTION__, entity, formatName]; + [self _logMessage:message]; + + completionBlockCallingBlock(); + } + } else { + completionBlockCallingBlock(); + } + } + + return imageExists; +} + +- (void)_imageDidLoad:(UIImage *)image forURL:(NSURL *)URL { + NSDictionary *requestDictionary = [_requests objectForKey:URL]; + if (image != nil && requestDictionary != nil) { + NSMutableDictionary *entityRequestsDictionary = [requestDictionary objectForKey:FICImageCacheEntitiesKey]; + for (NSMutableDictionary *entityDictionary in [entityRequestsDictionary allValues]) { + id entity = [entityDictionary objectForKey:FICImageCacheEntityKey]; + NSString *formatName = [entityDictionary objectForKey:FICImageCacheFormatKey]; + NSDictionary *completionBlocksDictionary = [entityDictionary objectForKey:FICImageCacheCompletionBlocksKey]; + [self _processImage:image forEntity:entity withFormatName:formatName completionBlocksDictionary:completionBlocksDictionary]; + } + } + + [_requests removeObjectForKey:URL]; +} + +static void _FICAddCompletionBlockForEntity(NSString *formatName, NSMutableDictionary *entityRequestsDictionary, id entity, FICImageCacheCompletionBlock completionBlock) { + NSString *entityUUID = [entity UUID]; + NSMutableDictionary *requestDictionary = [entityRequestsDictionary objectForKey:entityUUID]; + NSMutableDictionary *completionBlocks = nil; + + if (requestDictionary == nil) { + // This is the first time we're dealing with this particular entity for this URL request. + requestDictionary = [NSMutableDictionary dictionaryWithObjectsAndKeys:entity, FICImageCacheEntityKey, nil]; + [entityRequestsDictionary setObject:requestDictionary forKey:entityUUID]; + [requestDictionary setObject:formatName forKey:FICImageCacheFormatKey]; + + // Dictionary where keys are imageFormats, and each value is an array of the completion blocks for the requests for this + // URL at the specified format. + completionBlocks = [NSMutableDictionary dictionary]; + [requestDictionary setObject:completionBlocks forKey:FICImageCacheCompletionBlocksKey]; + } else { + // We already have a request dictionary for this entity, so we just need to append a completion block. + completionBlocks = [requestDictionary objectForKey:FICImageCacheCompletionBlocksKey]; + } + + if (completionBlock != nil) { + NSMutableArray *blocksArray = [completionBlocks objectForKey:formatName]; + if (blocksArray == nil) { + blocksArray = [NSMutableArray array]; + [completionBlocks setObject:blocksArray forKey:formatName]; + } + + FICImageCacheCompletionBlock completionBlockCopy = [[completionBlock copy] autorelease]; + [blocksArray addObject:completionBlockCopy]; + } +} + +#pragma mark - Storing Images + +- (void)setImage:(UIImage *)image forEntity:(id )entity withFormatName:(NSString *)formatName completionBlock:(FICImageCacheCompletionBlock)completionBlock { + if (image != nil && entity != nil) { + NSDictionary *completionBlocksDictionary = nil; + + if (completionBlock != nil) { + completionBlocksDictionary = [NSDictionary dictionaryWithObject:[NSArray arrayWithObject:[[completionBlock copy] autorelease]] forKey:formatName]; + } + + NSString *entityUUID = [entity UUID]; + FICImageTable *imageTable = [_imageTables objectForKey:formatName]; + [imageTable deleteEntryForEntityUUID:entityUUID]; + + [self _processImage:image forEntity:entity withFormatName:formatName completionBlocksDictionary:completionBlocksDictionary]; + } +} + +- (void)_processImage:(UIImage *)image forEntity:(id )entity withFormatName:(NSString *)formatName completionBlocksDictionary:(NSDictionary *)completionBlocksDictionary { + FICImageFormat *imageFormat = [_formats objectForKey:formatName]; + NSString *formatFamily = [imageFormat family]; + NSString *entityUUID = [entity UUID]; + NSString *sourceImageUUID = [entity sourceImageUUID]; + + if (formatFamily != nil) { + BOOL shouldProcessAllFormatsInFamily = YES; + if (_delegateImplementsShouldProcessAllFormatsInFamilyForEntity) { + shouldProcessAllFormatsInFamily = [_delegate imageCache:self shouldProcessAllFormatsInFamily:formatFamily forEntity:entity]; + } + // All of the formats in a given family use the same source asset, so once we have that source asset, we can generate all of the family's formats. + for (FICImageTable *table in [_imageTables allValues]) { + FICImageFormat *imageFormat = [table imageFormat]; + NSString *tableFormatFamily = [imageFormat family]; + if ([formatFamily isEqualToString:tableFormatFamily]) { + NSArray *completionBlocks = [completionBlocksDictionary objectForKey:[imageFormat name]]; + + BOOL imageExistsForEntity = [table entryExistsForEntityUUID:entityUUID sourceImageUUID:sourceImageUUID]; + BOOL shouldProcessFamilyFormat = shouldProcessAllFormatsInFamily && imageExistsForEntity == NO; + if (shouldProcessFamilyFormat || [completionBlocks count] > 0) { + [self _processImage:image forEntity:entity imageTable:table completionBlocks:completionBlocks]; + } + } + } + } else { + FICImageTable *imageTable = [_imageTables objectForKey:formatName]; + NSArray *completionBlocks = [completionBlocksDictionary objectForKey:formatName]; + [self _processImage:image forEntity:entity imageTable:imageTable completionBlocks:completionBlocks]; + } +} + +- (void)_processImage:(UIImage *)image forEntity:(id )entity imageTable:(FICImageTable *)imageTable completionBlocks:(NSArray *)completionBlocks { + if (imageTable != nil) { + if ([entity UUID] == nil) { + [self _logMessage:[NSString stringWithFormat:@"*** FIC Error: %s entity %@ is missing its UUID.", __PRETTY_FUNCTION__, entity]]; + return; + } + + if ([entity sourceImageUUID] == nil) { + [self _logMessage:[NSString stringWithFormat:@"*** FIC Error: %s entity %@ is missing its source image UUID.", __PRETTY_FUNCTION__, entity]]; + return; + } + + NSString *entityUUID = [entity UUID]; + NSString *sourceImageUUID = [entity sourceImageUUID]; + FICImageFormat *imageFormat = [imageTable imageFormat]; + NSString *imageFormatName = [imageFormat name]; + FICEntityImageDrawingBlock imageDrawingBlock = [entity drawingBlockForImage:image withFormatName:imageFormatName]; + + dispatch_async(__imageCacheDispatchQueue, ^{ + [imageTable setEntryForEntityUUID:entityUUID sourceImageUUID:sourceImageUUID imageDrawingBlock:imageDrawingBlock]; + + UIImage *resultImage = [imageTable newImageForEntityUUID:entityUUID sourceImageUUID:sourceImageUUID]; + + if (completionBlocks != nil) { + dispatch_async(dispatch_get_main_queue(), ^{ + NSString *formatName = [[imageTable imageFormat] name]; + for (FICImageCacheCompletionBlock completionBlock in completionBlocks) { + completionBlock(entity, formatName, resultImage); + } + }); + } + + [resultImage release]; + }); + } +} + +#pragma mark - Checking for Image Existence + +- (BOOL)imageExistsForEntity:(id )entity withFormatName:(NSString *)formatName { + FICImageTable *imageTable = [_imageTables objectForKey:formatName]; + NSString *entityUUID = [entity UUID]; + NSString *sourceImageUUID = [entity sourceImageUUID]; + + BOOL imageExists = [imageTable entryExistsForEntityUUID:entityUUID sourceImageUUID:sourceImageUUID]; + + return imageExists; +} + +#pragma mark - Invalidating Image Data + +- (void)deleteImageForEntity:(id )entity withFormatName:(NSString *)formatName { + FICImageTable *imageTable = [_imageTables objectForKey:formatName]; + NSString *entityUUID = [entity UUID]; + + [imageTable deleteEntryForEntityUUID:entityUUID]; + [imageTable saveMetadata]; +} + +- (void)reset { + for (FICImageTable *imageTable in [_imageTables allValues]) { + [imageTable reset]; + } +} + +#pragma mark - Logging Errors + +- (void)_logMessage:(NSString *)message { + if (_delegateImplementsErrorDidOccurWithMessage) { + [_delegate imageCache:self errorDidOccurWithMessage:message]; + } +} + +@end diff --git a/FastImageCache/FICImageFormat.h b/FastImageCache/FICImageFormat.h new file mode 100644 index 0000000..c4e422d --- /dev/null +++ b/FastImageCache/FICImageFormat.h @@ -0,0 +1,108 @@ +// +// FICImageFormat.h +// FastImageCache +// +// Copyright (c) 2013 Path, Inc. +// See LICENSE for full license agreement. +// + +#import "FICImports.h" + +@class FICImageTable; + +typedef NS_OPTIONS(NSUInteger, FICImageFormatDevices) { + FICImageFormatDevicePhone = 1 << UIUserInterfaceIdiomPhone, + FICImageFormatDevicePad = 1 << UIUserInterfaceIdiomPad, +}; + +/** + `FICImageFormat` acts as a definition for the types of images that are stored in the image cache. Each image format must have a unique name, but multiple formats can belong to the same family. + All images associated with a particular format must have the same image dimentions and opacity preference. You can define the maximum number of entries that an image format can accommodate to + prevent the image cache from consuming too much disk space. Each `` managed by the image cache is associated with a single image format. + */ +@interface FICImageFormat : NSObject + +///------------------------------ +/// @name Image Format Properties +///------------------------------ + +/** + The name of the image format. Each image format must have a unique name. + */ +@property (nonatomic, copy) NSString *name; + +/** + The optional family that the image format belongs to. Families group together related image formats. + + @discussion If you are using the image cache to create several different cached variants of the same source image, all of those variants would be unique image formats that share the same family. + + For example, you might define a `userPhoto` family that groups together image formats with the following names: `userPhotoSmallThumbnail`, `userPhotoLargeThumbnail`, `userPhotoLargeThumbnailBorder`. + Ideally, the same source image can be processed to create cached image data for every image format belonging to the same family. + + `` provides its delegate a chance to process all image formats in a given family at the same time when a particular entity-image format pair is being processed. This allows you to process + a source image once instead of having to download and process the same source image multiple times for different formats in the same family. + + @see [FICImageCacheDelegate imageCache:shouldProcessAllFormatsInFamily:forEntity:] + */ +@property (nonatomic, copy) NSString *family; + +/** + The size, in points, of the images stored in the image table created by this format. + */ +@property (nonatomic, assign) CGSize imageSize; + +/** + The size, in pixels, of the images stored in the image table created by this format. This takes into account the screen scale. + */ +@property (nonatomic, assign, readonly) CGSize pixelSize; + +/** + Whether or not the bitmap of the image table created by this format needs to include an alpha channel. + */ +@property (nonatomic, assign, getter = isOpaque) BOOL opaque; + +/** + The maximum number of entries that an image table can contain for this image format. + + @discussion Images inserted into the image table defined by this image format after the maximum number of entries has been exceeded will replace the least-recently accessed entry. + */ +@property (nonatomic, assign) NSInteger maximumCount; + +/** + A bitmask of type `` that defines which devices are managed by an image table. + + @discussion If the current device is not included in a particular image format, the image cache will not store image data for that device. + */ +@property (nonatomic, assign) FICImageFormatDevices devices; + +/** + The dictionary representation of this image format. + + @discussion Fast Image Cache automatically serializes the image formats that it uses to disk. If an image format ever changes, Fast Image Cache automatically detects the change and invalidates the image table associated with that image format. The image table is then recreated from the updated image format. + */ +@property (nonatomic, copy, readonly) NSDictionary *dictionaryRepresentation; + +///----------------------------------- +/// @name Initializing an Image Format +///----------------------------------- + +/** + Convenience initializer to create a new image format. + + @param name The name of the image format. Each image format must have a unique name. + + @param family The optional family that the image format belongs to. See the `` property description for more information. + + @param imageSize The size, in points, of the images stored in the image table created by this format. + + @param isOpaque Whether or not the image table's backing bitmap data provider is opaque. + + @param maximumCount The maximum number of entries that an image table can contain for this image format. + + @param devices A bitmask of type `` that defines which devices are managed by an image table. + + @return An autoreleased instance of `` or one of its subclasses, if any exist. + */ ++ (instancetype)formatWithName:(NSString *)name family:(NSString *)family imageSize:(CGSize)imageSize isOpaque:(BOOL)isOpaque maximumCount:(NSInteger)maximumCount devices:(FICImageFormatDevices)devices; + +@end diff --git a/FastImageCache/FICImageFormat.m b/FastImageCache/FICImageFormat.m new file mode 100644 index 0000000..ac8c7d9 --- /dev/null +++ b/FastImageCache/FICImageFormat.m @@ -0,0 +1,118 @@ +// +// FICImageFormat.m +// FastImageCache +// +// Copyright (c) 2013 Path, Inc. +// See LICENSE for full license agreement. +// + +#import "FICImageFormat.h" +#import "FICImageTable.h" +#import "FICImageTableEntry.h" + +#pragma mark Internal Definitions + +static NSString *const FICImageFormatNameKey = @"name"; +static NSString *const FICImageFormatFamilyKey = @"family"; +static NSString *const FICImageFormatWidthKey = @"width"; +static NSString *const FICImageFormatHeightKey = @"height"; +static NSString *const FICImageFormatIsOpaqueKey = @"isOpaque"; +static NSString *const FICImageFormatMaximumCountKey = @"maximumCount"; +static NSString *const FICImageFormatDevicesKey = @"devices"; + +#pragma mark - Class Extension + +@interface FICImageFormat () { + NSString *_name; + NSString *_family; + CGSize _imageSize; + CGSize _pixelSize; + BOOL _isOpaque; + NSInteger _maximumCount; + FICImageFormatDevices _devices; +} + +@end + +#pragma mark + +@implementation FICImageFormat + +@synthesize name = _name; +@synthesize family = _family; +@synthesize imageSize = _imageSize; +@synthesize pixelSize = _pixelSize; +@synthesize opaque = _isOpaque; +@synthesize maximumCount = _maximumCount; +@synthesize devices = _devices; + +#pragma mark - Property Accessors + +- (void)setImageSize:(CGSize)imageSize { + BOOL currentSizeEqualToNewSize = CGSizeEqualToSize(imageSize, _imageSize); + if (currentSizeEqualToNewSize == NO) { + _imageSize = imageSize; + + CGFloat screenScale = [[UIScreen mainScreen] scale]; + _pixelSize = CGSizeMake(screenScale * _imageSize.width, screenScale * _imageSize.height); + } +} + +#pragma mark - Object Lifecycle + ++ (instancetype)formatWithName:(NSString *)name family:(NSString *)family imageSize:(CGSize)imageSize isOpaque:(BOOL)isOpaque maximumCount:(NSInteger)maximumCount devices:(FICImageFormatDevices)devices { + FICImageFormat *imageFormat = [[[FICImageFormat alloc] init] autorelease]; + + [imageFormat setName:name]; + [imageFormat setFamily:family]; + [imageFormat setImageSize:imageSize]; + [imageFormat setOpaque:isOpaque]; + [imageFormat setMaximumCount:maximumCount]; + [imageFormat setDevices:devices]; + + return imageFormat; +} + +- (void)dealloc { + [_name release]; + [_family release]; + + [super dealloc]; +} + +#pragma mark - Working with Dictionary Representations + +- (NSDictionary *)dictionaryRepresentation { + NSMutableDictionary *dictionaryRepresentation = [NSMutableDictionary dictionary]; + + [dictionaryRepresentation setValue:_name forKey:FICImageFormatNameKey]; + [dictionaryRepresentation setValue:_family forKey:FICImageFormatFamilyKey]; + [dictionaryRepresentation setValue:[NSNumber numberWithUnsignedInteger:_imageSize.width] forKey:FICImageFormatWidthKey]; + [dictionaryRepresentation setValue:[NSNumber numberWithUnsignedInteger:_imageSize.height] forKey:FICImageFormatHeightKey]; + [dictionaryRepresentation setValue:[NSNumber numberWithBool:_isOpaque] forKey:FICImageFormatIsOpaqueKey]; + [dictionaryRepresentation setValue:[NSNumber numberWithUnsignedInteger:_maximumCount] forKey:FICImageFormatMaximumCountKey]; + [dictionaryRepresentation setValue:[NSNumber numberWithInt:_devices] forKey:FICImageFormatDevicesKey]; + [dictionaryRepresentation setValue:[NSNumber numberWithFloat:[[UIScreen mainScreen] scale]] forKey:FICImageTableScreenScaleKey]; + [dictionaryRepresentation setValue:[NSNumber numberWithUnsignedInteger:[FICImageTableEntry metadataVersion]] forKey:FICImageTableEntryDataVersionKey]; + + return dictionaryRepresentation; +} + +#pragma mark - Protocol Implementations + +#pragma mark - NSObject (NSCopying) + +- (id)copyWithZone:(NSZone *)zone { + FICImageFormat *imageFormatCopy = [[FICImageFormat alloc] init]; + + [imageFormatCopy setName:[self name]]; + [imageFormatCopy setFamily:[self family]]; + [imageFormatCopy setImageSize:[self imageSize]]; + [imageFormatCopy setOpaque:[self isOpaque]]; + [imageFormatCopy setMaximumCount:[self maximumCount]]; + [imageFormatCopy setDevices:[self devices]]; + + return imageFormatCopy; +} + +@end diff --git a/FastImageCache/FICImageTable.h b/FastImageCache/FICImageTable.h new file mode 100644 index 0000000..d9bcbc4 --- /dev/null +++ b/FastImageCache/FICImageTable.h @@ -0,0 +1,176 @@ +// +// FICImageTable.h +// FastImageCache +// +// Copyright (c) 2013 Path, Inc. +// See LICENSE for full license agreement. +// + +#import "FICImports.h" +#import "FICImageCache.h" +#import "FICEntity.h" + +@class FICImageFormat; +@class FICImageTableChunk; +@class FICImageTableEntry; +@class FICImage; + +extern NSString *const FICImageTableEntryDataVersionKey; +extern NSString *const FICImageTableScreenScaleKey; + +/** + `FICImageTable` is the primary class that efficiently stores and retrieves cached image data. Image tables are defined by instances of ``. Each image table is backed by a single + file on disk that sequentially stores image entry data. All images in an image table are either opaque or not and have the same dimensions. Therefore, when defining your image formats, keep in + mind that you cannot mix image dimensions or whether or not an image is opaque. + */ +@interface FICImageTable : NSObject + +///----------------------------- +/// @name Image Table Properties +///----------------------------- + +/** + The file system path where the image table's data file is located. + */ +@property (nonatomic, copy, readonly) NSString *tableFilePath; + +/** + The file system path where the image table's metadata file is located. + */ +@property (nonatomic, copy, readonly) NSString *metadataFilePath; + +/** + The image format that describes the image table. + */ +@property (nonatomic, retain, readonly) FICImageFormat *imageFormat; + +///----------------------------------------------- +/// @name Accessing Information about Image Tables +///----------------------------------------------- + +/** + Returns the page size for the current device. + + @return The number of bytes in a page of memory. + + @discussion This class method calls the UNIX function `getpagesize()` exactly once, storing the result in a static local variable. + */ ++ (int)pageSize; + +/** + Returns the file system path for the directory that stores image table files. + + @return The string representing the file system directory path where image table files are stored. + + @warning Image table files are stored in the user's caches directory, so you should be prepared for the image tables to be deleted from the file system at any time. + */ ++ (NSString *)directoryPath; + +///---------------------------------- +/// @name Initializing an Image Table +///---------------------------------- + +/** + Initializes a new image table described by the provided image format. + + @param imageFormat The image format that describes the image table. + + @return A new image table. + + @warning `FICImageTable` raises an exception if `imageFormat` is `nil`. `FICImageTable`'s implementation of `-init` simply calls through to this initializer, passing `nil` for `imageFormat`. + */ +- (instancetype)initWithFormat:(FICImageFormat *)imageFormat; + +///------------------------------------------------ +/// @name Storing, Retrieving, and Deleting Entries +///------------------------------------------------ + +/** + Stores new image entry data in the image table. + + @param entityUUID The UUID of the entity that uniquely identifies an image table entry. Must not be `nil`. + + @param sourceImageUUID The UUID of the source image that represents the actual image data stored in an image table entry. Must not be `nil`. + + @param imageDrawingBlock The drawing block provided by the entity that actually draws the source image into a bitmap context. Must not be `nil`. + + @discussion Objects conforming to `` are responsible for providing an image drawing block that does the actual drawing of their source images to a bitmap context provided + by the image table. Drawing in the provided bitmap context writes the uncompressed image data directly to the image table file on disk. + + @note If any of the parameters to this method are `nil`, this method does nothing. + + @see [FICEntity drawingBlockForImage:withFormatName:] + */ +- (void)setEntryForEntityUUID:(NSString *)entityUUID sourceImageUUID:(NSString *)sourceImageUUID imageDrawingBlock:(FICEntityImageDrawingBlock)imageDrawingBlock; + +/** + Returns a new image from the image entry data in the image table. + + @param entityUUID The UUID of the entity that uniquely identifies an image table entry. Must not be `nil`. + + @param sourceImageUUID The UUID of the source image that represents the actual image data stored in an image table entry. Must not be `nil`. + + @return A new image created from the entry data stored in the image table or `nil` if something went wrong. + + @discussion The `UIImage` returned by this method is initialized by a `CGImageRef` backed directly by mapped file data, so no memory copy occurs. + + @note If either of the parameters to this method are `nil`, the return value is `nil`. + + @note If either the entity UUID or the source image UUID doesn't match the corresponding UUIDs in the entry data, then something has changed. The entry data is deleted for the + provided entity UUID, and `nil` is returned. + */ +- (UIImage *)newImageForEntityUUID:(NSString *)entityUUID sourceImageUUID:(NSString *)sourceImageUUID; + +/** + Deletes image entry data in the image table. + + @param entityUUID The UUID of the entity that uniquely identifies an image table entry. Must not be `nil`. + + @note If `entityUUID` is `nil`, this method does nothing. + */ +- (void)deleteEntryForEntityUUID:(NSString *)entityUUID; + +///----------------------------------- +/// @name Checking for Entry Existence +///----------------------------------- + +/** + Returns whether or not an entry exists in the image table. + + @param entityUUID The UUID of the entity that uniquely identifies an image table entry. Must not be `nil`. + + @param sourceImageUUID The UUID of the source image that represents the actual image data stored in an image table entry. Must not be `nil`. + + @return `YES` if an entry exists in the image table for the provided entity UUID and source image UUID. Otherwise, `NO`. + + @note If either of the parameters to this method are `nil`, the return value is `NO`. + + @note If either the entity UUID or the source image UUID doesn't match the corresponding UUIDs in the entry data, then something has changed. The entry data is deleted for the + provided entity UUID, and `NO` is returned. + */ +- (BOOL)entryExistsForEntityUUID:(NSString *)entityUUID sourceImageUUID:(NSString *)sourceImageUUID; + +///--------------------------------------- +/// @name Saving an Image Table's Metadata +///--------------------------------------- + +/** + Saves the image table's metadata to disk. + + @discussion `FICImageTable` objects load their metadata from disk when they are initialized. If the deserialized dictionary representation for an image table's image format does not + match the current dictionary representation, then the image table is invalid. The image table is deleted and a new one is created from the modified image format. + + @see [FICImageFormat dictionaryRepresentation] + */ +- (void)saveMetadata; + +///-------------------------------- +/// @name Resetting the Image Table +///-------------------------------- + +/** + Resets the image table by deleting all its data and metadata. + */ +- (void)reset; + +@end diff --git a/FastImageCache/FICImageTable.m b/FastImageCache/FICImageTable.m new file mode 100644 index 0000000..261113d --- /dev/null +++ b/FastImageCache/FICImageTable.m @@ -0,0 +1,608 @@ +// +// FICImageTable.m +// FastImageCache +// +// Copyright (c) 2013 Path, Inc. +// See LICENSE for full license agreement. +// + +#import "FICImageTable.h" +#import "FICImageFormat.h" +#import "FICImageCache.h" +#import "FICImageTableChunk.h" +#import "FICImageTableEntry.h" +#import "FICUtilities.h" + +#import "FICImageCache+FICErrorLogging.h" + +#pragma mark External Definitions + +NSString *const FICImageTableEntryDataVersionKey = @"FICImageTableEntryDataVersionKey"; +NSString *const FICImageTableScreenScaleKey = @"FICImageTableScreenScaleKey"; + +#pragma mark - Internal Definitions + +static NSString *const FICImageTableMetadataFileExtension = @"metadata"; +static NSString *const FICImageTableFileExtension = @"imageTable"; + +static NSString *const FICImageTableIndexMapKey = @"indexMap"; +static NSString *const FICImageTableContextMapKey = @"contextMap"; +static NSString *const FICImageTableMRUArrayKey = @"mruArray"; +static NSString *const FICImageTableFormatKey = @"format"; + +#pragma mark - Class Extension + +@interface FICImageTable () { + FICImageFormat *_imageFormat; + CGFloat _screenScale; + NSInteger _imageRowLength; + + NSString *_filePath; + int _fileDescriptor; + off_t _fileLength; + + NSUInteger _entryCount; + off_t _entryLength; + NSUInteger _entriesPerChunk; + off_t _imageLength; + + size_t _chunkLength; + NSInteger _chunkCount; + + CFMutableDictionaryRef _chunkDictionary; + NSMutableArray *_recentChunks; + NSRecursiveLock *_lock; + + // Image table metadata + NSMutableDictionary *_indexMap; // Key: entity UUID, value: integer index into the table file + NSMutableDictionary *_sourceImageMap; // Key: entity UUID, value: source image UUID + NSMutableIndexSet *_occupiedIndexes; + NSMutableArray *_MRUEntries; + NSDictionary *_imageFormatDictionary; +} + +@end + +#pragma mark + +@implementation FICImageTable + +@synthesize imageFormat =_imageFormat; + +#pragma mark - Property Accessors (Public) + +- (NSString *)tableFilePath { + NSString *tableFilePath = [[_imageFormat name] stringByAppendingPathExtension:FICImageTableFileExtension]; + tableFilePath = [[FICImageTable directoryPath] stringByAppendingPathComponent:tableFilePath]; + + return tableFilePath; +} + +- (NSString *)metadataFilePath { + NSString *metadataFilePath = [[_imageFormat name] stringByAppendingPathExtension:FICImageTableMetadataFileExtension]; + metadataFilePath = [[FICImageTable directoryPath] stringByAppendingPathComponent:metadataFilePath]; + + return metadataFilePath; +} + +#pragma mark - Class-Level Definitions + ++ (int)pageSize { + static int __pageSize = 0; + + static dispatch_once_t onceToken; + dispatch_once(&onceToken, ^{ + __pageSize = getpagesize(); + }); + + return __pageSize; +} + ++ (NSString *)directoryPath { + static NSString *__directoryPath = nil; + + static dispatch_once_t onceToken; + dispatch_once(&onceToken, ^{ + NSArray *paths = NSSearchPathForDirectoriesInDomains(NSCachesDirectory, NSUserDomainMask, YES); + __directoryPath = [[[paths objectAtIndex:0] stringByAppendingPathComponent:@"ImageTables"] retain]; + + NSFileManager *fileManager = [[[NSFileManager alloc] init] autorelease]; + BOOL directoryExists = [fileManager fileExistsAtPath:__directoryPath]; + if (directoryExists == NO) { + [fileManager createDirectoryAtPath:__directoryPath withIntermediateDirectories:YES attributes:nil error:nil]; + } + }); + + return __directoryPath; +} + +#pragma mark - Object Lifecycle + +- (instancetype)initWithFormat:(FICImageFormat *)imageFormat { + self = [super init]; + + if (self != nil) { + if (imageFormat == nil) { + [NSException raise:NSInvalidArgumentException format:@"*** FIC Exception: %s must pass in an image format.", __PRETTY_FUNCTION__]; + } + + _lock = [[NSRecursiveLock alloc] init]; + _imageFormat = [imageFormat copy]; + _imageFormatDictionary = [[imageFormat dictionaryRepresentation] retain]; + + _screenScale = [[UIScreen mainScreen] scale]; + + int bytesPerPixel = 4; + _imageRowLength = FICByteAlignForCoreAnimation([_imageFormat pixelSize].width * bytesPerPixel, bytesPerPixel); + _imageLength = _imageRowLength * (NSInteger)[_imageFormat pixelSize].height; + + _chunkDictionary = CFDictionaryCreateMutable(kCFAllocatorDefault, 0, NULL, NULL); // Non-retained keys and values + + _indexMap = [[NSMutableDictionary alloc] init]; + _occupiedIndexes = [[NSMutableIndexSet alloc] init]; + + _MRUEntries = [[NSMutableArray alloc] init]; + _sourceImageMap = [[NSMutableDictionary alloc] init]; + + _recentChunks = [[NSMutableArray alloc] init]; + + _filePath = [[self tableFilePath] copy]; + + [self _loadMetadata]; + + _fileDescriptor = open([_filePath fileSystemRepresentation], O_RDWR | O_CREAT, 0666); + + if (_fileDescriptor >= 0) { + // The size of each entry in the table needs to be page-aligned. This will cause each entry to have a page-aligned base + // address, which will help us avoid Core Animation having to copy our images when we eventually set them on layers. + _entryLength = FICByteAlign(_imageLength + sizeof(FICImageTableEntryMetadata), [FICImageTable pageSize]); + + // Each chunk will map in n entries. Try to keep the chunkLength around 2MB. + NSInteger goalChunkLength = 2 * (1024 * 1024); + NSInteger goalEntriesPerChunk = goalChunkLength / _entryLength; + _entriesPerChunk = MAX(4, goalEntriesPerChunk); + _chunkLength = _entryLength * _entriesPerChunk; + + _fileLength = lseek(_fileDescriptor, 0, SEEK_END); + _entryCount = _fileLength / _entryLength; + _chunkCount = (_entryCount + _entriesPerChunk - 1) / _entriesPerChunk; + + if ([_indexMap count] > _entryCount) { + // It's possible that someone deleted the image table file but left behind the metadata file. If this happens, the metadata + // will obviously become out of sync with the image table file, so we need to reset the image table. + [self reset]; + } + } else { + // If something goes wrong and we can't open the image table file, then we have no choice but to release and nil self. + NSString *message = [NSString stringWithFormat:@"*** FIC Error: %s could not open the image table file at path %@. The image table was not created.", __PRETTY_FUNCTION__, _filePath]; + [[FICImageCache sharedImageCache] _logMessage:message]; + + [self release]; + self = nil; + } + } + + return self; +} + +- (instancetype)init { + return [self initWithFormat:nil]; +} + +- (void)dealloc { + [_imageFormat release]; + [_filePath release]; + + CFRelease(_chunkDictionary); + + [_indexMap release]; + [_occupiedIndexes release]; + [_MRUEntries release]; + [_sourceImageMap release]; + [_imageFormatDictionary release]; + [_recentChunks release]; + + if (_fileDescriptor >= 0) { + close(_fileDescriptor); + } + + [_lock release]; + + [super dealloc]; +} + +#pragma mark - Working with Chunks + +- (FICImageTableChunk *)_cachedChunkAtIndex:(NSInteger)index { + return (FICImageTableChunk *)CFDictionaryGetValue(_chunkDictionary, (const void *)index); +} + +- (void)_setChunk:(FICImageTableChunk *)chunk index:(NSInteger)index { + if (chunk != nil) { + CFDictionarySetValue(_chunkDictionary, (const void *)index, (const void *)chunk); + } else { + CFDictionaryRemoveValue(_chunkDictionary, (const void *)index); + } +} + +- (void)_cleanupRecentChunks { + [_lock lock]; + + [_recentChunks removeAllObjects]; + + [_lock unlock]; +} + +- (FICImageTableChunk *)_chunkAtIndex:(NSInteger)index { + FICImageTableChunk *chunk = nil; + + if (index < _chunkCount) { + chunk = [[self _cachedChunkAtIndex:index] retain]; + + if (chunk == nil) { + size_t chunkLength = _chunkLength; + off_t chunkOffset = index * (off_t)_chunkLength; + if (chunkOffset + chunkLength > _fileLength) { + chunkLength = _fileLength - chunkOffset; + } + + chunk = [[FICImageTableChunk alloc] initWithImageTable:self fileDescriptor:_fileDescriptor index:index length:chunkLength]; + [self _setChunk:chunk index:index]; + } + + if (chunk != nil) { + static const NSInteger __recentChunksToKeepMapped = 2; + [_recentChunks insertObject:chunk atIndex:0]; + + if ([_recentChunks count] > __recentChunksToKeepMapped) { + [_recentChunks removeLastObject]; + } + } + } + + return [chunk autorelease]; +} + +- (void)_chunkWillBeDeallocated:(FICImageTableChunk *)chunk { + [_lock lock]; + + [self _setChunk:nil index:[chunk index]]; + + [_lock unlock]; +} + +#pragma mark - Storing, Retrieving, and Deleting Entries + +- (void)setEntryForEntityUUID:(NSString *)entityUUID sourceImageUUID:(NSString *)sourceImageUUID imageDrawingBlock:(FICEntityImageDrawingBlock)imageDrawingBlock { + if (entityUUID != nil && sourceImageUUID != nil && imageDrawingBlock != NULL) { + [_lock lock]; + + NSInteger newEntryIndex = [self _indexOfEntryForEntityUUID:entityUUID]; + if (newEntryIndex == NSNotFound) { + newEntryIndex = [self _nextEntryIndex]; + + if (newEntryIndex >= _entryCount) { + NSInteger maximumEntryCount = [_imageFormat maximumCount]; + NSInteger newEntryCount = MIN(maximumEntryCount, _entryCount + MAX(_entriesPerChunk, newEntryIndex - _entryCount + 1)); + [self _setEntryCount:newEntryCount]; + } + } + + if (newEntryIndex < _entryCount) { + CGSize pixelSize = [_imageFormat pixelSize]; + CGBitmapInfo bitmapInfo; + if ([_imageFormat isOpaque]) { + bitmapInfo = kCGImageAlphaNoneSkipFirst | kCGBitmapByteOrder32Host; + } else { + bitmapInfo = kCGImageAlphaPremultipliedFirst | kCGBitmapByteOrder32Host; + } + + CGColorSpaceRef colorSpace = CGColorSpaceCreateDeviceRGB(); + + // Create context whose backing store *is* the mapped file data + FICImageTableEntry *entryData = [self _entryDataAtIndex:newEntryIndex]; + CGContextRef context = CGBitmapContextCreate([entryData bytes], pixelSize.width, pixelSize.height, 8, _imageRowLength, colorSpace, bitmapInfo); + CGColorSpaceRelease(colorSpace); + + CGContextTranslateCTM(context, 0, [_imageFormat pixelSize].height); + CGContextScaleCTM(context, _screenScale, -_screenScale); + + // Call drawing block to allow client to draw into the context + imageDrawingBlock(context, [_imageFormat imageSize]); + CGContextRelease(context); + + [entryData setEntityUUIDBytes:FICUUIDBytesWithString(entityUUID)]; + [entryData setSourceImageUUIDBytes:FICUUIDBytesWithString(sourceImageUUID)]; + + // Update our book-keeping + [_indexMap setObject:[NSNumber numberWithUnsignedInteger:newEntryIndex] forKey:entityUUID]; + [_occupiedIndexes addIndex:newEntryIndex]; + [_sourceImageMap setObject:sourceImageUUID forKey:entityUUID]; + + // Update MRU array + [self _entryWasAccessedWithEntityUUID:entityUUID]; + [self saveMetadata]; + + // Write the data back to the filesystem + [entryData flush]; + } + + [_lock unlock]; + } +} + +- (UIImage *)newImageForEntityUUID:(NSString *)entityUUID sourceImageUUID:(NSString *)sourceImageUUID { + UIImage *image = nil; + + if (entityUUID != nil && sourceImageUUID != nil) { + [_lock lock]; + + FICImageTableEntry *entryData = [self _entryDataForEntityUUID:entityUUID]; + if (entryData != nil) { + NSString *entryEntityUUID = FICStringWithUUIDBytes([entryData entityUUIDBytes]); + NSString *entrySourceImageUUID = FICStringWithUUIDBytes([entryData sourceImageUUIDBytes]); + BOOL entityUUIDIsCorrect = entityUUID == nil || [entityUUID isEqualToString:entryEntityUUID]; + BOOL sourceImageUUIDIsCorrect = sourceImageUUID == nil || [sourceImageUUID isEqualToString:entrySourceImageUUID]; + + if (entityUUIDIsCorrect == NO || sourceImageUUIDIsCorrect == NO) { + // The UUIDs don't match, so we need to invalidate the entry. + [self deleteEntryForEntityUUID:entityUUID]; + [self saveMetadata]; + } else { + [self _entryWasAccessedWithEntityUUID:entityUUID]; + + [entryData retain]; // Released by _FICReleaseImageData + + // Create CGImageRef whose backing store *is* the mapped image table entry. We avoid a memcpy this way. + CGColorSpaceRef colorSpace = CGColorSpaceCreateDeviceRGB(); + CGDataProviderRef dataProvider = CGDataProviderCreateWithData((void *)entryData, [entryData bytes], [entryData imageLength], _FICReleaseImageData); + + CGSize pixelSize = [_imageFormat pixelSize]; + CGBitmapInfo bitmapInfo; + if ([_imageFormat isOpaque]) { + bitmapInfo = kCGImageAlphaNoneSkipFirst | kCGBitmapByteOrder32Host; + } else { + bitmapInfo = kCGImageAlphaPremultipliedFirst | kCGBitmapByteOrder32Host; + } + + CGImageRef imageRef = CGImageCreate(pixelSize.width, pixelSize.height, 8, 32, _imageRowLength, colorSpace, bitmapInfo, dataProvider, NULL, false, (CGColorRenderingIntent)0); + CGDataProviderRelease(dataProvider); + CGColorSpaceRelease(colorSpace); + + if (imageRef != NULL) { + image = [[UIImage alloc] initWithCGImage:imageRef scale:_screenScale orientation:UIImageOrientationUp]; + CGImageRelease(imageRef); + } else { + NSString *message = [NSString stringWithFormat:@"*** FIC Error: %s could not create a new CGImageRef for entity UUID %@.", __PRETTY_FUNCTION__, entityUUID]; + [[FICImageCache sharedImageCache] _logMessage:message]; + } + } + } + + [_lock unlock]; + } + + return image; +} + +static void _FICReleaseImageData(void *info, const void *data, size_t size) { + FICImageTableEntry *entryData = (FICImageTableEntry *)info; + [entryData release]; +} + +- (void)deleteEntryForEntityUUID:(NSString *)entityUUID { + if (entityUUID != nil) { + [_lock lock]; + + NSInteger index = [self _indexOfEntryForEntityUUID:entityUUID]; + if (index != NSNotFound) { + [_sourceImageMap removeObjectForKey:entityUUID]; + [_indexMap removeObjectForKey:entityUUID]; + [_occupiedIndexes removeIndex:index]; + NSInteger index = [_MRUEntries indexOfObject:entityUUID]; + if (index != NSNotFound) { + [_MRUEntries removeObjectAtIndex:index]; + } + } + + [_lock unlock]; + } +} + +#pragma mark - Checking for Entry Existence + +- (BOOL)entryExistsForEntityUUID:(NSString *)entityUUID sourceImageUUID:(NSString *)sourceImageUUID { + BOOL imageExists = NO; + + [_lock lock]; + + FICImageTableEntry *entryData = [self _entryDataForEntityUUID:entityUUID]; + if (entryData != nil && sourceImageUUID != nil) { + NSString *existingEntityUUID = FICStringWithUUIDBytes([entryData entityUUIDBytes]); + BOOL entityUUIDIsCorrect = [entityUUID isEqualToString:existingEntityUUID]; + + NSString *existingSourceImageUUID = FICStringWithUUIDBytes([entryData sourceImageUUIDBytes]); + BOOL sourceImageUUIDIsCorrect = [sourceImageUUID isEqualToString:existingSourceImageUUID]; + + if (entityUUIDIsCorrect == NO || sourceImageUUIDIsCorrect == NO) { + // The source image UUIDs don't match, so the image data should be deleted for this entity. + [self deleteEntryForEntityUUID:entityUUID]; + [self saveMetadata]; + entryData = nil; + } + } + + [_lock unlock]; + + imageExists = entryData != nil; + + return imageExists; +} + +#pragma mark - Working with Entries + +- (void)_setEntryCount:(NSInteger)entryCount { + if (entryCount != _entryCount) { + off_t fileLength = entryCount * _entryLength; + int result = ftruncate(_fileDescriptor, fileLength); + + if (result != 0) { + NSString *message = [NSString stringWithFormat:@"*** FIC Error: %s ftruncate returned %d, error = %d, fd = %d, filePath = %@, length = %lld", __PRETTY_FUNCTION__, result, errno, _fileDescriptor, _filePath, fileLength]; + [[FICImageCache sharedImageCache] _logMessage:message]; + } else { + _fileLength = fileLength; + _entryCount = entryCount; + _chunkCount = (_entryCount + _entriesPerChunk - 1) / _entriesPerChunk; + } + } +} + +- (FICImageTableEntry *)_entryDataAtIndex:(NSInteger)index { + FICImageTableEntry *entryData = nil; + + [_lock lock]; + + if (index < _entryCount) { + off_t entryOffset = index * _entryLength; + size_t chunkIndex = entryOffset / _chunkLength; + + FICImageTableChunk *chunk = [self _chunkAtIndex:chunkIndex]; + if (chunk != nil) { + off_t chunkOffset = chunkIndex * _chunkLength; + off_t entryOffsetInChunk = entryOffset - chunkOffset; + void *mappedChunkAddress = [chunk bytes]; + void *mappedEntryAddress = mappedChunkAddress + entryOffsetInChunk; + entryData = [[FICImageTableEntry alloc] initWithImageTableChunk:chunk bytes:mappedEntryAddress length:_entryLength]; + } + } + + [_lock unlock]; + + return [entryData autorelease]; +} + +- (NSInteger)_nextEntryIndex { + NSMutableIndexSet *unoccupiedIndexes = [[NSMutableIndexSet alloc] initWithIndexesInRange:NSMakeRange(0, _entryCount)]; + [unoccupiedIndexes removeIndexes:_occupiedIndexes]; + + NSInteger index = [unoccupiedIndexes firstIndex]; + if (index == NSNotFound) { + index = _entryCount; + } + [unoccupiedIndexes release]; + + if (index >= [_imageFormat maximumCount] && [_MRUEntries count]) { + // Evict the oldest/least-recently accessed entry here + [self deleteEntryForEntityUUID:[_MRUEntries lastObject]]; + index = [self _nextEntryIndex]; + } + + return index; +} + +- (NSInteger)_indexOfEntryForEntityUUID:(NSString *)entityUUID { + NSInteger index = NSNotFound; + if (_indexMap != nil && entityUUID != nil) { + NSNumber *indexNumber = [_indexMap objectForKey:entityUUID]; + index = indexNumber ? [indexNumber integerValue] : NSNotFound; + + if (index != NSNotFound && index >= _entryCount) { + [_indexMap removeObjectForKey:entityUUID]; + [_occupiedIndexes removeIndex:index]; + [_sourceImageMap removeObjectForKey:entityUUID]; + index = NSNotFound; + } + } + + return index; +} + +- (FICImageTableEntry *)_entryDataForEntityUUID:(NSString *)entityUUID { + FICImageTableEntry *entryData = nil; + NSInteger index = [self _indexOfEntryForEntityUUID:entityUUID]; + if (index != NSNotFound) { + entryData = [self _entryDataAtIndex:index]; + } + + return entryData; +} + +- (void)_entryWasAccessedWithEntityUUID:(NSString *)entityUUID { + // Update MRU array + NSInteger index = [_MRUEntries indexOfObject:entityUUID]; + if (index == NSNotFound) { + [_MRUEntries insertObject:entityUUID atIndex:0]; + } else if (index != 0) { + [entityUUID retain]; + [_MRUEntries removeObjectAtIndex:index]; + [_MRUEntries insertObject:entityUUID atIndex:0]; + [entityUUID release]; + } +} + +#pragma mark - Working with Metadata + +- (void)saveMetadata { + [_lock lock]; + + NSDictionary *metadataDictionary = [NSDictionary dictionaryWithObjectsAndKeys: + _indexMap, FICImageTableIndexMapKey, + _sourceImageMap, FICImageTableContextMapKey, + _MRUEntries, FICImageTableMRUArrayKey, + _imageFormatDictionary, FICImageTableFormatKey, nil]; + + NSData *data = [NSPropertyListSerialization dataWithPropertyList:metadataDictionary format:NSPropertyListBinaryFormat_v1_0 options:0 error:NULL]; + BOOL fileWriteResult = [data writeToFile:[self metadataFilePath] atomically:NO]; + if (fileWriteResult == NO) { + NSString *message = [NSString stringWithFormat:@"*** FIC Error: %s couldn't write metadata for format %@", __PRETTY_FUNCTION__, [_imageFormat name]]; + [[FICImageCache sharedImageCache] _logMessage:message]; + } + + [_lock unlock]; +} + +- (void)_loadMetadata { + NSString *metadataFilePath = [[_filePath stringByDeletingPathExtension] stringByAppendingPathExtension:FICImageTableMetadataFileExtension]; + NSData *metadataData = [NSData dataWithContentsOfMappedFile:metadataFilePath]; + if (metadataData != nil) { + NSDictionary *metadataDictionary = (NSDictionary *)[NSPropertyListSerialization propertyListWithData:metadataData options:0 format:NULL error:NULL]; + NSDictionary *formatDictionary = [metadataDictionary objectForKey:FICImageTableFormatKey]; + if ([formatDictionary isEqualToDictionary:_imageFormatDictionary] == NO) { + // Something about this image format has changed, so the existing metadata is no longer valid. The image table file + // must be deleted and recreated. + [[NSFileManager defaultManager] removeItemAtPath:_filePath error:NULL]; + [[NSFileManager defaultManager] removeItemAtPath:metadataFilePath error:NULL]; + metadataDictionary = nil; + + NSString *message = [NSString stringWithFormat:@"*** FIC Notice: Image format %@ has changed; deleting data and starting over.", [_imageFormat name]]; + [[FICImageCache sharedImageCache] _logMessage:message]; + } + + [_indexMap setDictionary:[metadataDictionary objectForKey:FICImageTableIndexMapKey]]; + + for (NSNumber *index in [_indexMap allValues]) { + [_occupiedIndexes addIndex:[index intValue]]; + } + + [_sourceImageMap setDictionary:[metadataDictionary objectForKey:FICImageTableContextMapKey]]; + [_MRUEntries setArray:[metadataDictionary objectForKey:FICImageTableMRUArrayKey]]; + } +} + +#pragma mark - Resetting the Image Table + +- (void)reset { + [_lock lock]; + + [_indexMap removeAllObjects]; + [_occupiedIndexes removeAllIndexes]; + [_MRUEntries removeAllObjects]; + [_sourceImageMap removeAllObjects]; + + [self _setEntryCount:0]; + [self saveMetadata]; + + [_lock unlock]; +} + +@end diff --git a/FastImageCache/FICImageTableChunk.h b/FastImageCache/FICImageTableChunk.h new file mode 100644 index 0000000..b93f6c1 --- /dev/null +++ b/FastImageCache/FICImageTableChunk.h @@ -0,0 +1,58 @@ +// +// FICImageTableChunk.h +// FastImageCache +// +// Copyright (c) 2013 Path, Inc. +// See LICENSE for full license agreement. +// + +#import "FICImports.h" + +@class FICImageTable; + +/** + `FICImageTableChunk` represents a contiguous portion of image table file data. + */ +@interface FICImageTableChunk : NSObject + +///----------------------------------- +/// @name Image Table Chunk Properties +///----------------------------------- + +/** + The bytes of file data contained in the chunk. + + @discussion `FICImageTableChunk` maps file data directly to `bytes`, so no memory copy occurs. + */ +@property (nonatomic, assign, readonly) void *bytes; + +/** + The index of the chunk in the image table file. + */ +@property (nonatomic, assign, readonly) NSInteger index; + +/** + The offset in the image table file where the chunk begins. + */ +@property (nonatomic, assign, readonly) off_t fileOffset; + +///---------------------------------------- +/// @name Initializing an Image Table Chunk +///---------------------------------------- + +/** + Initializes a new image table chunk. + + @param imageTable The image table to create a chunk from. + + @param fileDescriptor The image table's file descriptor to map from. + + @param index The index of the chunk. + + @param length The length, in bytes, of the chunk. + + @return A new image table chunk. + */ +- (instancetype)initWithImageTable:(FICImageTable *)imageTable fileDescriptor:(int)fileDescriptor index:(NSInteger)index length:(size_t)length; + +@end \ No newline at end of file diff --git a/FastImageCache/FICImageTableChunk.m b/FastImageCache/FICImageTableChunk.m new file mode 100644 index 0000000..66feed0 --- /dev/null +++ b/FastImageCache/FICImageTableChunk.m @@ -0,0 +1,81 @@ +// +// FICImageTableChunk.m +// FastImageCache +// +// Copyright (c) 2013 Path, Inc. +// See LICENSE for full license agreement. +// + +#import "FICImageTableChunk.h" +#import "FICImageTable.h" + +#import + +#pragma mark FICImageTable (FICImageTableChunkAdditions) + +@interface FICImageTable (FICImageTableChunkAdditions) + +- (void)_chunkWillBeDeallocated:(FICImageTableChunk *)chunk; + +@end + +#pragma mark - Class Extension + +@interface FICImageTableChunk () { + FICImageTable *_owningImageTable; + NSInteger _index; + void *_bytes; + size_t _length; + off_t _fileOffset; +} + +@end + +#pragma mark + +@implementation FICImageTableChunk + +@synthesize bytes = _bytes; +@synthesize fileOffset = _fileOffset; + +#pragma mark - Object Lifecycle + +- (id)initWithImageTable:(FICImageTable *)imageTable fileDescriptor:(int)fileDescriptor index:(NSInteger)index length:(size_t)length { + self = [super init]; + + if (self != nil) { + _owningImageTable = [imageTable retain]; + _index = index; + _length = length; + _fileOffset = _index * _length; + _bytes = mmap(NULL, _length, (PROT_READ|PROT_WRITE), (MAP_FILE|MAP_SHARED), fileDescriptor, _fileOffset); + + if (_bytes == MAP_FAILED) { + _bytes = NULL; + } + } + + return self; +} + +- (void)dealloc { + [_owningImageTable release]; + + if (_bytes != NULL) { + munmap(_bytes, _length); + } + + [super dealloc]; +} + +- (oneway void)release { + // While it is good practice to never access retainCount, in this case, it is necessary. This is the only way + // to know that self will soon be deallocated prior to the start of execution of the dealloc method. + if ([self retainCount] == 1) { + [_owningImageTable _chunkWillBeDeallocated:self]; + } + + [super release]; +} + +@end diff --git a/FastImageCache/FICImageTableEntry.h b/FastImageCache/FICImageTableEntry.h new file mode 100644 index 0000000..9febbe8 --- /dev/null +++ b/FastImageCache/FICImageTableEntry.h @@ -0,0 +1,94 @@ +// +// FICImageTableEntry.h +// FastImageCache +// +// Copyright (c) 2013 Path, Inc. +// See LICENSE for full license agreement. +// + +#import "FICImports.h" + +@class FICImageTableChunk; + +typedef struct { + CFUUIDBytes _entityUUIDBytes; + CFUUIDBytes _sourceImageUUIDBytes; +} FICImageTableEntryMetadata; + +/** + `FICImageTableEntry` represents an entry in an image table. It contains the necessary data and metadata to store a single entry of image data. Entries are created from instances of + ``. + */ +@interface FICImageTableEntry : NSObject + +///--------------------------------------------- +/// @name Accessing Image Table Entry Properties +///--------------------------------------------- + +/** + The length, in bytes, of the entry data. + + @discussion Entries begin with the image data, followed by the metadata struct. + */ +@property (nonatomic, assign, readonly) size_t length; + +/** + The length, in bytes, of just the image data. + */ +@property (nonatomic, assign, readonly) size_t imageLength; + +/** + The bytes that represent the entry data. + */ +@property (nonatomic, assign, readonly) void *bytes; + +/** + The entity UUID, in byte form, associated with the entry. + */ +@property (nonatomic, assign) CFUUIDBytes entityUUIDBytes; + +/** + The source image UUID, in byte form, associated with the entry. + */ +@property (nonatomic, assign) CFUUIDBytes sourceImageUUIDBytes; + +///---------------------------------------- +/// @name Initializing an Image Table Entry +///---------------------------------------- + +/** + Initializes a new image table entry from an image table chunk. + + @param imageTableChunk The image table chunk that contains the entry data. + + @param bytes The bytes from the chunk that contain the entry data. + + @param length The length, in bytes, of the entry data. + + @return A new image table entry. + */ +- (instancetype)initWithImageTableChunk:(FICImageTableChunk *)imageTableChunk bytes:(void *)bytes length:(size_t)length; + +///-------------------------------------------- +/// @name Flushing a Modified Image Table Entry +///-------------------------------------------- + +/** + Writes a modified image table entry back to disk. + */ +- (void)flush; + +///-------------------------------------------- +/// @name Versioning Image Table Entry Metadata +///-------------------------------------------- + +/** + Returns the current metadata version for image table entries. + + @return The integer version number of the current metadata version. + + @discussion Whenever the `` struct is changed in any way, the metadata version must be changed. + */ ++ (NSInteger)metadataVersion; + +@end diff --git a/FastImageCache/FICImageTableEntry.m b/FastImageCache/FICImageTableEntry.m new file mode 100644 index 0000000..e271dd4 --- /dev/null +++ b/FastImageCache/FICImageTableEntry.m @@ -0,0 +1,104 @@ +// +// FICImageTableEntry.m +// FastImageCache +// +// Copyright (c) 2013 Path, Inc. +// See LICENSE for full license agreement. +// + +#import "FICImageTableEntry.h" +#import "FICImageTable.h" +#import "FICImageTableChunk.h" +#import "FICImageCache.h" + +#import "FICImageCache+FICErrorLogging.h" + +#import + +#pragma mark Class Extension + +@interface FICImageTableEntry () { + FICImageTableChunk *_imageTableChunk; + void *_bytes; + size_t _length; +} + +@end + +#pragma mark + +@implementation FICImageTableEntry + +@synthesize bytes = _bytes; +@synthesize length = _length; + +#pragma mark - Property Accessors + +- (size_t)imageLength { + return _length - sizeof(FICImageTableEntryMetadata); +} + +- (CFUUIDBytes)entityUUIDBytes { + return [self _metadata]->_entityUUIDBytes; +} + +- (void)setEntityUUIDBytes:(CFUUIDBytes)entityUUIDBytes { + [self _metadata]->_entityUUIDBytes = entityUUIDBytes; +} + +- (CFUUIDBytes)sourceImageUUIDBytes { + return [self _metadata]->_sourceImageUUIDBytes; +} + +- (void)setSourceImageUUIDBytes:(CFUUIDBytes)sourceImageUUIDBytes { + [self _metadata]->_sourceImageUUIDBytes = sourceImageUUIDBytes; +} + +#pragma mark - Object Lifecycle + +- (id)initWithImageTableChunk:(FICImageTableChunk *)imageTableChunk bytes:(void *)bytes length:(size_t)length { + self = [super init]; + + if (self != nil) { + _imageTableChunk = [imageTableChunk retain]; + _bytes = bytes; + _length = length; + } + + return self; +} + +- (void)dealloc { + [_imageTableChunk release]; + + [super dealloc]; +} + +#pragma mark - Other Accessors + ++ (NSInteger)metadataVersion { + return 7; +} + +- (FICImageTableEntryMetadata *)_metadata { + return (FICImageTableEntryMetadata *)(_bytes + [self imageLength]); +} + +#pragma mark - Flushing a Modified Image Table Entry + +- (void)flush { + int pageSize = [FICImageTable pageSize]; + void *address = _bytes; + size_t pageIndex = (size_t)address / pageSize; + void *pageAlignedAddress = (void *)(pageIndex * pageSize); + size_t bytesBeforeData = address - pageAlignedAddress; + size_t bytesToFlush = (bytesBeforeData + _length); + int result = msync(pageAlignedAddress, bytesToFlush, MS_SYNC); + + if (result) { + NSString *message = [NSString stringWithFormat:@"*** FIC Error: %s msync(%p, %ld) returned %d errno=%d", __PRETTY_FUNCTION__, pageAlignedAddress, bytesToFlush, result, errno]; + [[FICImageCache sharedImageCache] _logMessage:message]; + } +} + +@end diff --git a/FastImageCache/FICImports.h b/FastImageCache/FICImports.h new file mode 100644 index 0000000..5a917f5 --- /dev/null +++ b/FastImageCache/FICImports.h @@ -0,0 +1,11 @@ +// +// FICImports.h +// FastImageCache +// +// Copyright (c) 2013 Path, Inc. +// See LICENSE for full license agreement. +// + +#import +#import +#import diff --git a/FastImageCache/FICUtilities.h b/FastImageCache/FICUtilities.h new file mode 100644 index 0000000..156ee43 --- /dev/null +++ b/FastImageCache/FICUtilities.h @@ -0,0 +1,17 @@ +// +// FICUtilities.h +// FastImageCache +// +// Copyright (c) 2013 Path, Inc. +// See LICENSE for full license agreement. +// + +#import "FICImports.h" + +size_t FICByteAlign(size_t bytesPerRow, size_t alignment); +size_t FICByteAlignForCoreAnimation(size_t bytesPerRow, size_t bytesPerPixel); + +NSString * FICStringWithUUIDBytes(CFUUIDBytes UUIDBytes); +CFUUIDBytes FICUUIDBytesWithString(NSString *string); +CFUUIDBytes FICUUIDBytesFromMD5HashOfString(NSString *MD5Hash); // Useful for computing an entity's UUID from a URL, for example + diff --git a/FastImageCache/FICUtilities.m b/FastImageCache/FICUtilities.m new file mode 100644 index 0000000..010f414 --- /dev/null +++ b/FastImageCache/FICUtilities.m @@ -0,0 +1,64 @@ +// +// FICUtilities.m +// FastImageCache +// +// Copyright (c) 2013 Path, Inc. +// See LICENSE for full license agreement. +// + +#import "FICUtilities.h" + +#import + +#pragma mark Internal Definitions + +#define FIC_CA_BACKING_STORE_ALIGNMENT_PIXELS 8 +// Core Animation will make a copy of any image that a client application provides whose backing store isn't properly byte-aligned. +// This copy operation can be prohibitively expensive, so we want to avoid this by properly aligning any UIImages we're working with. +// To produce a UIImage that is properly aligned, we need to ensure that the backing store's bytes per row is a multiple of +// (bytes per pixel * FIC_CA_BACKING_STORE_ALIGNMENT_PIXELS). + +#pragma mark - Byte Alignment + +inline size_t FICByteAlign(size_t width, size_t alignment) { + return ((width + (alignment - 1)) / alignment) * alignment; +} + +inline size_t FICByteAlignForCoreAnimation(size_t bytesPerRow, size_t bytesPerPixel) { + return FICByteAlign(bytesPerRow, (bytesPerPixel * FIC_CA_BACKING_STORE_ALIGNMENT_PIXELS)); +} + +#pragma mark - Strings and UUIDs + +NSString * FICStringWithUUIDBytes(CFUUIDBytes UUIDBytes) { + NSString *UUIDString = nil; + CFUUIDRef UUIDRef = CFUUIDCreateFromUUIDBytes(kCFAllocatorDefault, UUIDBytes); + + if (UUIDRef != NULL) { + UUIDString = (NSString *)CFUUIDCreateString(kCFAllocatorDefault, UUIDRef); + CFRelease(UUIDRef); + } + + return [UUIDString autorelease]; +} + +CFUUIDBytes FICUUIDBytesWithString(NSString *string) { + CFUUIDBytes UUIDBytes; + CFUUIDRef UUIDRef = CFUUIDCreateFromString(kCFAllocatorDefault, (CFStringRef)string); + + if (UUIDRef != NULL) { + UUIDBytes = CFUUIDGetUUIDBytes(UUIDRef); + CFRelease(UUIDRef); + } + + return UUIDBytes; +} + +CFUUIDBytes FICUUIDBytesFromMD5HashOfString(NSString *MD5Hash) { + const char *UTF8String = [MD5Hash UTF8String]; + CFUUIDBytes UUIDBytes; + + CC_MD5(UTF8String, strlen(UTF8String), (unsigned char*)&UUIDBytes); + + return UUIDBytes; +} diff --git a/FastImageCacheDemo/Classes/FICDAppDelegate.h b/FastImageCacheDemo/Classes/FICDAppDelegate.h new file mode 100644 index 0000000..19e3ee8 --- /dev/null +++ b/FastImageCacheDemo/Classes/FICDAppDelegate.h @@ -0,0 +1,16 @@ +// +// FICDAppDelegate.h +// FastImageCacheDemo +// +// Copyright (c) 2013 Path, Inc. +// See LICENSE for full license agreement. +// + +@class FICDViewController; + +@interface FICDAppDelegate : UIResponder + +@property (nonatomic, retain) UIWindow *window; +@property (nonatomic, retain) FICDViewController *viewController; + +@end diff --git a/FastImageCacheDemo/Classes/FICDAppDelegate.m b/FastImageCacheDemo/Classes/FICDAppDelegate.m new file mode 100644 index 0000000..d837d11 --- /dev/null +++ b/FastImageCacheDemo/Classes/FICDAppDelegate.m @@ -0,0 +1,102 @@ +// +// FICDAppDelegate.m +// FastImageCacheDemo +// +// Copyright (c) 2013 Path, Inc. +// See LICENSE for full license agreement. +// + +#import "FICDAppDelegate.h" +#import "FICImageCache.h" +#import "FICDViewController.h" +#import "FICDPhoto.h" + +#pragma mark Class Extension + +@interface FICDAppDelegate () + +@end + +#pragma mark + +@implementation FICDAppDelegate + +#pragma mark - Object Lifeycle + +- (void)dealloc { + [_window release]; + [_viewController release]; + + [super dealloc]; +} + +#pragma mark - Application Lifecycle + +- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions { + NSMutableArray *mutableImageFormats = [NSMutableArray array]; + + // Square image format + NSInteger squareImageFormatMaximumCount = 400; + FICImageFormatDevices squareImageFormatDevices = FICImageFormatDevicePhone | FICImageFormatDevicePad; + + FICImageFormat *squareImageFormat = [FICImageFormat formatWithName:FICDPhotoSquareImageFormatName family:FICDPhotoImageFormatFamily imageSize:FICDPhotoSquareImageSize isOpaque:NO + maximumCount:squareImageFormatMaximumCount devices:squareImageFormatDevices]; + + [mutableImageFormats addObject:squareImageFormat]; + + if ([UIViewController instancesRespondToSelector:@selector(preferredStatusBarStyle)]) { + // Pixel image format + NSInteger pixelImageFormatMaximumCount = 1000; + FICImageFormatDevices pixelImageFormatDevices = FICImageFormatDevicePhone | FICImageFormatDevicePad; + + FICImageFormat *pixelImageFormat = [FICImageFormat formatWithName:FICDPhotoPixelImageFormatName family:FICDPhotoImageFormatFamily imageSize:FICDPhotoPixelImageSize isOpaque:YES + maximumCount:pixelImageFormatMaximumCount devices:pixelImageFormatDevices]; + + [mutableImageFormats addObject:pixelImageFormat]; + } + + // Configure the image cache + FICImageCache *sharedImageCache = [FICImageCache sharedImageCache]; + [sharedImageCache setDelegate:self]; + [sharedImageCache setFormats:mutableImageFormats]; + + // Configure the window + CGRect windowFrame = [[UIScreen mainScreen] bounds]; + UIWindow *window = [[[UIWindow alloc] initWithFrame:windowFrame] autorelease]; + [self setWindow:window]; + + UIViewController *rootViewController = [[[FICDViewController alloc] init] autorelease]; + UINavigationController *navigationController = [[[UINavigationController alloc] initWithRootViewController:rootViewController] autorelease]; + + [[self window] setRootViewController:navigationController]; + [[self window] makeKeyAndVisible]; + + return YES; +} + +#pragma mark - Protocol Implementations + +#pragma mark - FICImageCacheDelegate + +- (void)imageCache:(FICImageCache *)imageCache wantsSourceImageForEntity:(id)entity withFormatName:(NSString *)formatName completionBlock:(FICImageRequestCompletionBlock)completionBlock { + // Images typically come from the Internet rather than from the app bundle directly, so this would be the place to fire off a network request to download the image. + // For the purposes of this demo app, we'll just access images stored locally on disk. + dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{ + UIImage *sourceImage = [(FICDPhoto *)entity sourceImage]; + dispatch_async(dispatch_get_main_queue(), ^{ + completionBlock(sourceImage); + }); + }); +} + +- (BOOL)imageCache:(FICImageCache *)imageCache shouldProcessAllFormatsInFamily:(NSString *)formatFamily forEntity:(id)entity { + BOOL shouldProcessAllFormats = [formatFamily isEqualToString:FICDPhotoImageFormatFamily]; + + return shouldProcessAllFormats; +} + +- (void)imageCache:(FICImageCache *)imageCache errorDidOccurWithMessage:(NSString *)errorMessage { + NSLog(@"%@", errorMessage); +} + +@end diff --git a/FastImageCacheDemo/Classes/FICDFullscreenPhotoDisplayController.h b/FastImageCacheDemo/Classes/FICDFullscreenPhotoDisplayController.h new file mode 100644 index 0000000..2cc8711 --- /dev/null +++ b/FastImageCacheDemo/Classes/FICDFullscreenPhotoDisplayController.h @@ -0,0 +1,34 @@ +// +// FICDFullscreenPhotoDisplayController.h +// FastImageCacheDemo +// +// Copyright (c) 2013 Path, Inc. +// See LICENSE for full license agreement. +// + +@class FICDPhoto; + +@protocol FICDFullscreenPhotoDisplayControllerDelegate; + +@interface FICDFullscreenPhotoDisplayController : NSObject + +@property (nonatomic, assign) id delegate; + ++ (instancetype)sharedDisplayController; + +@property (nonatomic, assign, readonly, getter = isDisplayingPhoto) BOOL displayingPhoto; + +- (void)showFullscreenPhoto:(FICDPhoto *)photo withThumbnailImageView:(UIImageView *)thumbnailImageView; +- (void)hideFullscreenPhoto; + +@end + +@protocol FICDFullscreenPhotoDisplayControllerDelegate + +@optional +- (void)photoDisplayController:(FICDFullscreenPhotoDisplayController *)photoDisplayController willShowSourceImage:(UIImage *)sourceImage forPhoto:(FICDPhoto *)photo withThumbnailImageView:(UIImageView *)thumbnailImageView; +- (void)photoDisplayController:(FICDFullscreenPhotoDisplayController *)photoDisplayController didShowSourceImage:(UIImage *)sourceImage forPhoto:(FICDPhoto *)photo withThumbnailImageView:(UIImageView *)thumbnailImageView; +- (void)photoDisplayController:(FICDFullscreenPhotoDisplayController *)photoDisplayController willHideSourceImage:(UIImage *)sourceImage forPhoto:(FICDPhoto *)photo withThumbnailImageView:(UIImageView *)thumbnailImageView; +- (void)photoDisplayController:(FICDFullscreenPhotoDisplayController *)photoDisplayController didHideSourceImage:(UIImage *)sourceImage forPhoto:(FICDPhoto *)photo withThumbnailImageView:(UIImageView *)thumbnailImageView; + +@end diff --git a/FastImageCacheDemo/Classes/FICDFullscreenPhotoDisplayController.m b/FastImageCacheDemo/Classes/FICDFullscreenPhotoDisplayController.m new file mode 100644 index 0000000..45dbd3e --- /dev/null +++ b/FastImageCacheDemo/Classes/FICDFullscreenPhotoDisplayController.m @@ -0,0 +1,213 @@ +// +// FICDFullscreenPhotoDisplayController.m +// FastImageCacheDemo +// +// Copyright (c) 2013 Path, Inc. +// See LICENSE for full license agreement. +// + +#import "FICDFullscreenPhotoDisplayController.h" +#import "FICDPhoto.h" + +#pragma mark Class Extension + +@interface FICDFullscreenPhotoDisplayController () { + id _delegate; + BOOL _delegateImplementsWillShowSourceImageForPhotoWithThumbnailImageView; + BOOL _delegateImplementsDidShowSourceImageForPhotoWithThumbnailImageView; + BOOL _delegateImplementsWillHideSourceImageForPhotoWithThumbnailImageView; + BOOL _delegateImplementsDidHideSourceImageForPhotoWithThumbnailImageView; + + UIView *_fullscreenView; + UIView *_backgroundView; + + UIImageView *_thumbnailImageView; + CGRect _originalThumbnailImageViewFrame; + NSUInteger _originalThumbnailImageViewSubviewIndex; + UIView *_originalThumbnailImageViewSuperview; + + UIImageView *_sourceImageView; + FICDPhoto *_photo; + + UITapGestureRecognizer *_tapGestureRecognizer; +} + +@end + +#pragma mark + +@implementation FICDFullscreenPhotoDisplayController + +@synthesize delegate = _delegate; + +#pragma mark - Property Accessors + +- (void)setDelegate:(id)delegate { + _delegate = delegate; + + _delegateImplementsWillShowSourceImageForPhotoWithThumbnailImageView = [_delegate respondsToSelector:@selector(photoDisplayController:willShowSourceImage:forPhoto:withThumbnailImageView:)]; + _delegateImplementsDidShowSourceImageForPhotoWithThumbnailImageView = [_delegate respondsToSelector:@selector(photoDisplayController:didShowSourceImage:forPhoto:withThumbnailImageView:)]; + _delegateImplementsWillHideSourceImageForPhotoWithThumbnailImageView = [_delegate respondsToSelector:@selector(photoDisplayController:willHideSourceImage:forPhoto:withThumbnailImageView:)]; + _delegateImplementsDidHideSourceImageForPhotoWithThumbnailImageView = [_delegate respondsToSelector:@selector(photoDisplayController:didHideSourceImage:forPhoto:withThumbnailImageView:)]; +} + +- (BOOL)isDisplayingPhoto { + return _photo != nil; +} + +#pragma mark - Object Lifecycle + ++ (instancetype)sharedDisplayController { + static FICDFullscreenPhotoDisplayController *__sharedDisplayController = nil; + + if (__sharedDisplayController == nil) { + static dispatch_once_t onceToken; + dispatch_once(&onceToken, ^{ + __sharedDisplayController = [[[self class] alloc] init]; + }); + } + + return __sharedDisplayController; +} + +- (id)init { + self = [super init]; + + if (self != nil) { + UIViewAutoresizing autoresizingMask = (UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleHeight); + + _fullscreenView = [[UIView alloc] init]; + [_fullscreenView setAutoresizingMask:autoresizingMask]; + + _backgroundView = [[UIView alloc] init]; + [_backgroundView setAutoresizingMask:autoresizingMask]; + [_backgroundView setBackgroundColor:[UIColor colorWithWhite:0 alpha:0.8]]; + + _sourceImageView = [[UIImageView alloc] init]; + [_sourceImageView setAutoresizingMask:autoresizingMask]; + [_sourceImageView setContentMode:UIViewContentModeScaleAspectFill]; + [_sourceImageView setClipsToBounds:YES]; + + _tapGestureRecognizer = [[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(_tapGestureRecognizerStateDidChange)]; + [_fullscreenView addGestureRecognizer:_tapGestureRecognizer]; + } + + return self; +} + +- (void)dealloc { + [_fullscreenView release]; + [_backgroundView release]; + [_thumbnailImageView release]; + [_originalThumbnailImageViewSuperview release]; + [_sourceImageView release]; + [_photo release]; + + [_tapGestureRecognizer setDelegate:nil]; + [_tapGestureRecognizer release]; + + [super dealloc]; +} + +#pragma mark - Showing and Hiding a Fullscreen Photo + +- (void)showFullscreenPhoto:(FICDPhoto *)photo withThumbnailImageView:(UIImageView *)thumbnailImageView { + // Stash away the photo + _photo = [photo retain]; + + // Stash away original thumbnail image view information + _thumbnailImageView = [thumbnailImageView retain]; + _originalThumbnailImageViewSuperview = [[thumbnailImageView superview] retain]; + _originalThumbnailImageViewFrame = [thumbnailImageView frame]; + _originalThumbnailImageViewSubviewIndex = [[[thumbnailImageView superview] subviews] indexOfObject:thumbnailImageView]; + + // Configure the fullscreen view + UIView *rootViewControllerView = [[[[UIApplication sharedApplication] keyWindow] rootViewController] view]; + [_fullscreenView setFrame:[rootViewControllerView bounds]]; + [rootViewControllerView addSubview:_fullscreenView]; + + // Configure the background view + [_backgroundView setFrame:[_fullscreenView bounds]]; + [_backgroundView setAlpha:0]; + [_fullscreenView addSubview:_backgroundView]; + + // Configure the thumbnail image view + CGRect convertedThumbnailImageViewFrame = [_originalThumbnailImageViewSuperview convertRect:_originalThumbnailImageViewFrame toView:_fullscreenView]; + [_thumbnailImageView setFrame:convertedThumbnailImageViewFrame]; + [_fullscreenView addSubview:_thumbnailImageView]; + + // Configure the source image view + UIImage *sourceImage = [photo sourceImage]; + [_sourceImageView setImage:sourceImage]; + [_sourceImageView setFrame:convertedThumbnailImageViewFrame]; + [_sourceImageView setAlpha:0]; + [_fullscreenView addSubview:_sourceImageView]; + + // Inform the delegate that we're about to show a fullscreen photo + if (_delegateImplementsWillShowSourceImageForPhotoWithThumbnailImageView) { + [_delegate photoDisplayController:self willShowSourceImage:sourceImage forPhoto:_photo withThumbnailImageView:_thumbnailImageView]; + } + + // Animate fullscreen photo appearance + [UIView animateWithDuration:0.3 animations:^{ + [_backgroundView setAlpha:1]; + [_thumbnailImageView setFrame:[_fullscreenView bounds]]; + [_sourceImageView setAlpha:1]; + [_sourceImageView setFrame:[_fullscreenView bounds]]; + } completion:^(BOOL finished) { + // Inform the delegate that we just showed a fullscreen photo + if (_delegateImplementsDidShowSourceImageForPhotoWithThumbnailImageView) { + [_delegate photoDisplayController:self didShowSourceImage:sourceImage forPhoto:_photo withThumbnailImageView:_thumbnailImageView]; + } + }]; +} + +- (void)hideFullscreenPhoto { + UIImage *sourceImage = [_sourceImageView image]; + // Inform the delegate that we're about to hide a fullscreen photo + if (_delegateImplementsWillHideSourceImageForPhotoWithThumbnailImageView) { + [_delegate photoDisplayController:self willHideSourceImage:sourceImage forPhoto:_photo withThumbnailImageView:_thumbnailImageView]; + } + + CGRect convertedThumbnailImageViewFrame = [_originalThumbnailImageViewSuperview convertRect:_originalThumbnailImageViewFrame toView:_fullscreenView]; + + // Animate fullscreen photo appearance + [UIView animateWithDuration:0.3 animations:^{ + [_backgroundView setAlpha:0]; + [_thumbnailImageView setFrame:convertedThumbnailImageViewFrame]; + [_sourceImageView setAlpha:0]; + [_sourceImageView setFrame:convertedThumbnailImageViewFrame]; + } completion:^(BOOL finished) { + [_thumbnailImageView setFrame:_originalThumbnailImageViewFrame]; + [_originalThumbnailImageViewSuperview insertSubview:_thumbnailImageView atIndex:_originalThumbnailImageViewSubviewIndex]; + + [_fullscreenView removeFromSuperview]; + + // Clean up photo ownership + [_photo release]; + _photo = nil; + + // Clean up thumbnail image view ownership + [_thumbnailImageView release]; + _thumbnailImageView = nil; + + [_originalThumbnailImageViewSuperview release]; + _originalThumbnailImageViewSuperview = nil; + + _originalThumbnailImageViewFrame = CGRectZero; + _originalThumbnailImageViewSubviewIndex = 0; + + // Inform the delegate that we just hide a fullscreen photo + if (_delegateImplementsDidHideSourceImageForPhotoWithThumbnailImageView) { + [_delegate photoDisplayController:self didHideSourceImage:sourceImage forPhoto:_photo withThumbnailImageView:_thumbnailImageView]; + } + }]; +} + +- (void)_tapGestureRecognizerStateDidChange { + if ([_tapGestureRecognizer state] == UIGestureRecognizerStateEnded) { + [self hideFullscreenPhoto]; + } +} + +@end diff --git a/FastImageCacheDemo/Classes/FICDPhoto.h b/FastImageCacheDemo/Classes/FICDPhoto.h new file mode 100644 index 0000000..14fb7d9 --- /dev/null +++ b/FastImageCacheDemo/Classes/FICDPhoto.h @@ -0,0 +1,30 @@ +// +// FICDPhoto.h +// FastImageCacheDemo +// +// Copyright (c) 2013 Path, Inc. +// See LICENSE for full license agreement. +// + +#import "FICEntity.h" + +extern NSString *const FICDPhotoImageFormatFamily; + +extern NSString *const FICDPhotoSquareImageFormatName; +extern NSString *const FICDPhotoPixelImageFormatName; + +extern CGSize const FICDPhotoSquareImageSize; +extern CGSize const FICDPhotoPixelImageSize; + +@interface FICDPhoto : NSObject + +@property (nonatomic, copy) NSURL *sourceImageURL; +@property (nonatomic, retain, readonly) UIImage *sourceImage; +@property (nonatomic, retain, readonly) UIImage *thumbnailImage; +@property (nonatomic, assign, readonly) BOOL thumbnailImageExists; + +// Methods for demonstrating more conventional caching techniques +- (void)generateThumbnail; +- (void)deleteThumbnail; + +@end diff --git a/FastImageCacheDemo/Classes/FICDPhoto.m b/FastImageCacheDemo/Classes/FICDPhoto.m new file mode 100644 index 0000000..a08ef42 --- /dev/null +++ b/FastImageCacheDemo/Classes/FICDPhoto.m @@ -0,0 +1,225 @@ +// +// FICDPhoto.m +// FastImageCacheDemo +// +// Copyright (c) 2013 Path, Inc. +// See LICENSE for full license agreement. +// + +#import "FICDPhoto.h" +#import "FICUtilities.h" + +#pragma mark External Definitions + +NSString *const FICDPhotoImageFormatFamily = @"FICDPhotoImageFormatFamily"; + +NSString *const FICDPhotoSquareImageFormatName = @"FICDPhotoSquareImageFormatName"; +NSString *const FICDPhotoPixelImageFormatName = @"FICDPhotoPixelImageFormatName"; + +CGSize const FICDPhotoSquareImageSize = {75, 75}; +CGSize const FICDPhotoPixelImageSize = {1, 1}; + +#pragma mark - Class Extension + +@interface FICDPhoto () { + NSURL *_sourceImageURL; + NSString *_UUID; + NSString *_thumbnailFilePath; + BOOL _thumbnailFileExists; + BOOL _didCheckForThumbnailFile; +} + +@end + +#pragma mark + +@implementation FICDPhoto + +@synthesize sourceImageURL = _sourceImageURL; + +#pragma mark - Property Accessors + +- (UIImage *)sourceImage { + UIImage *sourceImage = [UIImage imageWithContentsOfFile:[_sourceImageURL path]]; + + return sourceImage; +} + +- (UIImage *)thumbnailImage { + UIImage *thumbnailImage = [UIImage imageWithContentsOfFile:[self _thumbnailFilePath]]; + + return thumbnailImage; +} + +- (BOOL)thumbnailImageExists { + BOOL thumbnailImageExists = [[NSFileManager defaultManager] fileExistsAtPath:[self _thumbnailFilePath]]; + + return thumbnailImageExists; +} + +#pragma mark - Object Lifecycle + +- (void)dealloc { + [_sourceImageURL release]; + [_UUID release]; + [_thumbnailFilePath release]; + + [super dealloc]; +} + +#pragma mark - Image Helper Functions + +static CGMutablePathRef _FICDCreateRoundedRectPath(CGRect rect, CGFloat cornerRadius) { + CGMutablePathRef path = CGPathCreateMutable(); + + CGFloat minX = CGRectGetMinX(rect); + CGFloat midX = CGRectGetMidX(rect); + CGFloat maxX = CGRectGetMaxX(rect); + CGFloat minY = CGRectGetMinY(rect); + CGFloat midY = CGRectGetMidY(rect); + CGFloat maxY = CGRectGetMaxY(rect); + + CGPathMoveToPoint(path, NULL, minX, midY); + CGPathAddArcToPoint(path, NULL, minX, maxY, midX, maxY, cornerRadius); + CGPathAddArcToPoint(path, NULL, maxX, maxY, maxX, midY, cornerRadius); + CGPathAddArcToPoint(path, NULL, maxX, minY, midX, minY, cornerRadius); + CGPathAddArcToPoint(path, NULL, minX, minY, minX, midY, cornerRadius); + + return path; +} + +static UIImage * _FICDSquareImageFromImage(UIImage *image) { + UIImage *squareImage = nil; + CGSize imageSize = [image size]; + + if (imageSize.width == imageSize.height) { + squareImage = image; + } else { + // Compute square crop rect + CGFloat smallerDimension = MIN(imageSize.width, imageSize.height); + CGRect cropRect = CGRectMake(0, 0, smallerDimension, smallerDimension); + + // Center the crop rect either vertically or horizontally, depending on which dimension is smaller + if (imageSize.width <= imageSize.height) { + cropRect.origin = CGPointMake(0, rintf((imageSize.height - smallerDimension) / 2.0)); + } else { + cropRect.origin = CGPointMake(rintf((imageSize.width - smallerDimension) / 2.0), 0); + } + + CGImageRef croppedImageRef = CGImageCreateWithImageInRect([image CGImage], cropRect); + squareImage = [UIImage imageWithCGImage:croppedImageRef]; + CGImageRelease(croppedImageRef); + } + + return squareImage; +} + +static UIImage * _FICDStatusBarImageFromImage(UIImage *image) { + CGSize imageSize = [image size]; + CGSize statusBarSize = [[UIApplication sharedApplication] statusBarFrame].size; + CGRect cropRect = CGRectMake(0, 0, imageSize.width, statusBarSize.height); + + CGImageRef croppedImageRef = CGImageCreateWithImageInRect([image CGImage], cropRect); + UIImage *statusBarImage = [UIImage imageWithCGImage:croppedImageRef]; + CGImageRelease(croppedImageRef); + + return statusBarImage; +} + +#pragma mark - Conventional Image Caching Techniques + +- (NSString *)_thumbnailFilePath { + if (!_thumbnailFilePath) { + NSURL *photoURL = [self sourceImageURL]; + _thumbnailFilePath = [[NSTemporaryDirectory() stringByAppendingPathComponent:[[photoURL absoluteString] lastPathComponent]] retain]; + } + return _thumbnailFilePath; +} + +- (void)generateThumbnail { + NSString *thumbnailFilePath = [self _thumbnailFilePath]; + if (!_didCheckForThumbnailFile) { + _didCheckForThumbnailFile = YES; + _thumbnailFileExists = [[NSFileManager defaultManager] fileExistsAtPath:thumbnailFilePath]; + } + + if (_thumbnailFileExists == NO) { + CGFloat screenScale = [[UIScreen mainScreen] scale]; + CGRect contextBounds = CGRectZero; + contextBounds.size = CGSizeMake(FICDPhotoSquareImageSize.width * screenScale, FICDPhotoSquareImageSize.height * screenScale); + + UIImage *sourceImage = [self sourceImage]; + UIImage *squareImage = _FICDSquareImageFromImage(sourceImage); + + UIGraphicsBeginImageContext(contextBounds.size); + + [squareImage drawInRect:contextBounds]; + UIImage *scaledImage = UIGraphicsGetImageFromCurrentImageContext(); + NSData *scaledImageJPEGRepresentation = UIImageJPEGRepresentation(scaledImage, 0.8); + + [scaledImageJPEGRepresentation writeToFile:thumbnailFilePath atomically:NO]; + + UIGraphicsEndImageContext(); + _thumbnailFileExists = YES; + } +} + +- (void)deleteThumbnail { + [[NSFileManager defaultManager] removeItemAtPath:[self _thumbnailFilePath] error:NULL]; + _thumbnailFileExists = NO; +} + +#pragma mark - Protocol Implementations + +#pragma mark - FICImageCacheEntity + +- (NSString *)UUID { + if (_UUID == nil) { + // MD5 hashing is expensive enough that we only want to do it once + CFUUIDBytes UUIDBytes = FICUUIDBytesFromMD5HashOfString([_sourceImageURL absoluteString]); + _UUID = [FICStringWithUUIDBytes(UUIDBytes) retain]; + } + + return _UUID; +} + +- (NSString *)sourceImageUUID { + return [self UUID]; +} + +- (NSURL *)sourceImageURLWithFormatName:(NSString *)formatName { + return _sourceImageURL; +} + +- (FICEntityImageDrawingBlock)drawingBlockForImage:(UIImage *)image withFormatName:(NSString *)formatName { + FICEntityImageDrawingBlock drawingBlock = ^(CGContextRef contextRef, CGSize contextSize) { + CGRect contextBounds = CGRectZero; + contextBounds.size = contextSize; + CGContextClearRect(contextRef, contextBounds); + + if ([formatName isEqualToString:FICDPhotoSquareImageFormatName]) { + UIImage *squareImage = _FICDSquareImageFromImage(image); + + // Clip to a rounded rect + CGPathRef path = _FICDCreateRoundedRectPath(contextBounds, 12); + CGContextAddPath(contextRef, path); + CFRelease(path); + CGContextEOClip(contextRef); + + UIGraphicsPushContext(contextRef); + [squareImage drawInRect:contextBounds]; + UIGraphicsPopContext(); + } else if ([formatName isEqualToString:FICDPhotoPixelImageFormatName]) { + UIImage *statusBarImage = _FICDStatusBarImageFromImage(image); + CGContextSetInterpolationQuality(contextRef, kCGInterpolationMedium); + + UIGraphicsPushContext(contextRef); + [statusBarImage drawInRect:contextBounds]; + UIGraphicsPopContext(); + } + }; + + return [[drawingBlock copy] autorelease]; +} + +@end diff --git a/FastImageCacheDemo/Classes/FICDPhotosTableViewCell.h b/FastImageCacheDemo/Classes/FICDPhotosTableViewCell.h new file mode 100644 index 0000000..06d582e --- /dev/null +++ b/FastImageCacheDemo/Classes/FICDPhotosTableViewCell.h @@ -0,0 +1,31 @@ +// +// FICDPhotosTableViewCell.h +// FastImageCacheDemo +// +// Copyright (c) 2013 Path, Inc. +// See LICENSE for full license agreement. +// + +@class FICDPhoto; + +@protocol FICDPhotosTableViewCellDelegate; + +@interface FICDPhotosTableViewCell : UITableViewCell + +@property (nonatomic, assign) id delegate; +@property (nonatomic, assign) BOOL usesImageTable; +@property (nonatomic, copy) NSArray *photos; + ++ (NSString *)reuseIdentifier; ++ (NSInteger)photosPerRow; ++ (CGFloat)outerPadding; ++ (CGFloat)rowHeightForInterfaceOrientation:(UIInterfaceOrientation)interfaceOrientation; + +@end + +@protocol FICDPhotosTableViewCellDelegate + +@required +- (void)photosTableViewCell:(FICDPhotosTableViewCell *)photosTableViewCell didSelectPhoto:(FICDPhoto *)photo withImageView:(UIImageView *)imageView; + +@end diff --git a/FastImageCacheDemo/Classes/FICDPhotosTableViewCell.m b/FastImageCacheDemo/Classes/FICDPhotosTableViewCell.m new file mode 100644 index 0000000..de8b7ae --- /dev/null +++ b/FastImageCacheDemo/Classes/FICDPhotosTableViewCell.m @@ -0,0 +1,186 @@ +// +// FICDPhotosTableViewCell.m +// FastImageCacheDemo +// +// Copyright (c) 2013 Path, Inc. +// See LICENSE for full license agreement. +// + +#import "FICDPhotosTableViewCell.h" +#import "FICDPhoto.h" +#import "FICImageCache.h" +#import "FICDAppDelegate.h" + +#pragma mark Class Extension + +@interface FICDPhotosTableViewCell () { + id _delegate; + + NSArray *_photos; + NSMutableArray *_imageViews; + BOOL _usesImageTable; + UITapGestureRecognizer *_tapGestureRecognizer; +} + +@end + +#pragma mark + +@implementation FICDPhotosTableViewCell + +@synthesize delegate = _delegate; +@synthesize photos = _photos; +@synthesize usesImageTable = _usesImageTable; + +#pragma mark - Property Accessors + +- (void)setPhotos:(NSArray *)photos { + if (photos != _photos) { + [_photos release]; + _photos = [photos copy]; + + // Either create the image views for this cell or clear them out if they already exist + if (_imageViews == nil) { + NSInteger photosPerRow = [[self class] photosPerRow]; + _imageViews = [[NSMutableArray alloc] initWithCapacity:photosPerRow]; + + for (NSInteger i = 0; i < photosPerRow; i++) { + UIImageView *imageView = [[UIImageView alloc] init]; + [imageView setContentMode:UIViewContentModeScaleAspectFill]; + [_imageViews addObject:imageView]; + [imageView release]; + } + } else { + for (UIImageView *imageView in _imageViews) { + [imageView setImage:nil]; + [imageView removeFromSuperview]; + } + } + + NSInteger photosCount = [_photos count]; + for (NSInteger i = 0; i < photosCount; i++) { + FICDPhoto *photo = [_photos objectAtIndex:i]; + UIImageView *imageView = [_imageViews objectAtIndex:i]; + + if (_usesImageTable) { + [[FICImageCache sharedImageCache] retrieveImageForEntity:photo withFormatName:FICDPhotoSquareImageFormatName completionBlock:^(id entity, NSString *formatName, UIImage *image) { + // This completion block may be called much later. We should check to make sure this cell hasn't been reused for different photos before displaying the image that has loaded. + if (photos == [self photos]) { + [imageView setImage:image]; + } + }]; + } else { + [imageView setImage:[photo thumbnailImage]]; + } + } + + [self setNeedsLayout]; + } +} + +#pragma mark - Class-Level Definitions + ++ (NSString *)reuseIdentifier { + static NSString *__reuseIdentifier = nil; + + static dispatch_once_t onceToken; + dispatch_once(&onceToken, ^{ + __reuseIdentifier = [NSStringFromClass([FICDPhotosTableViewCell class]) retain]; + }); + + return __reuseIdentifier; +} + ++ (NSInteger)photosPerRow { + NSInteger photosPerRow = ([[UIDevice currentDevice] userInterfaceIdiom] == UIUserInterfaceIdiomPad) ? 9 : 4; + + return photosPerRow; +} + ++ (CGFloat)outerPadding { + CGFloat outerPadding = ([[UIDevice currentDevice] userInterfaceIdiom] == UIUserInterfaceIdiomPad) ? 10 : 4; + + return outerPadding; +} + ++ (CGFloat)rowHeightForInterfaceOrientation:(UIInterfaceOrientation)interfaceOrientation { + CGFloat rowHeight = ([[UIDevice currentDevice] userInterfaceIdiom] == UIUserInterfaceIdiomPad) ? 84 : 79; + + return rowHeight; +} + +#pragma mark - Object Lifecycle + +- (id)initWithStyle:(UITableViewCellStyle)style reuseIdentifier:(NSString *)reuseIdentifier { + self = [super initWithStyle:style reuseIdentifier:reuseIdentifier]; + + if (self != nil) { + _tapGestureRecognizer = [[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(_tapGestureRecognizerStateDidChange)]; + [self addGestureRecognizer:_tapGestureRecognizer]; + } + + return self; +} + +- (id)init { + return [self initWithStyle:UITableViewCellStyleDefault reuseIdentifier:nil]; +} + +- (void)dealloc { + [_photos release]; + [_imageViews release]; + + [_tapGestureRecognizer setDelegate:nil]; + [_tapGestureRecognizer release]; + + [super dealloc]; +} + +#pragma mark - Configuring the View Hierarchy + +- (void)layoutSubviews { + [super layoutSubviews]; + + CGFloat innerPadding = ([[UIDevice currentDevice] userInterfaceIdiom] == UIUserInterfaceIdiomPad) ? 9 : 4; + CGFloat outerPadding = [[self class] outerPadding]; + + CGRect imageViewFrame = CGRectMake(outerPadding, outerPadding, FICDPhotoSquareImageSize.width, FICDPhotoSquareImageSize.height); + + UIView *contentView = [self contentView]; + NSInteger count = [_photos count]; + + for (NSInteger i = 0; i < count; i++) { + UIImageView *imageView = [_imageViews objectAtIndex:i]; + [imageView setFrame:imageViewFrame]; + [contentView addSubview:imageView]; + + imageViewFrame.origin.x += imageViewFrame.size.width + innerPadding; + } +} + +#pragma mark - Responding to User Interaction Events + +- (void)_tapGestureRecognizerStateDidChange { + if ([_tapGestureRecognizer state] == UIGestureRecognizerStateEnded) { + CGPoint tapLocationInSelf = [_tapGestureRecognizer locationInView:self]; + UIImageView *selectedImageView = nil; + + for (UIImageView *imageView in _imageViews) { + CGRect imageViewFrame = [imageView frame]; + BOOL frameContainsTapLocation = CGRectContainsPoint(imageViewFrame, tapLocationInSelf); + + if (frameContainsTapLocation) { + selectedImageView = imageView; + } + } + + if (selectedImageView != nil) { + NSUInteger imageViewIndex = [_imageViews indexOfObject:selectedImageView]; + FICDPhoto *selectedPhoto = [_photos objectAtIndex:imageViewIndex]; + + [_delegate photosTableViewCell:self didSelectPhoto:selectedPhoto withImageView:selectedImageView]; + } + } +} + +@end diff --git a/FastImageCacheDemo/Classes/FICDTableView.h b/FastImageCacheDemo/Classes/FICDTableView.h new file mode 100644 index 0000000..500fec9 --- /dev/null +++ b/FastImageCacheDemo/Classes/FICDTableView.h @@ -0,0 +1,15 @@ +// +// FICDTableView.h +// FastImageCacheDemo +// +// Copyright (c) 2013 Path, Inc. +// See LICENSE for full license agreement. +// + +@interface FICDTableView : UITableView + +@property (nonatomic, assign, readonly) CGFloat averageFPS; + +- (void)resetScrollingPerformanceCounters; + +@end diff --git a/FastImageCacheDemo/Classes/FICDTableView.m b/FastImageCacheDemo/Classes/FICDTableView.m new file mode 100644 index 0000000..97ea688 --- /dev/null +++ b/FastImageCacheDemo/Classes/FICDTableView.m @@ -0,0 +1,117 @@ +// +// FICDTableView.m +// FastImageCacheDemo +// +// Copyright (c) 2013 Path, Inc. +// See LICENSE for full license agreement. +// + +#import "FICDTableView.h" + +#pragma mark Class Extension + +@interface FICDTableView () { + CADisplayLink *_displayLink; + NSInteger _framesInLastInterval; + CFAbsoluteTime _lastLogTime; + NSInteger _totalFrames; + NSTimeInterval _scrollingTime; + CGFloat _averageFPS; +} + +@property (nonatomic, assign, readwrite) CGFloat averageFPS; + +@end + +#pragma mark + +@implementation FICDTableView + +@synthesize averageFPS = _averageFPS; + +#pragma mark - Object Lifecycle + +- (void)dealloc { + [_displayLink invalidate]; + [_displayLink release]; + + [super dealloc]; +} + +- (void)didMoveToWindow { + if ([self window] != nil) { + [self _scrollingStatusDidChange]; + } else { + [_displayLink invalidate]; + [_displayLink release]; + _displayLink = nil; + } +} + +#pragma mark - Monitoring Scrolling Performance + +- (void)resetScrollingPerformanceCounters { + _framesInLastInterval = 0; + _lastLogTime = CFAbsoluteTimeGetCurrent(); + _scrollingTime = 0; + _totalFrames = 0; +} + +- (void)_scrollingStatusDidChange { + NSString *currentRunLoopMode = [[NSRunLoop currentRunLoop] currentMode]; + BOOL isScrolling = [currentRunLoopMode isEqualToString:UITrackingRunLoopMode]; + + [NSObject cancelPreviousPerformRequestsWithTarget:self selector:@selector(_scrollingStatusDidChange) object:nil]; + + if (isScrolling) { + if (_displayLink == nil) { + _displayLink = [[CADisplayLink displayLinkWithTarget:self selector:@selector(_screenDidUpdateWhileScrolling:)] retain]; + [_displayLink addToRunLoop:[NSRunLoop currentRunLoop] forMode:UITrackingRunLoopMode]; + } + _framesInLastInterval = 0; + _lastLogTime = CFAbsoluteTimeGetCurrent(); + [_displayLink setPaused:NO]; + + // Let us know when scrolling has stopped + [self performSelector:@selector(_scrollingStatusDidChange) withObject:nil afterDelay:0 inModes:[NSArray arrayWithObject:NSDefaultRunLoopMode]]; + } else { + [_displayLink setPaused:YES]; + + // Let us know when scrolling begins + [self performSelector:@selector(_scrollingStatusDidChange) withObject:nil afterDelay:0 inModes:[NSArray arrayWithObject:UITrackingRunLoopMode]]; + } +} + +- (void)_screenDidUpdateWhileScrolling:(CADisplayLink *)displayLink { + CFAbsoluteTime currentTime = CFAbsoluteTimeGetCurrent(); + if (!_lastLogTime) { + _lastLogTime = currentTime; + } + CGFloat delta = currentTime - _lastLogTime; + if (delta >= 1) { + _scrollingTime += delta; + _totalFrames += _framesInLastInterval; + NSInteger lastFPS = (NSInteger)rintf((CGFloat)_framesInLastInterval / delta); + CGFloat averageFPS = (CGFloat)(_totalFrames / _scrollingTime); + [self setAverageFPS:averageFPS]; + + static dispatch_queue_t __dispatchQueue = nil; + static dispatch_once_t onceToken; + dispatch_once(&onceToken, ^{ + __dispatchQueue = dispatch_queue_create("com.path.FastImageCacheDemo.ScrollingPerformanceMeasurement", 0); + }); + + // We don't want the logging of scrolling performance to be able to impact the scrolling performance, + // so move both the logging and the string formatting onto a GCD serial queue. + dispatch_async(__dispatchQueue, ^{ + NSLog(@"*** FIC Demo: Last FPS = %d, Average FPS = %.2f", lastFPS, averageFPS); + }); + + _framesInLastInterval = 0; + _lastLogTime = currentTime; + } else { + _framesInLastInterval++; + } +} + +@end diff --git a/FastImageCacheDemo/Classes/FICDViewController.h b/FastImageCacheDemo/Classes/FICDViewController.h new file mode 100644 index 0000000..54f3023 --- /dev/null +++ b/FastImageCacheDemo/Classes/FICDViewController.h @@ -0,0 +1,13 @@ +// +// FICDViewController.h +// FastImageCacheDemo +// +// Copyright (c) 2013 Path, Inc. +// See LICENSE for full license agreement. +// + +@class FICDTableView; + +@interface FICDViewController : UIViewController + +@end diff --git a/FastImageCacheDemo/Classes/FICDViewController.m b/FastImageCacheDemo/Classes/FICDViewController.m new file mode 100644 index 0000000..a2984b8 --- /dev/null +++ b/FastImageCacheDemo/Classes/FICDViewController.m @@ -0,0 +1,522 @@ +// +// FICDViewController.m +// FastImageCacheDemo +// +// Copyright (c) 2013 Path, Inc. +// See LICENSE for full license agreement. +// + +#import "FICDViewController.h" +#import "FICImageCache.h" +#import "FICDTableView.h" +#import "FICDAppDelegate.h" +#import "FICDPhoto.h" +#import "FICDFullscreenPhotoDisplayController.h" +#import "FICDPhotosTableViewCell.h" + +#pragma mark Class Extension + +@interface FICDViewController () { + FICDTableView *_tableView; + NSArray *_photos; + BOOL _usesImageTable; + BOOL _reloadTableViewAfterScrollingAnimationEnds; + BOOL _shouldResetData; + NSInteger _selectedSegmentControlIndex; + NSInteger _callbackCount; + UIAlertView *_noImagesAlertView; + UILabel *_averageFPSLabel; +} + +@end + +#pragma mark + +@implementation FICDViewController + +#pragma mark - Object Lifecycle + +- (id)init { + self = [super init]; + + if (self != nil) { + NSBundle *mainBundle = [NSBundle mainBundle]; + NSArray *imageURLs = [mainBundle URLsForResourcesWithExtension:@"jpg" subdirectory:@"Demo Images"]; + + if ([imageURLs count] > 0) { + NSMutableArray *photos = [[NSMutableArray alloc] init]; + for (NSURL *imageURL in imageURLs) { + FICDPhoto *photo = [[FICDPhoto alloc] init]; + [photo setSourceImageURL:imageURL]; + [photos addObject:photo]; + [photo release]; + } + + while ([photos count] < 5000) { + [photos addObjectsFromArray:photos]; // Create lots of photos to scroll through + } + + _photos = photos; + } else { + NSString *title = @"No Source Images"; + NSString *message = @"There are no JPEG images in the Demo Images folder. Please run the fetch_demo_images.sh script, or add your own JPEG images to this folder before running the demo app."; + _noImagesAlertView = [[UIAlertView alloc] initWithTitle:title message:message delegate:self cancelButtonTitle:@"OK" otherButtonTitles:nil]; + [_noImagesAlertView show]; + } + } + + return self; +} + +- (void)dealloc { + [_tableView setDelegate:nil]; + [_tableView setDataSource:nil]; + [_tableView release]; + + [_photos release]; + + [_noImagesAlertView setDelegate:nil]; + [_noImagesAlertView release]; + + [_averageFPSLabel release]; + + [super dealloc]; +} + +#pragma mark - View Controller Lifecycle + +- (void)loadView { + CGRect viewFrame = [[UIScreen mainScreen] bounds]; + UIView *view = [[[UIView alloc] initWithFrame:viewFrame] autorelease]; + [view setAutoresizingMask:(UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleHeight)]; + [view setBackgroundColor:[UIColor whiteColor]]; + + [self setView:view]; + + // Configure the table view + if (_tableView == nil) { + _tableView = [[FICDTableView alloc] initWithFrame:CGRectZero style:UITableViewStylePlain]; + [_tableView setDataSource:self]; + [_tableView setDelegate:self]; + [_tableView setAutoresizingMask:(UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleHeight)]; + [_tableView setSeparatorStyle:UITableViewCellSeparatorStyleNone]; + + CGFloat tableViewCellOuterPadding = [FICDPhotosTableViewCell outerPadding]; + [_tableView setContentInset:UIEdgeInsetsMake(0, 0, tableViewCellOuterPadding, 0)]; + + if ([[UIDevice currentDevice] userInterfaceIdiom] == UIUserInterfaceIdiomPad) { + [_tableView setScrollIndicatorInsets:UIEdgeInsetsMake(7, 0, 7, 1)]; + } + } + + [_tableView setFrame:[view bounds]]; + [view addSubview:_tableView]; + + // Configure the navigation item + UINavigationItem *navigationItem = [self navigationItem]; + + UIBarButtonItem *resetBarButtonItem = [[[UIBarButtonItem alloc] initWithTitle:@"Reset" style:UIBarButtonItemStyleBordered target:self action:@selector(_reset)] autorelease]; + [navigationItem setLeftBarButtonItem:resetBarButtonItem]; + + UISegmentedControl *segmentedControl = [[[UISegmentedControl alloc] initWithItems:[NSArray arrayWithObjects:@"Conventional", @"Image Table", nil]] autorelease]; + [segmentedControl setSelectedSegmentIndex:0]; + [segmentedControl addTarget:self action:@selector(_segmentedControlValueChanged:) forControlEvents:UIControlEventValueChanged]; + [segmentedControl setSegmentedControlStyle:UISegmentedControlStyleBar]; + [segmentedControl sizeToFit]; + [navigationItem setTitleView:segmentedControl]; + + // Configure the average FPS label + if (_averageFPSLabel == nil) { + _averageFPSLabel = [[UILabel alloc] initWithFrame:CGRectMake(0, 0, 54, 22)]; + [_averageFPSLabel setBackgroundColor:[UIColor clearColor]]; + [_averageFPSLabel setFont:[UIFont boldSystemFontOfSize:16]]; + [_averageFPSLabel setTextAlignment:NSTextAlignmentRight]; + + [_tableView addObserver:self forKeyPath:@"averageFPS" options:NSKeyValueObservingOptionNew context:NULL]; + } + + UIBarButtonItem *averageFPSLabelBarButtonItem = [[[UIBarButtonItem alloc] initWithCustomView:_averageFPSLabel] autorelease]; + [navigationItem setRightBarButtonItem:averageFPSLabelBarButtonItem]; +} + +- (void)viewDidAppear:(BOOL)animated { + [super viewDidAppear:animated]; + + [[FICDFullscreenPhotoDisplayController sharedDisplayController] setDelegate:self]; + [self reloadTableViewAndScrollToTop:YES]; +} + +#pragma mark - Reloading Data + +- (void)reloadTableViewAndScrollToTop:(BOOL)scrollToTop { + UIApplication *sharedApplication = [UIApplication sharedApplication]; + + // Don't allow interaction events to interfere with thumbnail generation + if ([sharedApplication isIgnoringInteractionEvents] == NO) { + [sharedApplication beginIgnoringInteractionEvents]; + } + + if (scrollToTop) { + // If the table view isn't already scrolled to top, we do that now, deferring the actual table view reloading logic until the animation finishes. + CGFloat tableViewTopmostContentOffsetY = 0; + CGFloat tableViewCurrentContentOffsetY = [_tableView contentOffset].y; + + if ([self respondsToSelector:@selector(topLayoutGuide)]) { + id topLayoutGuide = [self topLayoutGuide]; + tableViewTopmostContentOffsetY = -[topLayoutGuide length]; + } + + if (tableViewCurrentContentOffsetY > tableViewTopmostContentOffsetY) { + [_tableView scrollToRowAtIndexPath:[NSIndexPath indexPathForRow:0 inSection:0] atScrollPosition:UITableViewScrollPositionTop animated:YES]; + + _reloadTableViewAfterScrollingAnimationEnds = YES; + } + } + + if (_reloadTableViewAfterScrollingAnimationEnds == NO) { + // Reset the data now + if (_shouldResetData) { + _shouldResetData = NO; + [[FICImageCache sharedImageCache] reset]; + + // Delete all cached thumbnail images as well + for (FICDPhoto *photo in _photos) { + [photo deleteThumbnail]; + } + } + + _usesImageTable = _selectedSegmentControlIndex == 1; + + dispatch_block_t tableViewReloadBlock = ^{ + [_tableView reloadData]; + [_tableView resetScrollingPerformanceCounters]; + + if ([_tableView isHidden]) { + [[_tableView layer] addAnimation:[CATransition animation] forKey:kCATransition]; + } + + [_tableView setHidden:NO]; + + // Re-enable interaction events once every thumbnail has been generated + if ([sharedApplication isIgnoringInteractionEvents]) { + [sharedApplication endIgnoringInteractionEvents]; + } + }; + + dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0), ^{ + // In order to make a fair comparison for both methods, we ensure that the cached data is ready to go before updating the UI. + if (_usesImageTable) { + _callbackCount = 0; + NSSet *uniquePhotos = [NSSet setWithArray:_photos]; + for (FICDPhoto *photo in uniquePhotos) { + CFAbsoluteTime startTime = CFAbsoluteTimeGetCurrent(); + FICImageCache *sharedImageCache = [FICImageCache sharedImageCache]; + + if ([sharedImageCache imageExistsForEntity:photo withFormatName:FICDPhotoSquareImageFormatName] == NO) { + if (_callbackCount == 0) { + NSLog(@"*** FIC Demo: Fast Image Cache: Generating thumbnails..."); + + // Hide the table view's contents while we generate new thumbnails + dispatch_async(dispatch_get_main_queue(), ^{ + [_tableView setHidden:YES]; + [[_tableView layer] addAnimation:[CATransition animation] forKey:kCATransition]; + }); + } + + _callbackCount++; + + [sharedImageCache asynchronouslyRetrieveImageForEntity:photo withFormatName:FICDPhotoSquareImageFormatName completionBlock:^(id entity, NSString *formatName, UIImage *image) { + _callbackCount--; + + if (_callbackCount == 0) { + NSLog(@"*** FIC Demo: Fast Image Cache: Generated thumbnails in %g seconds", CFAbsoluteTimeGetCurrent() - startTime); + dispatch_async(dispatch_get_main_queue(), tableViewReloadBlock); + } + }]; + } + } + + if (_callbackCount == 0) { + dispatch_async(dispatch_get_main_queue(), tableViewReloadBlock); + } + } else { + [self _generateConventionalThumbnails]; + + dispatch_async(dispatch_get_main_queue(), tableViewReloadBlock); + } + }); + } +} + +- (void)_reset { + _shouldResetData = YES; + + [self reloadTableViewAndScrollToTop:YES]; +} + +- (void)_segmentedControlValueChanged:(UISegmentedControl *)segmentedControl { + _selectedSegmentControlIndex = [segmentedControl selectedSegmentIndex]; + + // If there's any scrolling momentum, we want to stop it now + CGPoint tableViewContentOffset = [_tableView contentOffset]; + [_tableView setContentOffset:tableViewContentOffset animated:NO]; + + [self reloadTableViewAndScrollToTop:NO]; +} + +#pragma mark - Image Helper Functions + +static UIImage * _FICDColorAveragedImageFromImage(UIImage *image) { + // Crop the image to the area occupied by the status bar + CGSize imageSize = [image size]; + CGSize statusBarSize = [[UIApplication sharedApplication] statusBarFrame].size; + CGRect cropRect = CGRectMake(0, 0, imageSize.width, statusBarSize.height); + + CGImageRef croppedImageRef = CGImageCreateWithImageInRect([image CGImage], cropRect); + UIImage *statusBarImage = [UIImage imageWithCGImage:croppedImageRef]; + CGImageRelease(croppedImageRef); + + // Draw the cropped image into a 1x1 bitmap context; this automatically averages the color values of every pixel + CGColorSpaceRef colorSpaceRef = CGColorSpaceCreateDeviceRGB(); + CGSize contextSize = CGSizeMake(1, 1); + CGContextRef bitmapContextRef = CGBitmapContextCreate(NULL, contextSize.width, contextSize.height, 8, 0, colorSpaceRef, (kCGImageAlphaNoneSkipFirst & kCGBitmapAlphaInfoMask)); + CGContextSetInterpolationQuality(bitmapContextRef, kCGInterpolationMedium); + + CGRect drawRect = CGRectZero; + drawRect.size = contextSize; + + UIGraphicsPushContext(bitmapContextRef); + [statusBarImage drawInRect:drawRect]; + UIGraphicsPopContext(); + + // Create an image from the bitmap context + CGImageRef colorAveragedImageRef = CGBitmapContextCreateImage(bitmapContextRef); + UIImage *colorAveragedImage = [UIImage imageWithCGImage:colorAveragedImageRef]; + CGImageRelease(colorAveragedImageRef); + + return colorAveragedImage; +} + +static BOOL _FICDImageIsLight(UIImage *image) { + BOOL imageIsLight = NO; + + CGImageRef imageRef = [image CGImage]; + CGDataProviderRef dataProviderRef = CGImageGetDataProvider(imageRef); + NSData *pixelData = (NSData *)CGDataProviderCopyData(dataProviderRef); + + if ([pixelData length] > 0) { + const UInt8 *pixelBytes = [pixelData bytes]; + + // Whether or not the image format is opaque, the first byte is always the alpha component, followed by RGB. + uint8_t pixelR = pixelBytes[1]; + uint8_t pixelG = pixelBytes[2]; + uint8_t pixelB = pixelBytes[3]; + + // Calculate the perceived luminance of the pixel; the human eye favors green, followed by red, then blue. + double percievedLuminance = 1 - (((0.299 * pixelR) + (0.587 * pixelG) + (0.114 * pixelB)) / 255); + + imageIsLight = percievedLuminance < 0.5; + } + + return imageIsLight; +} + +- (void)_updateStatusBarStyleForColorAveragedImage:(UIImage *)colorAveragedImage { + BOOL imageIsLight = _FICDImageIsLight(colorAveragedImage); + + UIStatusBarStyle statusBarStyle = imageIsLight ? UIStatusBarStyleDefault : UIStatusBarStyleLightContent; + [[UIApplication sharedApplication] setStatusBarStyle:statusBarStyle animated:YES]; +} + +#pragma mark - Working with Thumbnails + +- (void)_generateConventionalThumbnails { + BOOL neededToGenerateThumbnail = NO; + CFAbsoluteTime startTime = 0; + + NSSet *uniquePhotos = [NSSet setWithArray:_photos]; + for (FICDPhoto *photo in uniquePhotos) { + if ([photo thumbnailImageExists] == NO) { + if (neededToGenerateThumbnail == NO) { + NSLog(@"*** FIC Demo: Conventional Method: Generating thumbnails..."); + startTime = CFAbsoluteTimeGetCurrent(); + + neededToGenerateThumbnail = YES; + + // Hide the table view's contents while we generate new thumbnails + dispatch_async(dispatch_get_main_queue(), ^{ + [_tableView setHidden:YES]; + [[_tableView layer] addAnimation:[CATransition animation] forKey:kCATransition]; + }); + } + + @autoreleasepool { + [photo generateThumbnail]; + } + } + } + + if (neededToGenerateThumbnail) { + NSLog(@"*** FIC Demo: Conventional Method: Generated thumbnails in %g seconds", CFAbsoluteTimeGetCurrent() - startTime); + } +} + +#pragma mark - Displaying the Average Framerate + +- (void)_displayAverageFPS:(CGFloat)averageFPS { + if ([_averageFPSLabel attributedText] == nil) { + CATransition *fadeTransition = [CATransition animation]; + [[_averageFPSLabel layer] addAnimation:fadeTransition forKey:kCATransition]; + } + + NSString *averageFPSString = [NSString stringWithFormat:@"%.0f", averageFPS]; + NSUInteger averageFPSStringLength = [averageFPSString length]; + NSString *displayString = [NSString stringWithFormat:@"%@ FPS", averageFPSString]; + + UIColor *averageFPSColor = [UIColor blackColor]; + + if (averageFPS > 45) { + averageFPSColor = [UIColor colorWithHue:(114 / 359.0) saturation:0.99 brightness:0.89 alpha:1]; // Green + } else if (averageFPS <= 45 && averageFPS > 30) { + averageFPSColor = [UIColor colorWithHue:(38 / 359.0) saturation:0.99 brightness:0.89 alpha:1]; // Orange + } else if (averageFPS < 30) { + averageFPSColor = [UIColor colorWithHue:(6 / 359.0) saturation:0.99 brightness:0.89 alpha:1]; // Red + } + + NSMutableAttributedString *mutableAttributedString = [[[NSMutableAttributedString alloc] initWithString:displayString] autorelease]; + [mutableAttributedString addAttribute:NSForegroundColorAttributeName value:averageFPSColor range:NSMakeRange(0, averageFPSStringLength)]; + + [_averageFPSLabel setAttributedText:mutableAttributedString]; + + [NSObject cancelPreviousPerformRequestsWithTarget:self selector:@selector(_hideAverageFPSLabel) object:nil]; + [self performSelector:@selector(_hideAverageFPSLabel) withObject:nil afterDelay:1.5]; +} + +- (void)_hideAverageFPSLabel { + CATransition *fadeTransition = [CATransition animation]; + + [_averageFPSLabel setAttributedText:nil]; + [[_averageFPSLabel layer] addAnimation:fadeTransition forKey:kCATransition]; +} + +#pragma mark - Protocol Implementations + +#pragma mark - UITableViewDataSource + +- (NSInteger)tableView:(UITableView*)tableView numberOfRowsInSection:(NSInteger)section { + NSInteger numberOfRows = ceilf((CGFloat)[_photos count] / (CGFloat)[FICDPhotosTableViewCell photosPerRow]); + + return numberOfRows; +} + +- (UITableViewCell*)tableView:(UITableView*)table cellForRowAtIndexPath:(NSIndexPath*)indexPath { + NSString *reuseIdentifier = [FICDPhotosTableViewCell reuseIdentifier]; + + FICDPhotosTableViewCell *tableViewCell = (FICDPhotosTableViewCell *)[table dequeueReusableCellWithIdentifier:reuseIdentifier]; + if (tableViewCell == nil) { + tableViewCell = [[[FICDPhotosTableViewCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:reuseIdentifier] autorelease]; + [tableViewCell setBackgroundColor:[table backgroundColor]]; + [tableViewCell setSelectionStyle:UITableViewCellSelectionStyleNone]; + } + + [tableViewCell setDelegate:self]; + + NSInteger photosPerRow = [FICDPhotosTableViewCell photosPerRow]; + NSInteger startIndex = [indexPath row] * photosPerRow; + NSInteger count = MIN(photosPerRow, [_photos count] - startIndex); + NSArray *photos = [_photos subarrayWithRange:NSMakeRange(startIndex, count)]; + + [tableViewCell setUsesImageTable:_usesImageTable]; + [tableViewCell setPhotos:photos]; + + return tableViewCell; +} + +#pragma mark - UITableViewDelegate + +- (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath { + CGFloat rowHeight = [FICDPhotosTableViewCell rowHeightForInterfaceOrientation:[self interfaceOrientation]]; + + return rowHeight; +} + +- (void)tableView:(UITableView*)tableView didSelectRowAtIndexPath:(NSIndexPath*)indexPath { + [tableView deselectRowAtIndexPath:indexPath animated:NO]; +} + +#pragma mark - UIScrollViewDelegate + +- (void)scrollViewDidEndDragging:(UIScrollView *)scrollView willDecelerate:(BOOL)willDecelerate { + if (willDecelerate == NO) { + [_tableView resetScrollingPerformanceCounters]; + } +} + +- (void)scrollViewDidEndDecelerating:(UIScrollView *)scrollView { + [_tableView resetScrollingPerformanceCounters]; +} + +- (void)scrollViewDidEndScrollingAnimation:(UIScrollView *)scrollView { + [_tableView resetScrollingPerformanceCounters]; + + if (_reloadTableViewAfterScrollingAnimationEnds) { + _reloadTableViewAfterScrollingAnimationEnds = NO; + + // Add a slight delay before reloading the data + double delayInSeconds = 0.1; + dispatch_time_t popTime = dispatch_time(DISPATCH_TIME_NOW, (int64_t)(delayInSeconds * NSEC_PER_SEC)); + dispatch_after(popTime, dispatch_get_main_queue(), ^(void){ + [self reloadTableViewAndScrollToTop:NO]; + }); + } +} + +#pragma mark - UIAlertViewDelegate + +- (void)alertView:(UIAlertView *)alertView didDismissWithButtonIndex:(NSInteger)buttonIndex { + if (alertView == _noImagesAlertView) { + [NSThread exit]; + } +} + +#pragma mark - FICDPhotosTableViewCellDelegate + +- (void)photosTableViewCell:(FICDPhotosTableViewCell *)photosTableViewCell didSelectPhoto:(FICDPhoto *)photo withImageView:(UIImageView *)imageView { + [[FICDFullscreenPhotoDisplayController sharedDisplayController] showFullscreenPhoto:photo withThumbnailImageView:imageView]; +} + +#pragma mark - FICDFullscreenPhotoDisplayControllerDelegate + +- (void)photoDisplayController:(FICDFullscreenPhotoDisplayController *)photoDisplayController willShowSourceImage:(UIImage *)sourceImage forPhoto:(FICDPhoto *)photo withThumbnailImageView:(UIImageView *)thumbnailImageView { + // If we're running on iOS 7, we'll try to intelligently determine whether the photo contents underneath the status bar is light or dark. + if ([self respondsToSelector:@selector(preferredStatusBarStyle)]) { + if (_usesImageTable) { + [[FICImageCache sharedImageCache] retrieveImageForEntity:photo withFormatName:FICDPhotoPixelImageFormatName completionBlock:^(id entity, NSString *formatName, UIImage *image) { + if (image != nil && [photoDisplayController isDisplayingPhoto]) { + [self _updateStatusBarStyleForColorAveragedImage:image]; + } + }]; + } else { + UIImage *colorAveragedImage = _FICDColorAveragedImageFromImage(sourceImage); + [self _updateStatusBarStyleForColorAveragedImage:colorAveragedImage]; + } + } else { + [[UIApplication sharedApplication] setStatusBarStyle:UIStatusBarStyleBlackTranslucent animated:YES]; + } +} + +- (void)photoDisplayController:(FICDFullscreenPhotoDisplayController *)photoDisplayController willHideSourceImage:(UIImage *)sourceImage forPhoto:(FICDPhoto *)photo withThumbnailImageView:(UIImageView *)thumbnailImageView { + [[UIApplication sharedApplication] setStatusBarStyle:UIStatusBarStyleDefault animated:YES]; +} + +#pragma mark - NSObject (NSKeyValueObserving) + +- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context { + if (object == _tableView && [keyPath isEqualToString:@"averageFPS"]) { + CGFloat averageFPS = [[change valueForKey:NSKeyValueChangeNewKey] floatValue]; + averageFPS = MIN(MAX(0, averageFPS), 60); + [self _displayAverageFPS:averageFPS]; + } +} + +@end diff --git a/FastImageCacheDemo/Demo Images/README b/FastImageCacheDemo/Demo Images/README new file mode 100644 index 0000000..492d199 --- /dev/null +++ b/FastImageCacheDemo/Demo Images/README @@ -0,0 +1 @@ +Either place your own .jpg files in this directory, or run the fetch_demo_images.sh script in the FastImageCacheDemo directory. \ No newline at end of file diff --git a/FastImageCacheDemo/FastImageCacheDemo.xcodeproj/project.pbxproj b/FastImageCacheDemo/FastImageCacheDemo.xcodeproj/project.pbxproj new file mode 100644 index 0000000..22a6789 --- /dev/null +++ b/FastImageCacheDemo/FastImageCacheDemo.xcodeproj/project.pbxproj @@ -0,0 +1,373 @@ +// !$*UTF8*$! +{ + archiveVersion = 1; + classes = { + }; + objectVersion = 46; + objects = { + +/* Begin PBXBuildFile section */ + 2E095B1417AEBB1200ECE160 /* UIKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 2E095B1317AEBB1200ECE160 /* UIKit.framework */; }; + 2E095B1617AEBB1200ECE160 /* Foundation.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 2E095B1517AEBB1200ECE160 /* Foundation.framework */; }; + 2E1BC8F017C57C4700836A7E /* FICImageCache.m in Sources */ = {isa = PBXBuildFile; fileRef = 2E1BC8E417C57C4700836A7E /* FICImageCache.m */; }; + 2E1BC8F117C57C4700836A7E /* FICImageFormat.m in Sources */ = {isa = PBXBuildFile; fileRef = 2E1BC8E617C57C4700836A7E /* FICImageFormat.m */; }; + 2E1BC8F217C57C4700836A7E /* FICImageTable.m in Sources */ = {isa = PBXBuildFile; fileRef = 2E1BC8E817C57C4700836A7E /* FICImageTable.m */; }; + 2E1BC8F317C57C4700836A7E /* FICImageTableChunk.m in Sources */ = {isa = PBXBuildFile; fileRef = 2E1BC8EA17C57C4700836A7E /* FICImageTableChunk.m */; }; + 2E1BC8F417C57C4700836A7E /* FICImageTableEntry.m in Sources */ = {isa = PBXBuildFile; fileRef = 2E1BC8EC17C57C4700836A7E /* FICImageTableEntry.m */; }; + 2E1BC8FE17C57CDF00836A7E /* FICDAppDelegate.m in Sources */ = {isa = PBXBuildFile; fileRef = 2E1BC8F717C57CD300836A7E /* FICDAppDelegate.m */; }; + 2E1BC8FF17C57CDF00836A7E /* FICDPhoto.m in Sources */ = {isa = PBXBuildFile; fileRef = 2E1BC8F917C57CD300836A7E /* FICDPhoto.m */; }; + 2E1BC90017C57CDF00836A7E /* FICDPhotosTableViewCell.m in Sources */ = {isa = PBXBuildFile; fileRef = 2E1BC8FB17C57CD300836A7E /* FICDPhotosTableViewCell.m */; }; + 2E1BC90117C57CDF00836A7E /* FICDViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = 2E1BC8FD17C57CD300836A7E /* FICDViewController.m */; }; + 2E64541017FF24C0001D0531 /* FICDTableView.m in Sources */ = {isa = PBXBuildFile; fileRef = 2E64540F17FF24C0001D0531 /* FICDTableView.m */; }; + 2EA7994417B2A10200684B86 /* main.m in Sources */ = {isa = PBXBuildFile; fileRef = 2EA7993F17B2A10200684B86 /* main.m */; }; + EB642D8017DCB1750013D644 /* FICDFullscreenPhotoDisplayController.m in Sources */ = {isa = PBXBuildFile; fileRef = EB642D7F17DCB1750013D644 /* FICDFullscreenPhotoDisplayController.m */; }; + EB6BD53818079DAB00D762CE /* CoreGraphics.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = EB6BD53718079DAB00D762CE /* CoreGraphics.framework */; }; + EB6BD53A18079EA800D762CE /* FICUtilities.m in Sources */ = {isa = PBXBuildFile; fileRef = 2E1BC8EE17C57C4700836A7E /* FICUtilities.m */; }; + EBB922BD17DA8E6A00211050 /* QuartzCore.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = EBB922BC17DA8E6A00211050 /* QuartzCore.framework */; }; + EBB922BF17DC08E900211050 /* Demo Images in Resources */ = {isa = PBXBuildFile; fileRef = EBB922BE17DC08E900211050 /* Demo Images */; }; + EBC8A5F217FF6DEF007FECD4 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = EBC8A5F117FF6DEF007FECD4 /* Assets.xcassets */; }; +/* End PBXBuildFile section */ + +/* Begin PBXFileReference section */ + 2E095B1017AEBB1200ECE160 /* FastImageCacheDemo.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = FastImageCacheDemo.app; sourceTree = BUILT_PRODUCTS_DIR; }; + 2E095B1317AEBB1200ECE160 /* UIKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = UIKit.framework; path = System/Library/Frameworks/UIKit.framework; sourceTree = SDKROOT; }; + 2E095B1517AEBB1200ECE160 /* Foundation.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Foundation.framework; path = System/Library/Frameworks/Foundation.framework; sourceTree = SDKROOT; }; + 2E1BC8E317C57C4700836A7E /* FICImageCache.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = FICImageCache.h; sourceTree = ""; }; + 2E1BC8E417C57C4700836A7E /* FICImageCache.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = FICImageCache.m; sourceTree = ""; }; + 2E1BC8E517C57C4700836A7E /* FICImageFormat.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = FICImageFormat.h; sourceTree = ""; }; + 2E1BC8E617C57C4700836A7E /* FICImageFormat.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = FICImageFormat.m; sourceTree = ""; }; + 2E1BC8E717C57C4700836A7E /* FICImageTable.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = FICImageTable.h; sourceTree = ""; }; + 2E1BC8E817C57C4700836A7E /* FICImageTable.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = FICImageTable.m; sourceTree = ""; }; + 2E1BC8E917C57C4700836A7E /* FICImageTableChunk.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = FICImageTableChunk.h; sourceTree = ""; }; + 2E1BC8EA17C57C4700836A7E /* FICImageTableChunk.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = FICImageTableChunk.m; sourceTree = ""; }; + 2E1BC8EB17C57C4700836A7E /* FICImageTableEntry.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = FICImageTableEntry.h; sourceTree = ""; }; + 2E1BC8EC17C57C4700836A7E /* FICImageTableEntry.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = FICImageTableEntry.m; sourceTree = ""; }; + 2E1BC8ED17C57C4700836A7E /* FICUtilities.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = FICUtilities.h; sourceTree = ""; }; + 2E1BC8EE17C57C4700836A7E /* FICUtilities.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = FICUtilities.m; sourceTree = ""; }; + 2E1BC8F617C57CD300836A7E /* FICDAppDelegate.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = FICDAppDelegate.h; sourceTree = ""; }; + 2E1BC8F717C57CD300836A7E /* FICDAppDelegate.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = FICDAppDelegate.m; sourceTree = ""; }; + 2E1BC8F817C57CD300836A7E /* FICDPhoto.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = FICDPhoto.h; sourceTree = ""; }; + 2E1BC8F917C57CD300836A7E /* FICDPhoto.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = FICDPhoto.m; sourceTree = ""; }; + 2E1BC8FA17C57CD300836A7E /* FICDPhotosTableViewCell.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = FICDPhotosTableViewCell.h; sourceTree = ""; }; + 2E1BC8FB17C57CD300836A7E /* FICDPhotosTableViewCell.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = FICDPhotosTableViewCell.m; sourceTree = ""; }; + 2E1BC8FC17C57CD300836A7E /* FICDViewController.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = FICDViewController.h; sourceTree = ""; }; + 2E1BC8FD17C57CD300836A7E /* FICDViewController.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = FICDViewController.m; sourceTree = ""; }; + 2E64540E17FF24C0001D0531 /* FICDTableView.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = FICDTableView.h; sourceTree = ""; }; + 2E64540F17FF24C0001D0531 /* FICDTableView.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = FICDTableView.m; sourceTree = ""; }; + 2EA7993F17B2A10200684B86 /* main.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = main.m; sourceTree = ""; }; + EB642D3717DC9C690013D644 /* FastImageCacheDemo-Info.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; path = "FastImageCacheDemo-Info.plist"; sourceTree = ""; }; + EB642D7E17DCB1750013D644 /* FICDFullscreenPhotoDisplayController.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = FICDFullscreenPhotoDisplayController.h; sourceTree = ""; }; + EB642D7F17DCB1750013D644 /* FICDFullscreenPhotoDisplayController.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = FICDFullscreenPhotoDisplayController.m; sourceTree = ""; }; + EB6BD53718079DAB00D762CE /* CoreGraphics.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = CoreGraphics.framework; path = System/Library/Frameworks/CoreGraphics.framework; sourceTree = SDKROOT; }; + EB6BD53918079E2D00D762CE /* FICImports.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = FICImports.h; sourceTree = ""; }; + EBB922BA17D8EC7F00211050 /* FastImageCacheDemo-Prefix.pch */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "FastImageCacheDemo-Prefix.pch"; sourceTree = ""; }; + EBB922BC17DA8E6A00211050 /* QuartzCore.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = QuartzCore.framework; path = System/Library/Frameworks/QuartzCore.framework; sourceTree = SDKROOT; }; + EBB922BE17DC08E900211050 /* Demo Images */ = {isa = PBXFileReference; lastKnownFileType = folder; path = "Demo Images"; sourceTree = ""; }; + EBC8A5F117FF6DEF007FECD4 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; + EBC8A63718036430007FECD4 /* FICEntity.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = FICEntity.h; sourceTree = ""; }; + EBC8A6381804B63A007FECD4 /* FICImageCache+FICErrorLogging.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "FICImageCache+FICErrorLogging.h"; sourceTree = ""; }; +/* End PBXFileReference section */ + +/* Begin PBXFrameworksBuildPhase section */ + 2E095B0D17AEBB1200ECE160 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + EB6BD53818079DAB00D762CE /* CoreGraphics.framework in Frameworks */, + EBB922BD17DA8E6A00211050 /* QuartzCore.framework in Frameworks */, + 2E095B1417AEBB1200ECE160 /* UIKit.framework in Frameworks */, + 2E095B1617AEBB1200ECE160 /* Foundation.framework in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXFrameworksBuildPhase section */ + +/* Begin PBXGroup section */ + 2E095B0717AEBB1200ECE160 = { + isa = PBXGroup; + children = ( + 2E1BC8E017C57C4700836A7E /* FastImageCache */, + 2EA7993A17B2A0CF00684B86 /* Demo App */, + 2E095B1217AEBB1200ECE160 /* Frameworks */, + 2E095B1117AEBB1200ECE160 /* Products */, + ); + sourceTree = ""; + }; + 2E095B1117AEBB1200ECE160 /* Products */ = { + isa = PBXGroup; + children = ( + 2E095B1017AEBB1200ECE160 /* FastImageCacheDemo.app */, + ); + name = Products; + sourceTree = ""; + }; + 2E095B1217AEBB1200ECE160 /* Frameworks */ = { + isa = PBXGroup; + children = ( + EB6BD53718079DAB00D762CE /* CoreGraphics.framework */, + 2E095B1517AEBB1200ECE160 /* Foundation.framework */, + EBB922BC17DA8E6A00211050 /* QuartzCore.framework */, + 2E095B1317AEBB1200ECE160 /* UIKit.framework */, + ); + name = Frameworks; + sourceTree = ""; + }; + 2E095B1A17AEBB1200ECE160 /* Supporting Files */ = { + isa = PBXGroup; + children = ( + EBC8A5F117FF6DEF007FECD4 /* Assets.xcassets */, + 2EA7993F17B2A10200684B86 /* main.m */, + EBB922BA17D8EC7F00211050 /* FastImageCacheDemo-Prefix.pch */, + EB642D3717DC9C690013D644 /* FastImageCacheDemo-Info.plist */, + ); + path = "Supporting Files"; + sourceTree = ""; + }; + 2E1BC8E017C57C4700836A7E /* FastImageCache */ = { + isa = PBXGroup; + children = ( + EBC8A63718036430007FECD4 /* FICEntity.h */, + EBC8A6381804B63A007FECD4 /* FICImageCache+FICErrorLogging.h */, + 2E1BC8E317C57C4700836A7E /* FICImageCache.h */, + 2E1BC8E417C57C4700836A7E /* FICImageCache.m */, + 2E1BC8E517C57C4700836A7E /* FICImageFormat.h */, + 2E1BC8E617C57C4700836A7E /* FICImageFormat.m */, + 2E1BC8E717C57C4700836A7E /* FICImageTable.h */, + 2E1BC8E817C57C4700836A7E /* FICImageTable.m */, + 2E1BC8E917C57C4700836A7E /* FICImageTableChunk.h */, + 2E1BC8EA17C57C4700836A7E /* FICImageTableChunk.m */, + 2E1BC8EB17C57C4700836A7E /* FICImageTableEntry.h */, + 2E1BC8EC17C57C4700836A7E /* FICImageTableEntry.m */, + EB6BD53918079E2D00D762CE /* FICImports.h */, + 2E1BC8ED17C57C4700836A7E /* FICUtilities.h */, + 2E1BC8EE17C57C4700836A7E /* FICUtilities.m */, + ); + name = FastImageCache; + path = ../FastImageCache; + sourceTree = ""; + }; + 2EA7993A17B2A0CF00684B86 /* Demo App */ = { + isa = PBXGroup; + children = ( + EB642D3917DC9D970013D644 /* Classes */, + EBB922BE17DC08E900211050 /* Demo Images */, + 2E095B1A17AEBB1200ECE160 /* Supporting Files */, + ); + name = "Demo App"; + sourceTree = ""; + }; + EB642D3917DC9D970013D644 /* Classes */ = { + isa = PBXGroup; + children = ( + 2E1BC8F617C57CD300836A7E /* FICDAppDelegate.h */, + 2E1BC8F717C57CD300836A7E /* FICDAppDelegate.m */, + EB642D7E17DCB1750013D644 /* FICDFullscreenPhotoDisplayController.h */, + EB642D7F17DCB1750013D644 /* FICDFullscreenPhotoDisplayController.m */, + 2E1BC8F817C57CD300836A7E /* FICDPhoto.h */, + 2E1BC8F917C57CD300836A7E /* FICDPhoto.m */, + 2E1BC8FA17C57CD300836A7E /* FICDPhotosTableViewCell.h */, + 2E1BC8FB17C57CD300836A7E /* FICDPhotosTableViewCell.m */, + 2E64540E17FF24C0001D0531 /* FICDTableView.h */, + 2E64540F17FF24C0001D0531 /* FICDTableView.m */, + 2E1BC8FC17C57CD300836A7E /* FICDViewController.h */, + 2E1BC8FD17C57CD300836A7E /* FICDViewController.m */, + ); + path = Classes; + sourceTree = ""; + }; +/* End PBXGroup section */ + +/* Begin PBXNativeTarget section */ + 2E095B0F17AEBB1200ECE160 /* FastImageCacheDemo */ = { + isa = PBXNativeTarget; + buildConfigurationList = 2E095B3617AEBB1200ECE160 /* Build configuration list for PBXNativeTarget "FastImageCacheDemo" */; + buildPhases = ( + 2E095B0C17AEBB1200ECE160 /* Sources */, + 2E095B0D17AEBB1200ECE160 /* Frameworks */, + 2E095B0E17AEBB1200ECE160 /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + ); + name = FastImageCacheDemo; + productName = "Image Cache"; + productReference = 2E095B1017AEBB1200ECE160 /* FastImageCacheDemo.app */; + productType = "com.apple.product-type.application"; + }; +/* End PBXNativeTarget section */ + +/* Begin PBXProject section */ + 2E095B0817AEBB1200ECE160 /* Project object */ = { + isa = PBXProject; + attributes = { + CLASSPREFIX = PT; + LastUpgradeCheck = 0460; + ORGANIZATIONNAME = Path; + }; + buildConfigurationList = 2E095B0B17AEBB1200ECE160 /* Build configuration list for PBXProject "FastImageCacheDemo" */; + compatibilityVersion = "Xcode 3.2"; + developmentRegion = English; + hasScannedForEncodings = 0; + knownRegions = ( + en, + ); + mainGroup = 2E095B0717AEBB1200ECE160; + productRefGroup = 2E095B1117AEBB1200ECE160 /* Products */; + projectDirPath = ""; + projectRoot = ""; + targets = ( + 2E095B0F17AEBB1200ECE160 /* FastImageCacheDemo */, + ); + }; +/* End PBXProject section */ + +/* Begin PBXResourcesBuildPhase section */ + 2E095B0E17AEBB1200ECE160 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + EBB922BF17DC08E900211050 /* Demo Images in Resources */, + EBC8A5F217FF6DEF007FECD4 /* Assets.xcassets in Resources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXResourcesBuildPhase section */ + +/* Begin PBXSourcesBuildPhase section */ + 2E095B0C17AEBB1200ECE160 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 2EA7994417B2A10200684B86 /* main.m in Sources */, + 2E1BC8F017C57C4700836A7E /* FICImageCache.m in Sources */, + 2E1BC8F117C57C4700836A7E /* FICImageFormat.m in Sources */, + 2E1BC8F217C57C4700836A7E /* FICImageTable.m in Sources */, + EB642D8017DCB1750013D644 /* FICDFullscreenPhotoDisplayController.m in Sources */, + EB6BD53A18079EA800D762CE /* FICUtilities.m in Sources */, + 2E1BC8F317C57C4700836A7E /* FICImageTableChunk.m in Sources */, + 2E1BC8F417C57C4700836A7E /* FICImageTableEntry.m in Sources */, + 2E64541017FF24C0001D0531 /* FICDTableView.m in Sources */, + 2E1BC8FE17C57CDF00836A7E /* FICDAppDelegate.m in Sources */, + 2E1BC8FF17C57CDF00836A7E /* FICDPhoto.m in Sources */, + 2E1BC90017C57CDF00836A7E /* FICDPhotosTableViewCell.m in Sources */, + 2E1BC90117C57CDF00836A7E /* FICDViewController.m in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXSourcesBuildPhase section */ + +/* Begin XCBuildConfiguration section */ + 2E095B3417AEBB1200ECE160 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = NO; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; + COPY_PHASE_STRIP = NO; + GCC_C_LANGUAGE_STANDARD = gnu99; + GCC_DYNAMIC_NO_PIC = NO; + GCC_OPTIMIZATION_LEVEL = fast; + GCC_PREPROCESSOR_DEFINITIONS = ( + "DEBUG=1", + "$(inherited)", + ); + GCC_SYMBOLS_PRIVATE_EXTERN = NO; + GCC_WARN_ABOUT_RETURN_TYPE = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 6.1; + ONLY_ACTIVE_ARCH = YES; + SDKROOT = iphoneos; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = Debug; + }; + 2E095B3517AEBB1200ECE160 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = NO; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; + COPY_PHASE_STRIP = YES; + GCC_C_LANGUAGE_STANDARD = gnu99; + GCC_WARN_ABOUT_RETURN_TYPE = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 6.1; + OTHER_CFLAGS = "-DNS_BLOCK_ASSERTIONS=1"; + SDKROOT = iphoneos; + TARGETED_DEVICE_FAMILY = "1,2"; + VALIDATE_PRODUCT = YES; + }; + name = Release; + }; + 2E095B3717AEBB1200ECE160 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = Icon; + ASSETCATALOG_COMPILER_LAUNCHIMAGE_NAME = "Launch Image"; + "CODE_SIGN_IDENTITY[sdk=iphonesimulator*]" = ""; + GCC_PRECOMPILE_PREFIX_HEADER = YES; + GCC_PREFIX_HEADER = "Supporting Files/FastImageCacheDemo-Prefix.pch"; + INFOPLIST_FILE = "$(SRCROOT)/Supporting Files/FastImageCacheDemo-Info.plist"; + IPHONEOS_DEPLOYMENT_TARGET = 6.0; + PRODUCT_NAME = FastImageCacheDemo; + WRAPPER_EXTENSION = app; + }; + name = Debug; + }; + 2E095B3817AEBB1200ECE160 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = Icon; + ASSETCATALOG_COMPILER_LAUNCHIMAGE_NAME = "Launch Image"; + "CODE_SIGN_IDENTITY[sdk=iphonesimulator*]" = ""; + GCC_PRECOMPILE_PREFIX_HEADER = YES; + GCC_PREFIX_HEADER = "Supporting Files/FastImageCacheDemo-Prefix.pch"; + INFOPLIST_FILE = "$(SRCROOT)/Supporting Files/FastImageCacheDemo-Info.plist"; + IPHONEOS_DEPLOYMENT_TARGET = 6.0; + PRODUCT_NAME = FastImageCacheDemo; + WRAPPER_EXTENSION = app; + }; + name = Release; + }; +/* End XCBuildConfiguration section */ + +/* Begin XCConfigurationList section */ + 2E095B0B17AEBB1200ECE160 /* Build configuration list for PBXProject "FastImageCacheDemo" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 2E095B3417AEBB1200ECE160 /* Debug */, + 2E095B3517AEBB1200ECE160 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 2E095B3617AEBB1200ECE160 /* Build configuration list for PBXNativeTarget "FastImageCacheDemo" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 2E095B3717AEBB1200ECE160 /* Debug */, + 2E095B3817AEBB1200ECE160 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; +/* End XCConfigurationList section */ + }; + rootObject = 2E095B0817AEBB1200ECE160 /* Project object */; +} diff --git a/FastImageCacheDemo/Supporting Files/Assets.xcassets/Icon.appiconset/Contents.json b/FastImageCacheDemo/Supporting Files/Assets.xcassets/Icon.appiconset/Contents.json new file mode 100644 index 0000000..6fb4d76 --- /dev/null +++ b/FastImageCacheDemo/Supporting Files/Assets.xcassets/Icon.appiconset/Contents.json @@ -0,0 +1,98 @@ +{ + "images" : [ + { + "idiom" : "iphone", + "size" : "29x29", + "scale" : "1x" + }, + { + "idiom" : "iphone", + "size" : "29x29", + "scale" : "2x" + }, + { + "idiom" : "iphone", + "size" : "40x40", + "scale" : "2x" + }, + { + "size" : "57x57", + "idiom" : "iphone", + "filename" : "iPhone-App.png", + "scale" : "1x" + }, + { + "size" : "57x57", + "idiom" : "iphone", + "filename" : "iPhone-App@2x.png", + "scale" : "2x" + }, + { + "size" : "60x60", + "idiom" : "iphone", + "filename" : "iPhone-App-iOS7@2x.png", + "scale" : "2x" + }, + { + "idiom" : "ipad", + "size" : "29x29", + "scale" : "1x" + }, + { + "idiom" : "ipad", + "size" : "29x29", + "scale" : "2x" + }, + { + "idiom" : "ipad", + "size" : "40x40", + "scale" : "1x" + }, + { + "idiom" : "ipad", + "size" : "40x40", + "scale" : "2x" + }, + { + "idiom" : "ipad", + "size" : "50x50", + "scale" : "1x" + }, + { + "idiom" : "ipad", + "size" : "50x50", + "scale" : "2x" + }, + { + "size" : "72x72", + "idiom" : "ipad", + "filename" : "iPad-App.png", + "scale" : "1x" + }, + { + "size" : "72x72", + "idiom" : "ipad", + "filename" : "iPad-App@2x.png", + "scale" : "2x" + }, + { + "size" : "76x76", + "idiom" : "ipad", + "filename" : "iPad-App-iOS7.png", + "scale" : "1x" + }, + { + "size" : "76x76", + "idiom" : "ipad", + "filename" : "iPad-App-iOS7@2x.png", + "scale" : "2x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + }, + "properties" : { + "pre-rendered" : true + } +} \ No newline at end of file diff --git a/FastImageCacheDemo/Supporting Files/Assets.xcassets/Icon.appiconset/iPad-App-iOS7.png b/FastImageCacheDemo/Supporting Files/Assets.xcassets/Icon.appiconset/iPad-App-iOS7.png new file mode 100644 index 0000000..fd6b1bb Binary files /dev/null and b/FastImageCacheDemo/Supporting Files/Assets.xcassets/Icon.appiconset/iPad-App-iOS7.png differ diff --git a/FastImageCacheDemo/Supporting Files/Assets.xcassets/Icon.appiconset/iPad-App-iOS7@2x.png b/FastImageCacheDemo/Supporting Files/Assets.xcassets/Icon.appiconset/iPad-App-iOS7@2x.png new file mode 100644 index 0000000..5201f5a Binary files /dev/null and b/FastImageCacheDemo/Supporting Files/Assets.xcassets/Icon.appiconset/iPad-App-iOS7@2x.png differ diff --git a/FastImageCacheDemo/Supporting Files/Assets.xcassets/Icon.appiconset/iPad-App.png b/FastImageCacheDemo/Supporting Files/Assets.xcassets/Icon.appiconset/iPad-App.png new file mode 100644 index 0000000..3b957b7 Binary files /dev/null and b/FastImageCacheDemo/Supporting Files/Assets.xcassets/Icon.appiconset/iPad-App.png differ diff --git a/FastImageCacheDemo/Supporting Files/Assets.xcassets/Icon.appiconset/iPad-App@2x.png b/FastImageCacheDemo/Supporting Files/Assets.xcassets/Icon.appiconset/iPad-App@2x.png new file mode 100644 index 0000000..ca2929b Binary files /dev/null and b/FastImageCacheDemo/Supporting Files/Assets.xcassets/Icon.appiconset/iPad-App@2x.png differ diff --git a/FastImageCacheDemo/Supporting Files/Assets.xcassets/Icon.appiconset/iPhone-App-iOS7@2x.png b/FastImageCacheDemo/Supporting Files/Assets.xcassets/Icon.appiconset/iPhone-App-iOS7@2x.png new file mode 100644 index 0000000..3d9b0a0 Binary files /dev/null and b/FastImageCacheDemo/Supporting Files/Assets.xcassets/Icon.appiconset/iPhone-App-iOS7@2x.png differ diff --git a/FastImageCacheDemo/Supporting Files/Assets.xcassets/Icon.appiconset/iPhone-App.png b/FastImageCacheDemo/Supporting Files/Assets.xcassets/Icon.appiconset/iPhone-App.png new file mode 100644 index 0000000..d54adc8 Binary files /dev/null and b/FastImageCacheDemo/Supporting Files/Assets.xcassets/Icon.appiconset/iPhone-App.png differ diff --git a/FastImageCacheDemo/Supporting Files/Assets.xcassets/Icon.appiconset/iPhone-App@2x.png b/FastImageCacheDemo/Supporting Files/Assets.xcassets/Icon.appiconset/iPhone-App@2x.png new file mode 100644 index 0000000..27118cc Binary files /dev/null and b/FastImageCacheDemo/Supporting Files/Assets.xcassets/Icon.appiconset/iPhone-App@2x.png differ diff --git a/FastImageCacheDemo/Supporting Files/Assets.xcassets/Launch Image.launchimage/Contents.json b/FastImageCacheDemo/Supporting Files/Assets.xcassets/Launch Image.launchimage/Contents.json new file mode 100644 index 0000000..24a7ad4 --- /dev/null +++ b/FastImageCacheDemo/Supporting Files/Assets.xcassets/Launch Image.launchimage/Contents.json @@ -0,0 +1,77 @@ +{ + "images" : [ + { + "orientation" : "portrait", + "idiom" : "iphone", + "extent" : "full-screen", + "minimum-system-version" : "7.0", + "filename" : "iPhone-Portrait-iOS7@2x.png", + "scale" : "2x" + }, + { + "extent" : "full-screen", + "idiom" : "iphone", + "subtype" : "retina4", + "filename" : "iPhone-Portrait-R4@2x.png", + "minimum-system-version" : "7.0", + "orientation" : "portrait", + "scale" : "2x" + }, + { + "orientation" : "portrait", + "idiom" : "ipad", + "extent" : "full-screen", + "minimum-system-version" : "7.0", + "filename" : "iPad-Portrait-iOS7.png", + "scale" : "1x" + }, + { + "orientation" : "portrait", + "idiom" : "ipad", + "extent" : "full-screen", + "minimum-system-version" : "7.0", + "filename" : "iPad-Portrait-iOS7@2x.png", + "scale" : "2x" + }, + { + "orientation" : "portrait", + "idiom" : "iphone", + "extent" : "full-screen", + "filename" : "iPhone-Portrait.png", + "scale" : "1x" + }, + { + "orientation" : "portrait", + "idiom" : "iphone", + "extent" : "full-screen", + "filename" : "iPhone-Portrait@2x.png", + "scale" : "2x" + }, + { + "orientation" : "portrait", + "idiom" : "iphone", + "extent" : "full-screen", + "filename" : "iPhone-Portrait-R4@2x-1.png", + "subtype" : "retina4", + "scale" : "2x" + }, + { + "orientation" : "portrait", + "idiom" : "ipad", + "extent" : "to-status-bar", + "filename" : "iPad-Portrait-1004h.png", + "scale" : "1x" + }, + { + "orientation" : "portrait", + "idiom" : "ipad", + "extent" : "to-status-bar", + "filename" : "iPad-Portrait-1004h@2x.png", + "scale" : "2x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/FastImageCacheDemo/Supporting Files/Assets.xcassets/Launch Image.launchimage/iPad-Portrait-1004h.png b/FastImageCacheDemo/Supporting Files/Assets.xcassets/Launch Image.launchimage/iPad-Portrait-1004h.png new file mode 100644 index 0000000..01eab9c Binary files /dev/null and b/FastImageCacheDemo/Supporting Files/Assets.xcassets/Launch Image.launchimage/iPad-Portrait-1004h.png differ diff --git a/FastImageCacheDemo/Supporting Files/Assets.xcassets/Launch Image.launchimage/iPad-Portrait-1004h@2x.png b/FastImageCacheDemo/Supporting Files/Assets.xcassets/Launch Image.launchimage/iPad-Portrait-1004h@2x.png new file mode 100644 index 0000000..1dd42d8 Binary files /dev/null and b/FastImageCacheDemo/Supporting Files/Assets.xcassets/Launch Image.launchimage/iPad-Portrait-1004h@2x.png differ diff --git a/FastImageCacheDemo/Supporting Files/Assets.xcassets/Launch Image.launchimage/iPad-Portrait-iOS7.png b/FastImageCacheDemo/Supporting Files/Assets.xcassets/Launch Image.launchimage/iPad-Portrait-iOS7.png new file mode 100644 index 0000000..cf71825 Binary files /dev/null and b/FastImageCacheDemo/Supporting Files/Assets.xcassets/Launch Image.launchimage/iPad-Portrait-iOS7.png differ diff --git a/FastImageCacheDemo/Supporting Files/Assets.xcassets/Launch Image.launchimage/iPad-Portrait-iOS7@2x.png b/FastImageCacheDemo/Supporting Files/Assets.xcassets/Launch Image.launchimage/iPad-Portrait-iOS7@2x.png new file mode 100644 index 0000000..ac1541d Binary files /dev/null and b/FastImageCacheDemo/Supporting Files/Assets.xcassets/Launch Image.launchimage/iPad-Portrait-iOS7@2x.png differ diff --git a/FastImageCacheDemo/Supporting Files/Assets.xcassets/Launch Image.launchimage/iPhone-Portrait-R4@2x-1.png b/FastImageCacheDemo/Supporting Files/Assets.xcassets/Launch Image.launchimage/iPhone-Portrait-R4@2x-1.png new file mode 100644 index 0000000..7abc8a3 Binary files /dev/null and b/FastImageCacheDemo/Supporting Files/Assets.xcassets/Launch Image.launchimage/iPhone-Portrait-R4@2x-1.png differ diff --git a/FastImageCacheDemo/Supporting Files/Assets.xcassets/Launch Image.launchimage/iPhone-Portrait-R4@2x.png b/FastImageCacheDemo/Supporting Files/Assets.xcassets/Launch Image.launchimage/iPhone-Portrait-R4@2x.png new file mode 100644 index 0000000..7abc8a3 Binary files /dev/null and b/FastImageCacheDemo/Supporting Files/Assets.xcassets/Launch Image.launchimage/iPhone-Portrait-R4@2x.png differ diff --git a/FastImageCacheDemo/Supporting Files/Assets.xcassets/Launch Image.launchimage/iPhone-Portrait-iOS7@2x.png b/FastImageCacheDemo/Supporting Files/Assets.xcassets/Launch Image.launchimage/iPhone-Portrait-iOS7@2x.png new file mode 100644 index 0000000..50f7835 Binary files /dev/null and b/FastImageCacheDemo/Supporting Files/Assets.xcassets/Launch Image.launchimage/iPhone-Portrait-iOS7@2x.png differ diff --git a/FastImageCacheDemo/Supporting Files/Assets.xcassets/Launch Image.launchimage/iPhone-Portrait.png b/FastImageCacheDemo/Supporting Files/Assets.xcassets/Launch Image.launchimage/iPhone-Portrait.png new file mode 100644 index 0000000..bd1636f Binary files /dev/null and b/FastImageCacheDemo/Supporting Files/Assets.xcassets/Launch Image.launchimage/iPhone-Portrait.png differ diff --git a/FastImageCacheDemo/Supporting Files/Assets.xcassets/Launch Image.launchimage/iPhone-Portrait@2x.png b/FastImageCacheDemo/Supporting Files/Assets.xcassets/Launch Image.launchimage/iPhone-Portrait@2x.png new file mode 100644 index 0000000..50f7835 Binary files /dev/null and b/FastImageCacheDemo/Supporting Files/Assets.xcassets/Launch Image.launchimage/iPhone-Portrait@2x.png differ diff --git a/FastImageCacheDemo/Supporting Files/FastImageCacheDemo-Info.plist b/FastImageCacheDemo/Supporting Files/FastImageCacheDemo-Info.plist new file mode 100644 index 0000000..b48b80f --- /dev/null +++ b/FastImageCacheDemo/Supporting Files/FastImageCacheDemo-Info.plist @@ -0,0 +1,51 @@ + + + + + CFBundleDevelopmentRegion + en + CFBundleDisplayName + FIC Demo + CFBundleExecutable + ${EXECUTABLE_NAME} + CFBundleIcons + + CFBundleIcons~ipad + + CFBundleIdentifier + com.path.${PRODUCT_NAME:rfc1034identifier} + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + ${PRODUCT_NAME} + CFBundlePackageType + APPL + CFBundleShortVersionString + 1.0 + CFBundleSignature + ???? + CFBundleVersion + 1.0 + LSRequiresIPhoneOS + + UIRequiredDeviceCapabilities + + armv7 + + UIStatusBarHidden + + UIStatusBarHidden~ipad + + UISupportedInterfaceOrientations + + UIInterfaceOrientationPortrait + + UISupportedInterfaceOrientations~ipad + + UIInterfaceOrientationPortrait + UIInterfaceOrientationPortraitUpsideDown + + UIViewControllerBasedStatusBarAppearance + + + diff --git a/FastImageCacheDemo/Supporting Files/FastImageCacheDemo-Prefix.pch b/FastImageCacheDemo/Supporting Files/FastImageCacheDemo-Prefix.pch new file mode 100644 index 0000000..2367787 --- /dev/null +++ b/FastImageCacheDemo/Supporting Files/FastImageCacheDemo-Prefix.pch @@ -0,0 +1,24 @@ +// +// FastImageCacheDemo-Prefix.pch +// FastImageCacheDemo +// +// Copyright (c) 2013 Path, Inc. +// See LICENSE for full license agreement. +// + +#import + +#ifndef FastImageCacheDemo_Prefix_pch +#define FastImageCacheDemo_Prefix_pch + +#ifndef __IPHONE_7_0 +#warning "This project uses features only available in iOS SDK 7.0 and later." +#endif + +#ifdef __OBJC__ +#import +#import +#import +#endif + +#endif diff --git a/FastImageCacheDemo/Supporting Files/main.m b/FastImageCacheDemo/Supporting Files/main.m new file mode 100644 index 0000000..3d5d2d3 --- /dev/null +++ b/FastImageCacheDemo/Supporting Files/main.m @@ -0,0 +1,15 @@ +// +// main.m +// FastImageCacheDemo +// +// Copyright (c) 2013 Path, Inc. +// See LICENSE for full license agreement. +// + +#import "FICDAppDelegate.h" + +int main(int argc, char *argv[]) { + @autoreleasepool { + return UIApplicationMain(argc, argv, nil, NSStringFromClass([FICDAppDelegate class])); + } +} diff --git a/FastImageCacheDemo/fetch_demo_images.sh b/FastImageCacheDemo/fetch_demo_images.sh new file mode 100755 index 0000000..59ab032 --- /dev/null +++ b/FastImageCacheDemo/fetch_demo_images.sh @@ -0,0 +1,4 @@ +#!/bin/bash + +echo "Fetching demo images..." +`curl "https://s3.amazonaws.com/fast-image-cache/demo-images/FICDDemoImage[000-099].jpg" --create-dirs -o "Demo Images/FICDDemoImage#1.jpg" --silent` \ No newline at end of file diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..bde5e4c --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +The MIT License (MIT) + +Copyright (c) 2013 Path, Inc. + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..3e31d17 --- /dev/null +++ b/README.md @@ -0,0 +1,426 @@ +Path Logo + +# Fast Image Cache + +Fast Image Cache is an efficient, persistent, and—above all—fast way to store and retrieve images in your iOS application. Part of any good iOS application's user experience is fast, smooth scrolling, and Fast Image Cache helps make this easier. + +A significant burden on performance for graphics-rich applications like [Path](http://www.path.com) is image loading. The traditional method of loading individual images from disk is just too slow, especially while scrolling. Fast Image Cache was created specifically to solve this problem. + +## Table of Contents + +- [Version History](#version-history) +- [What Fast Image Cache Does](#what-fast-image-cache-does) +- [How Fast Image Cache Works](#how-fast-image-cache-works) +- [Considerations](#considerations) +- [Requirements](#requirements) +- [Getting Started](#getting-started) + - [Integrating Fast Image Cache](#integrating-fast-image-cache) + - [Initial Configuration](#initial-configuration) + - [Requesting Images from the Image Cache](#requesting-images-from-the-image-cache) + - [Providing Source Images to the Image Cache](#providing-source-images-to-the-image-cache) + - [Working with Image Format Families](#working-with-image-format-families) +- [Documentation](#documentation) +- [Demo Application](#demo-application) +- [Contributors](#contributors) +- [Credits](#credits) +- [License](#license) + +## Version History + +- [**1.0**](http://github.com/path/FastImageCache/tree/1.0) (10/12/2013): Initial release + +## What Fast Image Cache Does + +- Stores images of similar sizes and styles together +- Persists image data to disk +- Returns images to the user significantly faster than traditional methods +- Automatically manages cache expiry based on recency of usage +- Utilizes a model-based approach for storing and retrieving images +- Allows images to be processed on a per-model basis before being stored into the cache + +## How Fast Image Cache Works + +In order to understand how Fast Image Cache works, it's helpful to understand a typical scenario encountered by many applications that work with images. + +### The Scenario + +iOS applications, especially those in the social networking space, often have many images to display at once, such as user photos. The intuitive, traditional approach is to request image data from an API, process the original images to create the desired sizes and styles, and store these processed images on the device. + +Later, when an application needs to display these images, they are loaded from disk into memory and displayed in an image view or are otherwise rendered to the screen. + +### The Problem + +It turns out that the process of going from compressed, on-disk image data to a rendered Core Animation layer that the user can actually see is very expensive. As the number of images to be displayed increases, this cost easily adds up to a noticeable degradation in frame rate. And scrollable views further exacerbate the situation because content can change rapidly, requiring fast processing time to maintain a smooth 60FPS.1 + +Consider the workflow that occurs when loading an image from disk and displaying it on screen: + +1. [`+[UIImage imageWithContentsOfFile:]`](https://developer.apple.com/library/ios/documentation/uikit/reference/UIImage_Class/Reference/Reference.html#//apple_ref/doc/uid/TP40006890-CH3-SW12) uses [Image I/O](https://developer.apple.com/library/ios/documentation/graphicsimaging/conceptual/ImageIOGuide/imageio_intro/ikpg_intro.html#//apple_ref/doc/uid/TP40005462-CH201-TPXREF101) to create a [`CGImageRef`](http://www.google.com/url?sa=t&rct=j&q=&esrc=s&source=web&cd=1&cad=rja&ved=0CCwQFjAA&url=http%3A%2F%2Fdeveloper.apple.com%2Flibrary%2Fios%2Fdocumentation%2Fgraphicsimaging%2FReference%2FCGImage%2FReference%2Freference.html&ei=fG9XUpX_BqWqigLymIG4BQ&usg=AFQjCNHTelntXU5Gw0BQkQqj9HC5iZibyA&sig2=tLY7PDhyockUVlVFbrzyOQ) from memory-mapped data. At this point, the image has not yet been decoded. +1. The returned image is assigned to a [`UIImageView`](http://www.google.com/url?sa=t&rct=j&q=&esrc=s&source=web&cd=1&cad=rja&ved=0CCwQFjAA&url=http%3A%2F%2Fdeveloper.apple.com%2Flibrary%2Fios%2Fdocumentation%2Fuikit%2Freference%2FUIImageView_Class%2F&ei=VX9YUpGUKcG1iwLN3oHwDg&usg=AFQjCNGJCra_NhnVaXH2_pqIKjIHiNX9zQ&sig2=Lk2CMoN4kO5OzLJYhGh6Uw). +1. An implicit [`CATransaction`](https://www.google.com/url?sa=t&rct=j&q=&esrc=s&source=web&cd=1&cad=rja&ved=0CCwQFjAA&url=https%3A%2F%2Fdeveloper.apple.com%2Flibrary%2Fios%2Fdocumentation%2FGraphicsImaging%2FReference%2FCATransaction_class%2F&ei=AINYUsSqIqPfiAKsk4CoBA&usg=AFQjCNG5CarCxgkwdV_br80YDI7UwMTrmA&sig2=aPE_IoQSPUltdCYqARjt9Q) captures these layer tree modifications. +1. On the next iteration of the main run loop, Core Animation commits the implicit transaction, which may involve creating a copy of any images which have been set as layer contents. Depending on the image, copying it involves some or all of these steps: 2 + 1. Buffers are allocated to manage file IO and decompression operations. + 1. The file data is read from disk into memory. + 1. The compressed image data is decoded into its uncompressed bitmap form, which is typically a very CPU-intensive operation.3 + 1. The uncompressed bitmap data is then used by Core Animation to render the layer. + +**These costs can easily accumulate and kill perceived application performance.** Especially while scrolling, users are presented with an unsatisfying user experience that is not in line with the the overall iOS experience. + +--- + +1 `60FPS` ≈ `0.01666s per frame` = `16.7ms per frame`. This means that any main-thread work that takes longer than 16ms will cause your application to drop animation frames. + +2 The documentation for [`CALayer`](https://www.google.com/url?sa=t&rct=j&q=&esrc=s&source=web&cd=1&cad=rja&ved=0CCwQFjAA&url=https%3A%2F%2Fdeveloper.apple.com%2Flibrary%2Fios%2Fdocumentation%2Fgraphicsimaging%2Freference%2FCALayer_class%2FIntroduction%2FIntroduction.html&ei=P29XUpj2LeahiALptICgCQ&usg=AFQjCNGwJuHcQV4593kuookUcvNZYTvx5w&sig2=zi1audY4ZsNE_xLeESVD_Q)'s [`contents`](https://developer.apple.com/library/ios/documentation/graphicsimaging/reference/CALayer_class/Introduction/Introduction.html#//apple_ref/doc/uid/TP40004500-CH1-SW24) property states that "assigning a value to this property causes the layer to use your image rather than [creating] a separate backing store." However, the meaning of "use your image" is still vague. Profiling an application using [Instruments](https://developer.apple.com/library/ios/documentation/DeveloperTools/Conceptual/InstrumentsUserGuide/Introduction/Introduction.html#//apple_ref/doc/uid/TP40004652-CH1-SW1) often reveals calls to `CA::Render::copy_image`, even when the Core Animation Instrument has indicated that none of the images have been copied. One reason that Core Animation will require a copy of an image is improper [byte alignment](#byte-alignment). + +3 As of iOS 7, Apple does not make their hardware JPEG decoder available for third-party applications to use. As a result, only a slower, software decoder is used for this step. + +### The Solution + +Fast Image Cache minimizes (or avoids entirely) much of the work described above using a variety of techniques: + +#### Mapped Memory + +At the heart of how Fast Image Cache works are image tables. Image tables are similar to [sprite sheets](http://en.wikipedia.org/wiki/Sprite_sheet#Sprites_by_CSS), often used in 2D gaming. An image table packs together images of the same dimensions into a single file. This file is opened once and is left open for reading and writing for as long as an application remains in memory. + +Image tables use the [`mmap`](https://developer.apple.com/library/ios/documentation/system/conceptual/manpages_iphoneos/man2/mmap.2.html) system call to directly map file data into memory. No [`memcpy`](https://developer.apple.com/library/ios/documentation/system/conceptual/manpages_iphoneos/man3/memcpy.3.html) occurs. This system call merely creates a mapping between data on disk and a region of memory. + +When a request is made to the image cache to return a specific image, the image table finds (in constant time) the location of the desired image data in the file it maintains. That region of file data is mapped into memory, and a new [`CGImageRef`](http://www.google.com/url?sa=t&rct=j&q=&esrc=s&source=web&cd=1&cad=rja&ved=0CCwQFjAA&url=http%3A%2F%2Fdeveloper.apple.com%2Flibrary%2Fios%2Fdocumentation%2Fgraphicsimaging%2FReference%2FCGImage%2FReference%2Freference.html&ei=fG9XUpX_BqWqigLymIG4BQ&usg=AFQjCNHTelntXU5Gw0BQkQqj9HC5iZibyA&sig2=tLY7PDhyockUVlVFbrzyOQ) whose backing store **is** the mapped file data is created. + +When the returned [`CGImageRef`](http://www.google.com/url?sa=t&rct=j&q=&esrc=s&source=web&cd=1&cad=rja&ved=0CCwQFjAA&url=http%3A%2F%2Fdeveloper.apple.com%2Flibrary%2Fios%2Fdocumentation%2Fgraphicsimaging%2FReference%2FCGImage%2FReference%2Freference.html&ei=fG9XUpX_BqWqigLymIG4BQ&usg=AFQjCNHTelntXU5Gw0BQkQqj9HC5iZibyA&sig2=tLY7PDhyockUVlVFbrzyOQ) (wrapped into a [`UIImage`](http://www.google.com/url?sa=t&rct=j&q=&esrc=s&source=web&cd=1&cad=rja&ved=0CC0QFjAA&url=http%3A%2F%2Fdeveloper.apple.com%2Flibrary%2Fios%2Fdocumentation%2Fuikit%2Freference%2FUIImage_Class%2F&ei=lG9XUtTdJIm9iwKDq4CACA&usg=AFQjCNEa2LN2puQYOfBRVPaEsvsSawOVMg&sig2=0TzbC6wzT5EdynHsDMIEUw)) is ready to be drawn to the screen, iOS's virtual memory system pages in the actual file data. This is another benefit of using mapped memory; the VM system will automatically handle the memory management for us. In addition, mapped memory "doesn't count" toward an application's real memory usage. + +In like manner, when image data is being stored in an image table, a memory-mapped bitmap context is created. Along with the original image, this context is passed to an image table's corresponding entity object. This object is responsible for drawing the image into the current context, optionally further configuring the context (e.g., clipping the context to a rounded rect) or doing any additional drawing (e.g., drawing an overlay image atop the original image). [`mmap`](https://developer.apple.com/library/ios/documentation/system/conceptual/manpages_iphoneos/man2/mmap.2.html) marshals the drawn image data to disk, so no image buffer is allocated in memory. + +#### Uncompressed Image Data + +In order to avoid expensive image decompression operations, image tables store uncompressed image data in their files. If a source image is compressed, it must first be decompressed for the image table to work with it. **This is a one-time cost.** Furthermore, it is possible to [utilize image format families](#working-with-image-format-families) to perform this decompression exactly once for a collection of similar image formats. + +There are obvious consequences to this approach, however. Uncompressed image data requires more disk space, and the difference between compressed and uncompressed file sizes can be significant, especially for image formats like JPEG. For this reason, **Fast Image Cache works best with smaller images**, although there is no API restriction that enforce this. + +#### Byte Alignment + +For high-performance scrolling, it is critical that Core Animation is able to use an image without first having to create a copy. One of the reasons Core Animation would create a copy of an image is improper byte-alignment of the image's underlying CGImageRef. A properly aligned bytes-per-row value must be a multiple of (8 pixels * bytes per pixel). For a typical ARGB image, the aligned bytes-per-row value is a multiple of 32. Every image table is configured such that each image is always properly byte-aligned for Core Animation from the start. As a result, when images are retrieved from an image table, they are already in a form that Core Animation can work with directly without having to create a copy. + +## Considerations + +### Image Table Size + +Image tables are configured by image formats, which specify (among other things) the maximum number of entries (i.e., individual images) an image table can have. This is to prevent the size of an image table file from growing arbitrarily. + +Image tables allocate 4 bytes per pixel, so the maximum space occupied by an image table file can be determined as follows: + +`4 bytes per pixel` × `image width in pixels` × `image height in pixels` × `maximum number of entries` + +Applications using Fast Image Cache should carefully consider how many images each image table should contain. When a new image is stored in an image table that is already full, it will replace the least-recently-accessed image. + +### Image Table Transience + +Image table files are stored in the user's caches directory in a subdirectory called `ImageTables`. iOS can remove cached files at any time to free up disk space, so applications using Fast Image Cache must be able to recreate any stored images and should not rely on image table files persisting forever. + +> **Note**: As a reminder, data stored in a user's caches directory is not backed up to iTunes or iCloud. + +### Source Image Persistence + +Fast Image Cache does not persist the original source images processed by entities to create the image data stored in its image tables. + +For example, if an original image is resized by an entity to create a thumbnail to be stored in an image table, it is the application's responsibility to either persist the original image or be able to retrieve or recreate it again. + +Image format families can be specified to efficiently make use of a single source image. See [Working with Image Format Families](#working-with-image-format-families) for more information. + +## Requirements + +Fast Image Cache requires iOS 5.0 or greater and relies on the following frameworks: + +- Foundation +- Core Graphics +- UIKit + +> **Note**: Fast Image Cache does **not** use ARC. If your project uses ARC, you must configure Xcode to [disable ARC for Fast Image Cache source files](http://stackoverflow.com/questions/6646052/how-can-i-disable-arc-for-a-single-file-in-a-project). + +--- + +The `FastImageCacheDemo` Xcode project requires Xcode 5.0 or greater and is configured to deploy against iOS 6.0. + +## Getting Started + +### Integrating Fast Image Cache + +- Clone this repository, or [download the latest archive of `master`](https://github.com/path/FastImageCache/archive/master.zip). +- From the `FastImageCache` root directory, copy the source files from the inner [`FastImageCache`](./FastImageCache) subdirectory to your Xcode project. +- Import [`FICImageCache.h`](./FastImageCache/FICImageCache.h) wherever you use the image cache. +- Import [`FICEntity.h`](./FastImageCache/FICEntity.h) for each class that conforms to [`FICEntity`](https://s3.amazonaws.com/fast-image-cache/documentation/Protocols/FICEntity.html). + +### Initial Configuration + +Before the image cache can be used, it needs to be configured. This must occur each launch, so the application delegate might be a good place to do this. + +#### Creating Image Formats + +Each image format corresponds to an image table that the image cache will use. Image formats that can use the same source image to render the images they store in their image tables should belong to the same [image format family](#working-with-image-format-families). See [Image Table Size](#image-table-size) for more information about how to determine an appropriate maximum count. + +```objective-c +FICImageFormat *smallUserThumbnailImageFormat = [[[FICImageFormat alloc] init] autorelease]; +smallUserThumbnailImageFormat.name = XXImageFormatNameUserThumbnailSmall; +smallUserThumbnailImageFormat.family = XXImageFormatFamilyUserThumbnails; +smallUserThumbnailImageFormat.imageSize = CGSizeMake(50, 50); +smallUserThumbnailImageFormat.opaque = YES; +smallUserThumbnailImageFormat.maximumCount = 250; +smallUserThumbnailImageFormat.devices = FICImageFormatDevicePhone; + +FICImageFormat *mediumUserThumbnailImageFormat = [[[FICImageFormat alloc] init] autorelease]; +smallUserThumbnailImageFormat.name = XXImageFormatNameUserThumbnailMedium; +smallUserThumbnailImageFormat.family = XXImageFormatFamilyUserThumbnails; +smallUserThumbnailImageFormat.imageSize = CGSizeMake(100, 100); +smallUserThumbnailImageFormat.opaque = YES; +smallUserThumbnailImageFormat.maximumCount = 250; +smallUserThumbnailImageFormat.devices = FICImageFormatDevicePhone; + +NSArray *imageFormats = @[smallUserThumbnailImageFormat, mediumUserThumbnailImageFormat]; +``` + +#### Configuring the Image Cache + +Once one or more image formats have been defined, they need to be assigned to the image cache. Aside from assigning the image cache's delegate, there is nothing further that can be configured on the image cache itself. + +```objective-c +FICImageCache *sharedImageCache = [FICImageCache sharedImageCache]; +sharedImageCache.delegate = self; +sharedImageCache.formats = imageFormats; +``` + +#### Creating Entities + +Entities are objects that conform to the [`FICEntity`](https://s3.amazonaws.com/fast-image-cache/documentation/Protocols/FICEntity.html) protocol. Entities uniquely identify entries in an image table, and they are also responsible for drawing the images they wish to store in the image cache. Applications that already have model objects defined (perhaps managed by Core Data) are usually appropriate entity candidates. + +```objective-c +@interface XXUser : NSObject + +@property (nonatomic, assign, getter = isActive) BOOL active; +@property (nonatomic, copy) NSString *userID; +@property (nonatomic, copy) NSURL *userPhotoURL; + +@end +``` + +Here is an example implementation of the [`FICEntity`](https://s3.amazonaws.com/fast-image-cache/documentation/Protocols/FICEntity.html) protocol. + +```objective-c +- (NSString *)UUID { + return _userID; +} + +- (NSString *)sourceImageUUID { + return [_userPhotoURL absoluteString]; +} + +- (NSURL *)sourceImageURLWithFormatName:(NSString *)formatName { + return _sourceImageURL; +} + +- (FICEntityImageDrawingBlock)drawingBlockForImage:(UIImage *)image withFormatName:(NSString *)formatName { + FICEntityImageDrawingBlock drawingBlock = ^(CGContextRef context, CGSize contextSize) { + CGRect contextBounds = CGRectZero; + contextBounds.size = contextSize; + CGContextClearRect(context, contextBounds); + + // Clip medium thumbnails so they have rounded corners + if ([formatName isEqualToString:XXImageFormatNameUserThumbnailMedium]) { + UIBezierPath clippingPath = [self _clippingPath]; + [clippingPath addClip]; + } + + UIGraphicsPushContext(context); + [image drawInRect:contextBounds]; + UIGraphicsPopContext(); + }; + + return [[drawingBlock copy] autorelease]; +} +``` + +Ideally, an entity's [`UUID`](https://s3.amazonaws.com/fast-image-cache/documentation/Protocols/FICEntity.html#//api/name/UUID) should never change. This is why it corresponds nicely with a model object's server-generated ID in the case where an application is working with resources retrieved from an API. + +An entity's [`sourceImageUUID`](https://s3.amazonaws.com/fast-image-cache/documentation/Protocols/FICEntity.html#//api/name/sourceImageUUID) *can* change. For example, if a user updates their profile photo, the URL to that photo should change as well. The [`UUID`](https://s3.amazonaws.com/fast-image-cache/documentation/Protocols/FICEntity.html#//api/name/UUID) remains the same and identifies the same user, but the changed profile photo URL will indicate that there is a new source image. + +> **Note**: Often, it is best to hash whatever identifiers are being used to define [`UUID`](https://s3.amazonaws.com/fast-image-cache/documentation/Protocols/FICEntity.html#//api/name/UUID) and [`sourceImageUUID`](https://s3.amazonaws.com/fast-image-cache/documentation/Protocols/FICEntity.html#//api/name/sourceImageUUID). Fast Image Cache provides utility functions to do this. Because hashing can be expensive, it is recommended that the hash be computed only once (or only when the identifier changes) and stored in an instance variable. + +When the image cache is asked to provide an image for a particular entity and format name, the entity is responsible for providing a URL. The URL need not even point to an actual resource—e.g., the URL might be constructed of a custom URL-scheme—, but it must be a valid URL. + +The image cache uses these URLs merely to keep track of which image requests are already in flight; multiple requests to the image cache for the same image are handled correctly without any wasted effort. The choice to use URLs as a basis for keying image cache requests actually complements many real-world application designs whereby URLs to image resources (rather than the images themselves) are included with server-provided model data. + +> **Note**: Fast Image Cache does not provide any mechanism for making network requests. This is the responsibility of the image cache's delegate. + +Finally, once the source image is available, the entity is asked to provide a drawing block. The image table that will store the final image sets up a file-mapped bitmap context and invokes the entity's drawing block. This makes it convenient for each entity to decide how to process the source image for particular image formats. + +### Requesting Images from the Image Cache + +Fast Image Cache works under the on-demand, lazy-loading design pattern common to Cocoa. + +```objective-c +XXUser *user = [self _currentUser]; +NSString *formatName = XXImageFormatNameUserThumbnailSmall; +FICImageCacheCompletionBlock completionBlock = ^(id entity, NSString *formatName, UIImage *image) { + _imageView.image = image; + [_imageView.layer addAnimation:[CATransition animation] forKey:kCATransition]; +}; + +BOOL imageExists = [sharedImageCache retrieveImageForEntity:user withFormatName:formatName completionBlock:completionBlock]; + +if (imageExists == NO) { + _imageView.image = [self _userPlaceholderImage]; +} +``` + +There are a few things to note here. + +1. Note that it is an entity and an image format name that uniquely identifies the desired image in the image cache. As a format name uniquely identifies an image table, the entity alone uniquely identifies the desired image data in an image table. +1. The image cache never returns a [`UIImage`](http://www.google.com/url?sa=t&rct=j&q=&esrc=s&source=web&cd=1&cad=rja&ved=0CC0QFjAA&url=http%3A%2F%2Fdeveloper.apple.com%2Flibrary%2Fios%2Fdocumentation%2Fuikit%2Freference%2FUIImage_Class%2F&ei=lG9XUtTdJIm9iwKDq4CACA&usg=AFQjCNEa2LN2puQYOfBRVPaEsvsSawOVMg&sig2=0TzbC6wzT5EdynHsDMIEUw) directly. The requested image is included in the completion block. The return value will indicate whether or not the image already exists in the image cache. +1. [`-retrieveImageForEntity:withFormatName:completionBlock:`](https://s3.amazonaws.com/fast-image-cache/documentation/Classes/FICImageCache.html#//api/name/retrieveImageForEntity:withFormatName:completionBlock:) is a synchronous method. If the requested image already exists in the image cache, the completion block will be called immediately. There is an asynchronous counterpart to this method called [`-asynchronouslyRetrieveImageForEntity:withFormatName:completionBlock:`](https://s3.amazonaws.com/fast-image-cache/documentation/Classes/FICImageCache.html#//api/name/asynchronouslyRetrieveImageForEntity:withFormatName:completionBlock:). +1. If a requested image does **not** already exist in the image cache, then the image cache invokes the necessary actions to request the source image for its delegate. Afterwards, perhaps some time later, the completion block will be called. + +> **Note**: The distinction of synchronous and asynchronous only applies to the process of retrieving an image that already exists in the image cache. In the case where a synchronous image request is made for an image that does not already exist in the image case, the image cache does **not** block the calling thread until it has an image. The retrieval method will immediately return `NO`, and the completion block will be called later. +> +> See the [`FICImageCache` class reference](https://s3.amazonaws.com/fast-image-cache/documentation/Classes/FICImageCache.html) for a thorough explanation of how the execution lifecycle works for image retrieval, especially as it relates to the handling of the completion blocks. + +### Providing Source Images to the Image Cache + +There are two ways to provide source images to the image cache. + +1. **On Demand**: This is the preferred method. The image cache's delegate is responsible for supplying the image cache with source images. + + ```objective-c + - (void)imageCache:(FICImageCache *)imageCache wantsSourceImageForEntity:(id)entity withFormatName:(NSString *)formatName completionBlock:(FICImageRequestCompletionBlock)completionBlock { + dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{ + // Fetch the desired source image by making a network request + NSURL *requestURL = [entity sourceImageURLWithFormatName:formatName]; + UIImage *sourceImage = [self _sourceImageForURL:requestURL]; + + dispatch_async(dispatch_get_main_queue(), ^{ + completionBlock(sourceImage); + }); + }); + } + ``` + + This is where the URL-based nature of how the image cache manages image requests is convenient. First, an image retrieval request to the image cache for an image that is already being handled by the image cache's delegate—e.g., waiting on a large image to be downloaded—is simply added to the first request's array of completion blocks. Second, if source images are downloaded from the Internet (as is often the case), the URL for such a network request is readily available. + + > **Note**: The completion block must be called on the main thread. Fast Image Cache is architected such that this call will not block the main thread, as processing sources image is handled in the image cache's own serial dispatch queue. + +2. **Manually**: It is possible to manually insert image data into the image cache. + + ```objective-c + // Just finished downloading new user photo + + XXUser *user = [self _currentUser]; + NSString *formatName = XXImageFormatNameUserThumbnailSmall; + FICImageCacheCompletionBlock completionBlock = ^(id entity, NSString *formatName, UIImage *image) { + NSLog(@"Processed and stored image for entity: %@", entity); + }; + + [sharedImageCache setImage:newUserPhoto forEntity:user withFormatName:formatName completionBlock:completionBlock]; + ``` + +> **Note**: Fast Image Cache does **not** persist source images. See [Source Image Persistence](#source-image-persistence) for more information. + +### Working with Image Format Families + +The advantage of classifying image formats into families is that the image cache's delegate can tell the image cache to process entity source images for **all** image formats in a family when **any** image format in that family is processed. + +```objective-c +- (BOOL)imageCache:(FICImageCache *)imageCache shouldProcessAllFormatsInFamily:(NSString *)formatFamily forEntity:(id)entity { + BOOL shouldProcessAllFormats = NO; + + if ([formatFamily isEqualToString:XXImageFormatFamilyUserThumbnails]) { + XXUser *user = (XXUser *)entity; + shouldProcessAllFormats = user.active; + } + + return shouldProcessAllFormats; +} +``` + +The advantage of processing all image formats in a family at once is that the source image does not need to be repeatedly downloaded (or loaded into memory if cached on disk). + +For example, if a user changes their profile photo, it probably makes sense to process the new source image for every variant at the same time that the first image format is processed. That is, if the image cache is processing a new user profile photo for the image format named `XXImageFormatNameUserThumbnailSmall`, then it makes sense to also process and store new image data for that same user for the image format named `XXImageFormatNameUserThumbnailMedium`. + +## Documentation + +Fast Image Cache's header files are fully documented, and [appledoc](http://gentlebytes.com/appledoc/)-generated documentation is available in [HTML form](https://s3.amazonaws.com/fast-image-cache/documentation/index.html). + +Fast Image Cache's implementation files are minimally documented where special attention needs to be raised. Otherwise, every effort was made to write self-documenting code. + +## Demo Application + +Included with this repository is a demo app Xcode project. It demonstrates the difference between the conventional approach for loading and displaying images and the Fast Image Cache approach. See the [requirements for running the demo app Xcode project](#requirements). + +> **Note**: The demo application must either be supplied with JPEG images, or the included [`fetch_demo_images.sh`](./FastImageCacheDemo/fetch_demo_images.sh) script in the [`FastImageCacheDemo`](./FastImageCacheDemo) directory must be run. + +### Video + +

+ Fast Image Cache Demo App Video +

+ +> **Note**: This video of the demo application was captured on an iPad mini via AirPlay. AirPlay has a maximum output framerate of 30FPS, so it is not possible to accurately capture an application scrolling at 60FPS. However, the application has an average FPS indicator to display the actual, on-device render framerate. +> +> Outputting to AirPlay also incurs a performance penalty, so overall demonstrated scrolling performance is lessened. + +### Statistics + +The following statistics were measured from a run of the demo application: + +| Method | Scrolling Performance | Disk Usage | [RPRVT](http://www.mikeash.com/pyblog/friday-qa-2009-06-19-mac-os-x-process-memory-statistics.html)1 +| ---------------- |:-----------------------:|:------------:|:-----------------------------:| +| Conventional | `~35FPS` | `568KB` | `2.40MB`: `1.06MB` + `1.34MB` | +| Fast Image Cache | `~59FPS` | `2.2MB` | `1.15MB`: `1.06MB` + `0.09MB` | + +The takeaway is that Fast Image Cache sacrifices disk usage to achieve a faster framerate and overall less memory usage. + +--- +1 The first value is the the total RPRVT used by a method to display a screen's worth of JPEG thumbnails. The second value is the baseline RPRVT where all the table view cells and image views are on screen, but none of the image views have images set. The third value is how much additional RPRVT each method used beyond the baseline. + +## Contributors + +Mallory Paine +**Mallory Paine** — Author and Original API Design +@mallorypaine + +Mallory is the Director of Mobile Engineering at Path, previously a member of the original iOS team at Apple. + +--- + +Michael Potter +**Michael Potter** — Documentation and API Refactoring +@LucasTizma + +Michael is an iOS Engineer at Path and has been developing iOS applications since the SDK's public release. + +## Credits + +All [demo application](#demo-application) photos were taken from [morgueFile](http://www.morguefile.com) and are used according to the [morgueFile license](http://www.morguefile.com/license/full). + +## License + +Fast Image Cache is made available under the [MIT license](http://opensource.org/licenses/MIT): + +
+The MIT License (MIT)
+
+Copyright (c) 2013 Path, Inc.
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in
+all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+THE SOFTWARE.
+