From b4e3d392660c6ed35f09036ca5b8383e1ea9c2e8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rodrigo=20Garci=CC=81a=20S?= Date: Sun, 28 Jul 2013 17:36:57 -0400 Subject: [PATCH] Refactors JMImageCache to use queues instead of GCD to download image operations. Add convenient methods to access image on memory through NSCache --- JMImageCache.h | 2 + JMImageCache.m | 123 +++++++++++++++------------------ JMImageCacheOperation.h | 20 ++++++ JMImageCacheOperation.m | 116 +++++++++++++++++++++++++++++++ UIImageView+JMImageCache.h | 3 +- UIImageView+JMImageCache.m | 135 +++++++++++++++++++++++++++---------- 6 files changed, 290 insertions(+), 109 deletions(-) create mode 100644 JMImageCacheOperation.h create mode 100644 JMImageCacheOperation.m diff --git a/JMImageCache.h b/JMImageCache.h index f527da0..f70ac61 100644 --- a/JMImageCache.h +++ b/JMImageCache.h @@ -31,6 +31,8 @@ - (UIImage *) imageForURL:(NSURL *)url key:(NSString*)key delegate:(id)d; - (UIImage *) imageForURL:(NSURL *)url delegate:(id)d; +- (UIImage *) imageFromMemoryForURL:(NSURL *)url; +- (UIImage *) imageFromMemoryForKey:(NSString *)key; - (UIImage *) imageFromDiskForKey:(NSString *)key; - (UIImage *) imageFromDiskForURL:(NSURL *)url; diff --git a/JMImageCache.m b/JMImageCache.m index 007958e..3f8b87d 100644 --- a/JMImageCache.m +++ b/JMImageCache.m @@ -7,6 +7,8 @@ // #import "JMImageCache.h" +#import +#import "JMImageCacheOperation.h" static inline NSString *JMImageCacheDirectory() { static NSString *_JMImageCacheDirectory; @@ -15,7 +17,7 @@ dispatch_once(&onceToken, ^{ _JMImageCacheDirectory = [[NSHomeDirectory() stringByAppendingPathComponent:@"Library/Caches/JMCache"] copy]; }); - + return _JMImageCacheDirectory; } inline static NSString *keyForURL(NSURL *url) { @@ -29,6 +31,7 @@ @interface JMImageCache () @property (strong, nonatomic) NSOperationQueue *diskOperationQueue; +@property (strong, nonatomic) NSOperationQueue *imageOperationQueue; - (void) _downloadAndWriteImageForURL:(NSURL *)url key:(NSString *)key completionBlock:(void (^)(UIImage *image))completion failureBlock:(void (^)(NSURLRequest *request, NSURLResponse *response, NSError* error))failure; @@ -36,25 +39,29 @@ - (void) _downloadAndWriteImageForURL:(NSURL *)url key:(NSString *)key completio @implementation JMImageCache -@synthesize diskOperationQueue = _diskOperationQueue; +@synthesize diskOperationQueue; +@synthesize imageOperationQueue; + + (JMImageCache *) sharedCache { static JMImageCache *_sharedCache = nil; static dispatch_once_t onceToken; - + dispatch_once(&onceToken, ^{ _sharedCache = [[JMImageCache alloc] init]; }); - + return _sharedCache; } - (id) init { self = [super init]; if(!self) return nil; - + self.diskOperationQueue = [[NSOperationQueue alloc] init]; - + self.imageOperationQueue = [[NSOperationQueue alloc] init]; + self.imageOperationQueue.maxConcurrentOperationCount = 8; + [[NSFileManager defaultManager] createDirectoryAtPath:JMImageCacheDirectory() withIntermediateDirectories:YES attributes:nil @@ -65,70 +72,27 @@ - (id) init { - (void) _downloadAndWriteImageForURL:(NSURL *)url key:(NSString *)key completionBlock:(void (^)(UIImage *image))completion failureBlock:(void (^)(NSURLRequest *request, NSURLResponse *response, NSError* error))failure { if (!key && !url) return; - + if (!key) { key = keyForURL(url); } - - dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{ - - NSURLRequest* request = [NSURLRequest requestWithURL:url]; - NSURLResponse* response = nil; - NSError* error = nil; - NSData* data = [NSURLConnection sendSynchronousRequest:request returningResponse:&response error:&error]; - - if (error) - { - dispatch_async(dispatch_get_main_queue(), ^{ - - if(failure) failure(request, response, error); - }); - return; - } - - UIImage *i = [[UIImage alloc] initWithData:data]; - if (!i) - { - NSMutableDictionary *errorDetail = [NSMutableDictionary dictionary]; - [errorDetail setValue:[NSString stringWithFormat:@"Failed to init image with data from for URL: %@", url] forKey:NSLocalizedDescriptionKey]; - NSError* error = [NSError errorWithDomain:@"JMImageCacheErrorDomain" code:1 userInfo:errorDetail]; - dispatch_async(dispatch_get_main_queue(), ^{ - - if(failure) failure(request, response, error); - }); - } - else - { - NSString *cachePath = cachePathForKey(key); - NSInvocation *writeInvocation = [NSInvocation invocationWithMethodSignature:[self methodSignatureForSelector:@selector(writeData:toPath:)]]; - - [writeInvocation setTarget:self]; - [writeInvocation setSelector:@selector(writeData:toPath:)]; - [writeInvocation setArgument:&data atIndex:2]; - [writeInvocation setArgument:&cachePath atIndex:3]; - - [self performDiskWriteOperation:writeInvocation]; - [self setImage:i forKey:key]; - - dispatch_async(dispatch_get_main_queue(), ^{ - if(completion) completion(i); - }); - } - }); + + JMImageCacheOperation *op = [[JMImageCacheOperation alloc] initWithURL:url aSuccessBlock:completion andFailedBlock:failure]; + [self.imageOperationQueue addOperation:op]; } - (void) removeAllObjects { [super removeAllObjects]; - + dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{ NSFileManager *fileMgr = [NSFileManager defaultManager]; NSError *error = nil; NSArray *directoryContents = [fileMgr contentsOfDirectoryAtPath:JMImageCacheDirectory() error:&error]; - + if (error == nil) { for (NSString *path in directoryContents) { NSString *fullPath = [JMImageCacheDirectory() stringByAppendingPathComponent:path]; - + BOOL removeSuccess = [fileMgr removeItemAtPath:fullPath error:&error]; if (!removeSuccess) { //Error Occured @@ -141,13 +105,13 @@ - (void) removeAllObjects { } - (void) removeObjectForKey:(id)key { [super removeObjectForKey:key]; - + dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{ NSFileManager *fileMgr = [NSFileManager defaultManager]; NSString *cachePath = cachePathForKey(key); - + NSError *error = nil; - + BOOL removeSuccess = [fileMgr removeItemAtPath:cachePath error:&error]; if (!removeSuccess) { //Error Occured @@ -159,9 +123,9 @@ - (void) removeObjectForKey:(id)key { #pragma mark Getter Methods - (void) imageForURL:(NSURL *)url key:(NSString *)key completionBlock:(void (^)(UIImage *image))completion failureBlock:(void (^)(NSURLRequest *request, NSURLResponse *response, NSError* error))failure{ - + UIImage *i = [self cachedImageForKey:key]; - + if(i) { if(completion) completion(i); } else { @@ -173,20 +137,30 @@ - (void) imageForURL:(NSURL *)url completionBlock:(void (^)(UIImage *image))comp [self imageForURL:url key:keyForURL(url) completionBlock:completion failureBlock:(failure)]; } -- (UIImage *) cachedImageForKey:(NSString *)key { +- (UIImage *)imageFromMemoryForURL:(NSURL *)url { + return [super objectForKey:keyForURL(url)]; +} + +- (UIImage *) imageFromMemoryForKey:(NSString *)key { if(!key) return nil; + + return [super objectForKey:key]; +} +- (UIImage *) cachedImageForKey:(NSString *)key { + if(!key) return nil; + id returner = [super objectForKey:key]; - + if(returner) { return returner; } else { UIImage *i = [self imageFromDiskForKey:key]; if(i) [self setImage:i forKey:key]; - + return i; } - + return nil; } @@ -197,9 +171,9 @@ - (UIImage *) cachedImageForURL:(NSURL *)url { - (UIImage *) imageForURL:(NSURL *)url key:(NSString*)key delegate:(id)d { if(!url) return nil; - + UIImage *i = [self cachedImageForURL:url]; - + if(i) { return i; } else { @@ -213,9 +187,9 @@ - (UIImage *) imageForURL:(NSURL *)url key:(NSString*)key delegate:(id)d { - (UIImage *) imageFromDiskForKey:(NSString *)key { UIImage *i = [[UIImage alloc] initWithData:[NSData dataWithContentsOfFile:cachePathForKey(key) options:0 error:NULL]]; - return i; + return i; } - (UIImage *) imageFromDiskForURL:(NSURL *)url { @@ -255,6 +229,15 @@ - (void) removeImageForURL:(NSURL *)url { - (void) writeData:(NSData*)data toPath:(NSString *)path { [data writeToFile:path atomically:YES]; + NSString *cacheName = @"memory-cache"; + + NSDictionary *properties = [[NSFileManager defaultManager] attributesOfItemAtPath:path error:nil]; + + NSNumber *actualCacheSize = [[NSUserDefaults standardUserDefaults] objectForKey:cacheName]; + NSNumber *fileSize = [NSNumber numberWithUnsignedLong:([properties fileSize] + actualCacheSize.longValue)]; + + [[NSUserDefaults standardUserDefaults] setObject:fileSize forKey:cacheName]; + } - (void) performDiskWriteOperation:(NSInvocation *)invoction { NSInvocationOperation *operation = [[NSInvocationOperation alloc] initWithInvocation:invoction]; diff --git a/JMImageCacheOperation.h b/JMImageCacheOperation.h new file mode 100644 index 0000000..3e3f4d3 --- /dev/null +++ b/JMImageCacheOperation.h @@ -0,0 +1,20 @@ +// +// JMImageCacheOperation.h +// JMCache +// +// Created by Rodrigo Garcia on 7/10/13. +// Copyright 2011 Jake Marsh. All rights reserved. +// + +#import +typedef void (^JMImageCacheSuccessBlock) (UIImage *image); +typedef void (^JMImageCacheFailedBlock) (NSURLRequest *request, NSURLResponse *response, NSError* error); + +@interface JMImageCacheOperation : NSOperation + +@property(copy,nonatomic) JMImageCacheSuccessBlock successBlock; +@property(copy,nonatomic) JMImageCacheFailedBlock failedBlock; + +-(id)initWithURL:(NSURL *)aURL aSuccessBlock:(JMImageCacheSuccessBlock)aSuccessBlock andFailedBlock:(JMImageCacheFailedBlock)aFailedBlock; + +@end diff --git a/JMImageCacheOperation.m b/JMImageCacheOperation.m new file mode 100644 index 0000000..573e4d7 --- /dev/null +++ b/JMImageCacheOperation.m @@ -0,0 +1,116 @@ +// +// JMImageCacheOperation.m +// JMCache +// +// Created by Rodrigo Garcia on 7/10/13. +// +// + +#import "JMImageCacheOperation.h" + +static inline NSString *JMImageCacheDirectory() { + static NSString *_JMImageCacheDirectory; + static dispatch_once_t onceToken; + + dispatch_once(&onceToken, ^{ + _JMImageCacheDirectory = [[NSHomeDirectory() stringByAppendingPathComponent:@"Library/Caches/JMCache"] copy]; + }); + + return _JMImageCacheDirectory; +} +inline static NSString *keyForURL(NSURL *url) { + return [url absoluteString]; +} +static inline NSString *cachePathForKey(NSString *key) { + NSString *fileName = [NSString stringWithFormat:@"JMImageCache-%u", [key hash]]; + return [JMImageCacheDirectory() stringByAppendingPathComponent:fileName]; +} + + +@interface JMImageCacheOperation () + +@property(copy,nonatomic) NSURL *url; +@property(strong,nonatomic) UIImage *imageOperation; +@property(strong,nonatomic) NSData *dataRepresentation; + +@end + +@implementation JMImageCacheOperation + +-(id)initWithURL:(NSURL *)aURL aSuccessBlock:(JMImageCacheSuccessBlock)aSuccessBlock andFailedBlock:(JMImageCacheFailedBlock)aFailedBlock +{ + if(self=[super init]) + { + self.url = aURL; + self.successBlock = aSuccessBlock; + self.failedBlock = aFailedBlock; + } + + return self; +} + +-(void)main +{ + [self downloadImage]; + NSString *cachePath = cachePathForKey(self.url.absoluteString); + [self writeData:self.dataRepresentation toPath:cachePath]; + + if (!self.isCancelled && self.imageOperation) + { + dispatch_async(dispatch_get_main_queue(), ^{ + if(self.successBlock) self.successBlock(self.imageOperation); + }); + } +} + + +- (void)downloadImage +{ + NSURLRequest* request = [NSURLRequest requestWithURL:self.url]; + NSURLResponse* response = nil; + NSError* error = nil; + self.dataRepresentation = [NSURLConnection sendSynchronousRequest:request returningResponse:&response error:&error]; + + if (error) + { + dispatch_async(dispatch_get_main_queue(), ^{ + + if(self.failedBlock) self.failedBlock(request, response, error); + }); + return; + } + + self.imageOperation = [[UIImage alloc] initWithData:self.dataRepresentation]; + + if (!self.imageOperation) + { + NSMutableDictionary *errorDetail = [NSMutableDictionary dictionary]; + [errorDetail setValue:[NSString stringWithFormat:@"Failed to init image with data from for URL: %@", self.url] forKey:NSLocalizedDescriptionKey]; + NSError* error = [NSError errorWithDomain:@"JMImageCacheErrorDomain" code:1 userInfo:errorDetail]; + dispatch_async(dispatch_get_main_queue(), ^{ + + if(self.failedBlock) self.failedBlock(request, response, error); + }); + } +} + + +- (void)writeData:(NSData*)data toPath:(NSString *)path { + [data writeToFile:path atomically:YES]; + NSString *cacheName = @"memory-cache"; + + NSDictionary *properties = [[NSFileManager defaultManager] attributesOfItemAtPath:path error:nil]; + + NSNumber *actualCacheSize = [[NSUserDefaults standardUserDefaults] objectForKey:cacheName]; + NSNumber *fileSize = [NSNumber numberWithUnsignedLong:([properties fileSize] + actualCacheSize.longValue)]; + + [[NSUserDefaults standardUserDefaults] setObject:fileSize forKey:cacheName]; + +} + +-(BOOL)isConcurrent +{ + return YES; +} + +@end diff --git a/UIImageView+JMImageCache.h b/UIImageView+JMImageCache.h index e294bc7..e5f27e7 100644 --- a/UIImageView+JMImageCache.h +++ b/UIImageView+JMImageCache.h @@ -12,11 +12,10 @@ - (void) setImageWithURL:(NSURL *)url; - (void) setImageWithURL:(NSURL *)url placeholder:(UIImage *)placeholderImage; -- (void) setImageWithURL:(NSURL *)url placeholder:(UIImage *)placeholderImage completionBlock:(void (^)(UIImage *))completionBlock; +- (void) setImageWithURL:(NSURL *)url placeholder:(UIImage *)placeholderImage completionBlock:(void (^)(UIImage *image))completionBlock; - (void) setImageWithURL:(NSURL *)url placeholder:(UIImage *)placeholderImage completionBlock:(void (^)(UIImage *))completionBlock failureBlock:(void (^)(NSURLRequest *request, NSURLResponse *response, NSError* error))failure; - (void) setImageWithURL:(NSURL *)url key:(NSString*)key placeholder:(UIImage *)placeholderImage; - (void) setImageWithURL:(NSURL *)url key:(NSString*)key placeholder:(UIImage *)placeholderImage completionBlock:(void (^)(UIImage *image))completionBlock; - (void) setImageWithURL:(NSURL *)url key:(NSString*)key placeholder:(UIImage *)placeholderImage completionBlock:(void (^)(UIImage *image))completionBlock failureBlock:(void (^)(NSURLRequest *request, NSURLResponse *response, NSError* error))failure; - @end \ No newline at end of file diff --git a/UIImageView+JMImageCache.m b/UIImageView+JMImageCache.m index 3a3502a..20568ac 100644 --- a/UIImageView+JMImageCache.m +++ b/UIImageView+JMImageCache.m @@ -8,19 +8,24 @@ #import "UIImageView+JMImageCache.h" #import "JMImageCache.h" +#import "JMImageCacheOperation.h" #import static char kJMImageURLObjectKey; +static char kJMImageRequestObjectKey; + @interface UIImageView (_JMImageCache) @property (readwrite, nonatomic, retain, setter = jm_setImageURL:) NSURL *jm_imageURL; +@property (readwrite, nonatomic, retain, setter= jm_setOperationRequest:) JMImageCacheOperation *jm_operationRequest; @end @implementation UIImageView (_JMImageCache) @dynamic jm_imageURL; +@dynamic jm_operationRequest; @end @@ -35,6 +40,28 @@ - (void) jm_setImageURL:(NSURL *)imageURL { objc_setAssociatedObject(self, &kJMImageURLObjectKey, imageURL, OBJC_ASSOCIATION_RETAIN_NONATOMIC); } +- (JMImageCacheOperation *)jm_operationRequest +{ + return (JMImageCacheOperation *)objc_getAssociatedObject(self, &kJMImageRequestObjectKey); +} + +- (void)jm_setOperationRequest:(JMImageCacheOperation *)jm_operationRequest +{ + objc_setAssociatedObject(self, &kJMImageRequestObjectKey, jm_operationRequest, OBJC_ASSOCIATION_RETAIN_NONATOMIC); +} + ++ (NSOperationQueue *)jm_sharedImageRequestOperationQueue +{ + static NSOperationQueue *_jm_imageRequestOperationQueue = nil; + if(!_jm_imageRequestOperationQueue) + { + _jm_imageRequestOperationQueue = [[NSOperationQueue alloc] init]; + [_jm_imageRequestOperationQueue setMaxConcurrentOperationCount:8]; + } + + return _jm_imageRequestOperationQueue; +} + #pragma mark - Public Methods - (void) setImageWithURL:(NSURL *)url { @@ -43,7 +70,14 @@ - (void) setImageWithURL:(NSURL *)url { - (void) setImageWithURL:(NSURL *)url placeholder:(UIImage *)placeholderImage { [self setImageWithURL:url key:nil placeholder:placeholderImage]; } -- (void) setImageWithURL:(NSURL *)url placeholder:(UIImage *)placeholderImage completionBlock:(void (^)(UIImage *))completionBlock { + +- (void)setImageWithURL:(NSURL *)url + placeholderImage:(UIImage *)placeholderImage +{ + [self setImageWithURL:url key:nil placeholder:placeholderImage]; +} + +- (void) setImageWithURL:(NSURL *)url placeholder:(UIImage *)placeholderImage completionBlock:(void (^)(UIImage *image))completionBlock { [self setImageWithURL:url key:nil placeholder:placeholderImage completionBlock:completionBlock failureBlock:nil]; } - (void) setImageWithURL:(NSURL *)url placeholder:(UIImage *)placeholderImage completionBlock:(void (^)(UIImage *))completionBlock failureBlock:(void (^)(NSURLRequest *request, NSURLResponse *response, NSError* error))failureBlock { @@ -53,65 +87,92 @@ - (void) setImageWithURL:(NSURL *)url key:(NSString*)key placeholder:(UIImage *) [self setImageWithURL:url key:key placeholder:placeholderImage completionBlock:nil]; } - (void) setImageWithURL:(NSURL *)url key:(NSString*)key placeholder:(UIImage *)placeholderImage completionBlock:(void (^)(UIImage *image))completionBlock { - [self setImageWithURL:url key:key placeholder:placeholderImage completionBlock:completionBlock]; + [self setImageWithURL:url key:key placeholder:placeholderImage completionBlock:completionBlock failureBlock:nil]; } - (void) setImageWithURL:(NSURL *)url key:(NSString*)key placeholder:(UIImage *)placeholderImage completionBlock:(void (^)(UIImage *image))completionBlock failureBlock:(void (^)(NSURLRequest *request, NSURLResponse *response, NSError* error))failureBlock{ + + [[self jm_operationRequest] cancel]; + self.jm_operationRequest = nil; + + UIImage *memoryImage; + if (key != nil) + { + memoryImage = [[JMImageCache sharedCache] imageFromMemoryForKey:key]; + } + else + { + memoryImage = [[JMImageCache sharedCache] imageFromMemoryForURL:url]; + } + + if (memoryImage != nil) { + self.image = memoryImage; + if (completionBlock) completionBlock(memoryImage); + return; + } + self.jm_imageURL = url; self.image = placeholderImage; - - [self setNeedsDisplay]; - [self setNeedsLayout]; - + __weak UIImageView *safeSelf = self; - + dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{ UIImage *i; - + if (key) { i = [[JMImageCache sharedCache] cachedImageForKey:key]; } else { i = [[JMImageCache sharedCache] cachedImageForURL:url]; } - + if(i) { dispatch_async(dispatch_get_main_queue(), ^{ safeSelf.jm_imageURL = nil; - + safeSelf.image = i; - - [safeSelf setNeedsLayout]; - [safeSelf setNeedsDisplay]; + + if (completionBlock) completionBlock(i); }); } else { dispatch_async(dispatch_get_main_queue(), ^{ safeSelf.image = placeholderImage; - - [safeSelf setNeedsDisplay]; - [safeSelf setNeedsLayout]; }); - - [[JMImageCache sharedCache] imageForURL:url key:key completionBlock:^(UIImage *image) { - if ([url isEqual:safeSelf.jm_imageURL]) { - dispatch_async(dispatch_get_main_queue(), ^{ - if(image) { - safeSelf.image = image; - } else { - safeSelf.image = placeholderImage; - } - - safeSelf.jm_imageURL = nil; - - [safeSelf setNeedsLayout]; - [safeSelf setNeedsDisplay]; - - if (completionBlock) completionBlock(image); - }); - } + + UIImage *image = [[JMImageCache sharedCache] imageFromDiskForURL:url]; + if(image) + { + [[JMImageCache sharedCache] setObject:image forKey:url.absoluteString]; + + dispatch_async(dispatch_get_main_queue(), ^{ + safeSelf.image = image; + if (completionBlock) completionBlock(image); + }); } - failureBlock:^(NSURLRequest *request, NSURLResponse *response, NSError* error) + else { - if (failureBlock) failureBlock(request, response, error); - }]; + self.jm_operationRequest = [[JMImageCacheOperation alloc] initWithURL:self.jm_imageURL aSuccessBlock:^(UIImage *image) { + if ([url isEqual:safeSelf.jm_imageURL]) { + + if(image != nil) [[JMImageCache sharedCache] setObject:image forKey:url.absoluteString]; + + dispatch_async(dispatch_get_main_queue(), ^{ + if(image != nil) { + safeSelf.image = image; + } else { + safeSelf.image = placeholderImage; + } + + safeSelf.jm_imageURL = nil; + safeSelf.jm_operationRequest = nil; + + if (completionBlock) completionBlock(image); + }); + } + } andFailedBlock:^(NSURLRequest *request, NSURLResponse *response, NSError *error) { + if (failureBlock) failureBlock(request, response, error); + }]; + + [[self class].jm_sharedImageRequestOperationQueue addOperation:self.jm_operationRequest]; + } } }); }