Authored by Olivier Poitrey

Initial revision

  1 +//
  2 +// DMImageCache.h
  3 +// Dailymotion
  4 +//
  5 +// Created by Olivier Poitrey on 19/09/09.
  6 +// Copyright 2009 Dailymotion. All rights reserved.
  7 +//
  8 +
  9 +#import <Foundation/Foundation.h>
  10 +
  11 +@interface DMImageCache : NSObject
  12 +{
  13 + NSMutableDictionary *cache;
  14 + NSString *diskCachePath;
  15 +}
  16 +
  17 ++ (DMImageCache *)sharedImageCache;
  18 +- (void)storeImage:(UIImage *)image forKey:(NSString *)key;
  19 +- (void)storeImage:(UIImage *)image forKey:(NSString *)key toDisk:(BOOL)toDisk;
  20 +- (UIImage *)imageFromKey:(NSString *)key;
  21 +- (UIImage *)imageFromKey:(NSString *)key fromDisk:(BOOL)fromDisk;
  22 +- (void)removeImageForKey:(NSString *)key;
  23 +- (void)clearMemory;
  24 +- (void)clearDisk;
  25 +- (void)cleanDisk;
  26 +
  27 +@end
  1 +//
  2 +// DMImageCache.m
  3 +// Dailymotion
  4 +//
  5 +// Created by Olivier Poitrey on 19/09/09.
  6 +// Copyright 2009 Dailymotion. All rights reserved.
  7 +//
  8 +
  9 +#import "DMImageCache.h"
  10 +#import <CommonCrypto/CommonDigest.h>
  11 +
  12 +static NSInteger kMaxCacheAge = 60*60*24*7; // 1 week
  13 +
  14 +static DMImageCache *instance;
  15 +
  16 +@implementation DMImageCache
  17 +
  18 +#pragma mark NSObject
  19 +
  20 +- (id)init
  21 +{
  22 + if (self = [super init])
  23 + {
  24 + cache = [[NSMutableDictionary alloc] init];
  25 +
  26 + [[NSNotificationCenter defaultCenter] addObserver:self
  27 + selector:@selector(didReceiveMemoryWarning:)
  28 + name:UIApplicationDidReceiveMemoryWarningNotification
  29 + object:nil];
  30 +
  31 + [[NSNotificationCenter defaultCenter] addObserver:self
  32 + selector:@selector(willTerminate)
  33 + name:UIApplicationWillTerminateNotification
  34 + object:nil];
  35 +
  36 + // Init the cache
  37 + NSArray *paths = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES);
  38 + diskCachePath = [[[paths objectAtIndex:0] stringByAppendingPathComponent:@"ImageCache"] retain];
  39 +
  40 + if (![[NSFileManager defaultManager] fileExistsAtPath:diskCachePath])
  41 + {
  42 + [[NSFileManager defaultManager] createDirectoryAtPath:diskCachePath attributes:nil];
  43 + }
  44 + }
  45 +
  46 + return self;
  47 +}
  48 +
  49 +- (void)dealloc
  50 +{
  51 + [cache release];
  52 +
  53 + [[NSNotificationCenter defaultCenter] removeObserver:self
  54 + name:UIApplicationDidReceiveMemoryWarningNotification
  55 + object:nil];
  56 +
  57 + [[NSNotificationCenter defaultCenter] removeObserver:self
  58 + name:UIApplicationWillTerminateNotification
  59 + object:nil];
  60 +
  61 + [super dealloc];
  62 +}
  63 +
  64 +- (void)didReceiveMemoryWarning:(void *)object
  65 +{
  66 + [self clearMemory];
  67 +}
  68 +
  69 +- (void)willTerminate
  70 +{
  71 + [self cleanDisk];
  72 +}
  73 +
  74 +#pragma mark ImageCache (private)
  75 +
  76 +- (NSString *)cachePathForKey:(NSString *)key
  77 +{
  78 + const char *str = [key UTF8String];
  79 + unsigned char r[CC_MD5_DIGEST_LENGTH];
  80 + CC_MD5(str, strlen(str), r);
  81 + NSString *filename = [NSString stringWithFormat:@"%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x",
  82 + r[0], r[1], r[2], r[3], r[4], r[5], r[6], r[7], r[8], r[9], r[10], r[11], r[12], r[13], r[14], r[15]];
  83 +
  84 + return [diskCachePath stringByAppendingPathComponent:filename];
  85 +}
  86 +
  87 +#pragma mark ImageCache
  88 +
  89 ++ (DMImageCache *)sharedImageCache
  90 +{
  91 + if (instance == nil)
  92 + {
  93 + instance = [[DMImageCache alloc] init];
  94 + }
  95 +
  96 + return instance;
  97 +}
  98 +
  99 +- (void)storeImage:(UIImage *)image forKey:(NSString *)key
  100 +{
  101 + [self storeImage:image forKey:key toDisk:YES];
  102 +}
  103 +
  104 +- (void)storeImage:(UIImage *)image forKey:(NSString *)key toDisk:(BOOL)toDisk
  105 +{
  106 + if (image == nil)
  107 + {
  108 + return;
  109 + }
  110 +
  111 + [cache setObject:image forKey:key];
  112 +
  113 + if (toDisk)
  114 + {
  115 + [[NSFileManager defaultManager] createFileAtPath:[self cachePathForKey:key] contents:UIImageJPEGRepresentation(image, 1.0) attributes:nil];
  116 + }
  117 +}
  118 +
  119 +- (UIImage *)imageFromKey:(NSString *)key
  120 +{
  121 + return [self imageFromKey:key fromDisk:YES];
  122 +}
  123 +
  124 +- (UIImage *)imageFromKey:(NSString *)key fromDisk:(BOOL)fromDisk
  125 +{
  126 + UIImage *image = [cache objectForKey:key];
  127 +
  128 + if (!image && fromDisk)
  129 + {
  130 + image = [[UIImage alloc] initWithData:[NSData dataWithContentsOfFile:[self cachePathForKey:key]]];
  131 + if (image != nil)
  132 + {
  133 + [cache setObject:image forKey:key];
  134 + [image release];
  135 + }
  136 + }
  137 +
  138 + return image;
  139 +}
  140 +
  141 +- (void)removeImageForKey:(NSString *)key
  142 +{
  143 + [cache removeObjectForKey:key];
  144 + [[NSFileManager defaultManager] removeItemAtPath:[self cachePathForKey:key] error:nil];
  145 +}
  146 +
  147 +- (void)clearMemory
  148 +{
  149 + [cache removeAllObjects];
  150 +}
  151 +
  152 +- (void)clearDisk
  153 +{
  154 + [[NSFileManager defaultManager] removeItemAtPath:diskCachePath error:nil];
  155 + [[NSFileManager defaultManager] createDirectoryAtPath:diskCachePath attributes:nil];
  156 +}
  157 +
  158 +- (void)cleanDisk
  159 +{
  160 + NSDate *expirationDate = [NSDate dateWithTimeIntervalSinceNow:-kMaxCacheAge];
  161 + NSDirectoryEnumerator *fileEnumerator = [[NSFileManager defaultManager] enumeratorAtPath:diskCachePath];
  162 + for (NSString *fileName in fileEnumerator)
  163 + {
  164 + NSString *filePath = [diskCachePath stringByAppendingPathComponent:fileName];
  165 + NSDictionary *attrs = [[NSFileManager defaultManager] attributesOfItemAtPath:filePath error:nil];
  166 + if ([[[attrs fileModificationDate] laterDate:expirationDate] isEqualToDate:expirationDate])
  167 + {
  168 + [[NSFileManager defaultManager] removeItemAtPath:filePath error:nil];
  169 + }
  170 + }
  171 +}
  172 +
  173 +@end
  1 +//
  2 +// DMWebImageView.h
  3 +// Dailymotion
  4 +//
  5 +// Created by Olivier Poitrey on 18/09/09.
  6 +// Copyright 2009 Dailymotion. All rights reserved.
  7 +//
  8 +
  9 +#import <UIKit/UIKit.h>
  10 +
  11 +@class DMWebImageDownloadOperation;
  12 +
  13 +@interface DMWebImageView : UIImageView
  14 +{
  15 + UIImage *placeHolderImage;
  16 + DMWebImageDownloadOperation *currentOperation;
  17 +}
  18 +
  19 +- (void)setImageWithURL:(NSURL *)url;
  20 +- (void)downloadFinishedWithImage:(UIImage *)image;
  21 +
  22 +@end
  23 +
  24 +@interface DMWebImageDownloadOperation : NSOperation
  25 +{
  26 + NSURL *url;
  27 + DMWebImageView *delegate;
  28 +}
  29 +
  30 +@property (retain) NSURL *url;
  31 +@property (assign) DMWebImageView *delegate;
  32 +
  33 +- (id)initWithURL:(NSURL *)url delegate:(DMWebImageView *)delegate;
  34 +
  35 +@end
  1 +//
  2 +// DMWebImageView.m
  3 +// Dailymotion
  4 +//
  5 +// Created by Olivier Poitrey on 18/09/09.
  6 +// Copyright 2009 Dailymotion. All rights reserved.
  7 +//
  8 +
  9 +#import "DMWebImageView.h"
  10 +#import "DMImageCache.h"
  11 +
  12 +static NSOperationQueue *downloadQueue;
  13 +static NSOperationQueue *cacheInQueue;
  14 +
  15 +@implementation DMWebImageView
  16 +
  17 +- (void)dealloc
  18 +{
  19 + [placeHolderImage release];
  20 + [currentOperation release];
  21 + [super dealloc];
  22 +}
  23 +
  24 +#pragma mark RemoteImageView
  25 +
  26 +- (void)setImageWithURL:(NSURL *)url
  27 +{
  28 + if (currentOperation != nil)
  29 + {
  30 + [currentOperation cancel]; // remove from queue
  31 + [currentOperation release];
  32 + currentOperation = nil;
  33 + }
  34 +
  35 + // Save the placeholder image in order to re-apply it when view is reused
  36 + if (placeHolderImage == nil)
  37 + {
  38 + placeHolderImage = [self.image retain];
  39 + }
  40 + else
  41 + {
  42 + self.image = placeHolderImage;
  43 + }
  44 +
  45 + UIImage *cachedImage = [[DMImageCache sharedImageCache] imageFromKey:[url absoluteString]];
  46 +
  47 + if (cachedImage)
  48 + {
  49 + self.image = cachedImage;
  50 + }
  51 + else
  52 + {
  53 + if (downloadQueue == nil)
  54 + {
  55 + downloadQueue = [[NSOperationQueue alloc] init];
  56 + [downloadQueue setMaxConcurrentOperationCount:8];
  57 + }
  58 +
  59 + currentOperation = [[DMWebImageDownloadOperation alloc] initWithURL:url delegate:self];
  60 + [downloadQueue addOperation:currentOperation];
  61 + }
  62 +}
  63 +
  64 +- (void)downloadFinishedWithImage:(UIImage *)anImage
  65 +{
  66 + self.image = anImage;
  67 + [currentOperation release];
  68 + currentOperation = nil;
  69 +}
  70 +
  71 +@end
  72 +
  73 +@implementation DMWebImageDownloadOperation
  74 +
  75 +@synthesize url, delegate;
  76 +
  77 +- (void)dealloc
  78 +{
  79 + [url release];
  80 + [super dealloc];
  81 +}
  82 +
  83 +
  84 +- (id)initWithURL:(NSURL *)anUrl delegate:(DMWebImageView *)aDelegate
  85 +{
  86 + if (self = [super init])
  87 + {
  88 + self.url = anUrl;
  89 + self.delegate = aDelegate;
  90 + }
  91 +
  92 + return self;
  93 +}
  94 +
  95 +- (void)main
  96 +{
  97 + if (self.isCancelled)
  98 + {
  99 + return;
  100 + }
  101 +
  102 + NSData *data = [[NSData alloc] initWithContentsOfURL:url];
  103 + UIImage *image = [[UIImage alloc] initWithData:data];
  104 + [data release];
  105 +
  106 + if (!self.isCancelled)
  107 + {
  108 + [delegate performSelectorOnMainThread:@selector(downloadFinishedWithImage:) withObject:image waitUntilDone:YES];
  109 + }
  110 +
  111 + if (cacheInQueue == nil)
  112 + {
  113 + cacheInQueue = [[NSOperationQueue alloc] init];
  114 + [cacheInQueue setMaxConcurrentOperationCount:2];
  115 + }
  116 +
  117 + NSString *cacheKey = [url absoluteString];
  118 +
  119 + DMImageCache *imageCache = [DMImageCache sharedImageCache];
  120 +
  121 + // Store image in memory cache NOW, no need to wait for the cache-in operation queue completion
  122 + [imageCache storeImage:image forKey:cacheKey toDisk:NO];
  123 +
  124 + // Perform the cache-in in another operation queue in order to not block a download operation slot
  125 + NSInvocation *cacheInInvocation = [NSInvocation invocationWithMethodSignature:[[imageCache class] instanceMethodSignatureForSelector:@selector(storeImage:forKey:)]];
  126 + [cacheInInvocation setTarget:imageCache];
  127 + [cacheInInvocation setSelector:@selector(storeImage:forKey:)];
  128 + [cacheInInvocation setArgument:&image atIndex:2];
  129 + [cacheInInvocation setArgument:&cacheKey atIndex:3];
  130 + [cacheInInvocation retainArguments];
  131 + NSInvocationOperation *cacheInOperation = [[NSInvocationOperation alloc] initWithInvocation:cacheInInvocation];
  132 + [cacheInQueue addOperation:cacheInOperation];
  133 + [cacheInOperation release];
  134 +
  135 + [image release];
  136 +}
  137 +
  138 +@end
  1 +Dailymotion Web Image
  2 +=====================
  3 +
  4 +This library provides a drop-in remplacement for UIImageVIew with support for image coming from the web.
  5 +
  6 +TODO: doc