commit 9cc8f904ab081f5d5a1a6e97ab3aa658a28dc3b5 Author: Olivier Poitrey Date: Sat Sep 19 20:45:42 2009 +0200 Initial revision diff --git a/DMImageCache.h b/DMImageCache.h new file mode 100644 index 00000000..05798f76 --- /dev/null +++ b/DMImageCache.h @@ -0,0 +1,27 @@ +// +// DMImageCache.h +// Dailymotion +// +// Created by Olivier Poitrey on 19/09/09. +// Copyright 2009 Dailymotion. All rights reserved. +// + +#import + +@interface DMImageCache : NSObject +{ + NSMutableDictionary *cache; + NSString *diskCachePath; +} + ++ (DMImageCache *)sharedImageCache; +- (void)storeImage:(UIImage *)image forKey:(NSString *)key; +- (void)storeImage:(UIImage *)image forKey:(NSString *)key toDisk:(BOOL)toDisk; +- (UIImage *)imageFromKey:(NSString *)key; +- (UIImage *)imageFromKey:(NSString *)key fromDisk:(BOOL)fromDisk; +- (void)removeImageForKey:(NSString *)key; +- (void)clearMemory; +- (void)clearDisk; +- (void)cleanDisk; + +@end diff --git a/DMImageCache.m b/DMImageCache.m new file mode 100644 index 00000000..8d78b5c7 --- /dev/null +++ b/DMImageCache.m @@ -0,0 +1,173 @@ +// +// DMImageCache.m +// Dailymotion +// +// Created by Olivier Poitrey on 19/09/09. +// Copyright 2009 Dailymotion. All rights reserved. +// + +#import "DMImageCache.h" +#import + +static NSInteger kMaxCacheAge = 60*60*24*7; // 1 week + +static DMImageCache *instance; + +@implementation DMImageCache + +#pragma mark NSObject + +- (id)init +{ + if (self = [super init]) + { + cache = [[NSMutableDictionary alloc] init]; + + [[NSNotificationCenter defaultCenter] addObserver:self + selector:@selector(didReceiveMemoryWarning:) + name:UIApplicationDidReceiveMemoryWarningNotification + object:nil]; + + [[NSNotificationCenter defaultCenter] addObserver:self + selector:@selector(willTerminate) + name:UIApplicationWillTerminateNotification + object:nil]; + + // Init the cache + NSArray *paths = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES); + diskCachePath = [[[paths objectAtIndex:0] stringByAppendingPathComponent:@"ImageCache"] retain]; + + if (![[NSFileManager defaultManager] fileExistsAtPath:diskCachePath]) + { + [[NSFileManager defaultManager] createDirectoryAtPath:diskCachePath attributes:nil]; + } + } + + return self; +} + +- (void)dealloc +{ + [cache release]; + + [[NSNotificationCenter defaultCenter] removeObserver:self + name:UIApplicationDidReceiveMemoryWarningNotification + object:nil]; + + [[NSNotificationCenter defaultCenter] removeObserver:self + name:UIApplicationWillTerminateNotification + object:nil]; + + [super dealloc]; +} + +- (void)didReceiveMemoryWarning:(void *)object +{ + [self clearMemory]; +} + +- (void)willTerminate +{ + [self cleanDisk]; +} + +#pragma mark ImageCache (private) + +- (NSString *)cachePathForKey:(NSString *)key +{ + const char *str = [key UTF8String]; + unsigned char r[CC_MD5_DIGEST_LENGTH]; + CC_MD5(str, strlen(str), r); + NSString *filename = [NSString stringWithFormat:@"%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x", + 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]]; + + return [diskCachePath stringByAppendingPathComponent:filename]; +} + +#pragma mark ImageCache + ++ (DMImageCache *)sharedImageCache +{ + if (instance == nil) + { + instance = [[DMImageCache alloc] init]; + } + + return instance; +} + +- (void)storeImage:(UIImage *)image forKey:(NSString *)key +{ + [self storeImage:image forKey:key toDisk:YES]; +} + +- (void)storeImage:(UIImage *)image forKey:(NSString *)key toDisk:(BOOL)toDisk +{ + if (image == nil) + { + return; + } + + [cache setObject:image forKey:key]; + + if (toDisk) + { + [[NSFileManager defaultManager] createFileAtPath:[self cachePathForKey:key] contents:UIImageJPEGRepresentation(image, 1.0) attributes:nil]; + } +} + +- (UIImage *)imageFromKey:(NSString *)key +{ + return [self imageFromKey:key fromDisk:YES]; +} + +- (UIImage *)imageFromKey:(NSString *)key fromDisk:(BOOL)fromDisk +{ + UIImage *image = [cache objectForKey:key]; + + if (!image && fromDisk) + { + image = [[UIImage alloc] initWithData:[NSData dataWithContentsOfFile:[self cachePathForKey:key]]]; + if (image != nil) + { + [cache setObject:image forKey:key]; + [image release]; + } + } + + return image; +} + +- (void)removeImageForKey:(NSString *)key +{ + [cache removeObjectForKey:key]; + [[NSFileManager defaultManager] removeItemAtPath:[self cachePathForKey:key] error:nil]; +} + +- (void)clearMemory +{ + [cache removeAllObjects]; +} + +- (void)clearDisk +{ + [[NSFileManager defaultManager] removeItemAtPath:diskCachePath error:nil]; + [[NSFileManager defaultManager] createDirectoryAtPath:diskCachePath attributes:nil]; +} + +- (void)cleanDisk +{ + NSDate *expirationDate = [NSDate dateWithTimeIntervalSinceNow:-kMaxCacheAge]; + NSDirectoryEnumerator *fileEnumerator = [[NSFileManager defaultManager] enumeratorAtPath:diskCachePath]; + for (NSString *fileName in fileEnumerator) + { + NSString *filePath = [diskCachePath stringByAppendingPathComponent:fileName]; + NSDictionary *attrs = [[NSFileManager defaultManager] attributesOfItemAtPath:filePath error:nil]; + if ([[[attrs fileModificationDate] laterDate:expirationDate] isEqualToDate:expirationDate]) + { + [[NSFileManager defaultManager] removeItemAtPath:filePath error:nil]; + } + } +} + +@end diff --git a/DMWebImageView.h b/DMWebImageView.h new file mode 100644 index 00000000..b1af0b00 --- /dev/null +++ b/DMWebImageView.h @@ -0,0 +1,35 @@ +// +// DMWebImageView.h +// Dailymotion +// +// Created by Olivier Poitrey on 18/09/09. +// Copyright 2009 Dailymotion. All rights reserved. +// + +#import + +@class DMWebImageDownloadOperation; + +@interface DMWebImageView : UIImageView +{ + UIImage *placeHolderImage; + DMWebImageDownloadOperation *currentOperation; +} + +- (void)setImageWithURL:(NSURL *)url; +- (void)downloadFinishedWithImage:(UIImage *)image; + +@end + +@interface DMWebImageDownloadOperation : NSOperation +{ + NSURL *url; + DMWebImageView *delegate; +} + +@property (retain) NSURL *url; +@property (assign) DMWebImageView *delegate; + +- (id)initWithURL:(NSURL *)url delegate:(DMWebImageView *)delegate; + +@end \ No newline at end of file diff --git a/DMWebImageView.m b/DMWebImageView.m new file mode 100644 index 00000000..a86c9663 --- /dev/null +++ b/DMWebImageView.m @@ -0,0 +1,138 @@ +// +// DMWebImageView.m +// Dailymotion +// +// Created by Olivier Poitrey on 18/09/09. +// Copyright 2009 Dailymotion. All rights reserved. +// + +#import "DMWebImageView.h" +#import "DMImageCache.h" + +static NSOperationQueue *downloadQueue; +static NSOperationQueue *cacheInQueue; + +@implementation DMWebImageView + +- (void)dealloc +{ + [placeHolderImage release]; + [currentOperation release]; + [super dealloc]; +} + +#pragma mark RemoteImageView + +- (void)setImageWithURL:(NSURL *)url +{ + if (currentOperation != nil) + { + [currentOperation cancel]; // remove from queue + [currentOperation release]; + currentOperation = nil; + } + + // Save the placeholder image in order to re-apply it when view is reused + if (placeHolderImage == nil) + { + placeHolderImage = [self.image retain]; + } + else + { + self.image = placeHolderImage; + } + + UIImage *cachedImage = [[DMImageCache sharedImageCache] imageFromKey:[url absoluteString]]; + + if (cachedImage) + { + self.image = cachedImage; + } + else + { + if (downloadQueue == nil) + { + downloadQueue = [[NSOperationQueue alloc] init]; + [downloadQueue setMaxConcurrentOperationCount:8]; + } + + currentOperation = [[DMWebImageDownloadOperation alloc] initWithURL:url delegate:self]; + [downloadQueue addOperation:currentOperation]; + } +} + +- (void)downloadFinishedWithImage:(UIImage *)anImage +{ + self.image = anImage; + [currentOperation release]; + currentOperation = nil; +} + +@end + +@implementation DMWebImageDownloadOperation + +@synthesize url, delegate; + +- (void)dealloc +{ + [url release]; + [super dealloc]; +} + + +- (id)initWithURL:(NSURL *)anUrl delegate:(DMWebImageView *)aDelegate +{ + if (self = [super init]) + { + self.url = anUrl; + self.delegate = aDelegate; + } + + return self; +} + +- (void)main +{ + if (self.isCancelled) + { + return; + } + + NSData *data = [[NSData alloc] initWithContentsOfURL:url]; + UIImage *image = [[UIImage alloc] initWithData:data]; + [data release]; + + if (!self.isCancelled) + { + [delegate performSelectorOnMainThread:@selector(downloadFinishedWithImage:) withObject:image waitUntilDone:YES]; + } + + if (cacheInQueue == nil) + { + cacheInQueue = [[NSOperationQueue alloc] init]; + [cacheInQueue setMaxConcurrentOperationCount:2]; + } + + NSString *cacheKey = [url absoluteString]; + + DMImageCache *imageCache = [DMImageCache sharedImageCache]; + + // Store image in memory cache NOW, no need to wait for the cache-in operation queue completion + [imageCache storeImage:image forKey:cacheKey toDisk:NO]; + + // Perform the cache-in in another operation queue in order to not block a download operation slot + NSInvocation *cacheInInvocation = [NSInvocation invocationWithMethodSignature:[[imageCache class] instanceMethodSignatureForSelector:@selector(storeImage:forKey:)]]; + [cacheInInvocation setTarget:imageCache]; + [cacheInInvocation setSelector:@selector(storeImage:forKey:)]; + [cacheInInvocation setArgument:&image atIndex:2]; + [cacheInInvocation setArgument:&cacheKey atIndex:3]; + [cacheInInvocation retainArguments]; + NSInvocationOperation *cacheInOperation = [[NSInvocationOperation alloc] initWithInvocation:cacheInInvocation]; + [cacheInQueue addOperation:cacheInOperation]; + [cacheInOperation release]; + + [image release]; +} + +@end \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 00000000..0fde0857 --- /dev/null +++ b/README.md @@ -0,0 +1,6 @@ +Dailymotion Web Image +===================== + +This library provides a drop-in remplacement for UIImageVIew with support for image coming from the web. + +TODO: doc \ No newline at end of file