Remove the dirty hack of storing the helper into the first subview of the UIImageView and prevent duplicate downloads of the same URL

The helper (now called manager) is now handling the mapping between the UIImageView and its downloader.
This way we don't polute the UIImageView, and don't remove its capability to have subviews.
This change removes the automatic handling of image placeholder. The placeholder image can
be passed as second argument of setImageWithURL:placeholderImage:
The manager now handle duplicate downloads for the same URL gracefuly by sharing the same downloader
for all requestors.
Finaly, the manager handles URLs which can't create an image (HTTP error or invalid format) by flagging
them so it won't retry to download them again and again.
This commit is contained in:
Olivier Poitrey 2009-09-23 23:22:48 +02:00
parent 972c304957
commit 2fa0626aaa
11 changed files with 288 additions and 142 deletions

View File

@ -5,9 +5,12 @@ This library provides a category for UIImageVIew with support for remote images
It provides:
- Drop-in replacement to UIImageView
- Asynchronous image downloader
- Asynchronous memory + disk image caching with automatic cache expiration handling
- An UIImageView category adding web image and cache management to the Cocoa Touch framework
- An asynchronous image downloader using threads (NSOperation)
- An asynchronous memory + disk image caching with automatic cache expiration handling
- A garantie that the same URL won't be downloaded several times
- A garantie that bogus URLs won't be retried again and again
- Performances!
Motivation
----------
@ -59,11 +62,9 @@ How To Use It
### Using UIImageView+WebCache category with UITableView
Just #import the UIImageView+WebCache.h header, and call the setImageWithURL: method from the
tableView:cellForRowAtIndexPath: UITableViewDataSource method. Everything will be handled for you,
from parallel downloads to caching management. If you assigned an image to the view (via the
`images` property), this image will be used as a placeholder, waiting for the web image to be
loaded.
Just #import the UIImageView+WebCache.h header, and call the setImageWithURL:placeholderImage:
method from the tableView:cellForRowAtIndexPath: UITableViewDataSource method. Everything will be
handled for you, from parallel downloads to caching management.
#import "UIImageView+WebCache.h"
@ -79,31 +80,58 @@ loaded.
{
cell = [[[UITableViewCell alloc] initWithStyle:UITableViewCellStyleDefault
reuseIdentifier:MyIdentifier] autorelease];
// Here we set the placeholder image
cell.imageView.image = [UIImage imageNamed:@"placeholder.png"];
}
// Here we use the new provided setImageWithURL: method to load the web image
[cell.imageView setImageWithURL:[NSURL URLWithString:@"http://www.domain.com/path/to/image.jpg"]];
[cell.imageView setImageWithURL:[NSURL URLWithString:@"http://www.domain.com/path/to/image.jpg"]
placeholderImage:[UIImage imageNamed:@"placeholder.png"]];
cell.textLabel.text = @"My Text";
return cell;
}
### Asynchronous Image Downloader
### Using SDWebImageManager
The SDWebImageManager is the class behind the UIImageView+WebCache category. It ties the
asynchronous downloader with the image cache store. You can use this classe directly to benefits
from web image downloading with caching in another context than a UIView (ie: with Cocos).
Here is a simple example of how to use SDWebImageManager:
SDWebImageManager *manager = [SDWebImageManager sharedManager];
UIImage *cachedImage = [manager imageWithURL:url];
if (cachedImage)
{
// Use the cached image immediatly
}
else
{
// Start an async download
[manager downloadWithURL:url delegate:self];
}
Your class will have to implement the SDWebImageManagerDelegate protocol, and to implement the
imageHelper:didFinishWithImage: method from this protocol:
- (void)imageHelper:(SDWebImageManager *)imageHelper didFinishWithImage:(UIImage *)image
{
// Do something with the downloaded image
}
### Using Asynchronous Image Downloader Independently
It is possible to use the NSOperation based image downloader independently. Just create an instance
of SDWebImageDownloader using its convenience constructor downloaderWithURL:target:action:.
downloader = [SDWebImageDownloader downloaderWithURL:url
target:self
action:@selector(downloadFinishedWithImage:)];
downloader = [SDWebImageDownloader downloaderWithURL:url delegate:self];
The download will by queued immediately and the downloadFinishedWithImage: method will be called as
soon as the download of image will be completed (prepare not to be called from the main thread).
The download will by queued immediately and the imageDownloader:didFinishWithImage: method from the
SDWebImageDownloaderDelegate protocol will be called as soon as the download of image is completed
(prepare not to be called from the main thread).
### Asynchronous Image Caching
### Using Asynchronous Image Caching Independently
It is also possible to use the NSOperation based image cache store independently. SDImageCache
maintains a memory cache and an optional disk cache. Disk cache write operations are performed
@ -134,10 +162,8 @@ third argument.
Future Enhancements
-------------------
- Easy way to use it with default UITableView styles without requiring to create a custom UITableViewCell
- LRU memory cache cleanup instead of reset on memory warning
[Dailymotion]: http://www.dailymotion.com
[Fraggle]: http://fraggle.squarespace.com
[Urban Rivals]: http://fraggle.squarespace.com/blog/2009/9/15/almost-done-here-is-urban-rivals-iphone-trailer.html

View File

@ -7,19 +7,18 @@
*/
#import <Foundation/Foundation.h>
#import "SDWebImageDownloaderDelegate.h"
@interface SDWebImageDownloader : NSOperation
{
NSURL *url;
id target;
SEL action;
id<SDWebImageDownloaderDelegate> delegate;
}
@property (retain) NSURL *url;
@property (assign) id target;
@property (assign) SEL action;
@property (assign) id<SDWebImageDownloaderDelegate> delegate;
+ (id)downloaderWithURL:(NSURL *)url target:(id)target action:(SEL)action;
+ (id)downloaderWithURL:(NSURL *)url delegate:(id<SDWebImageDownloaderDelegate>)delegate;
+ (void)setMaxConcurrentDownloads:(NSUInteger)max;
@end

View File

@ -12,7 +12,7 @@ static NSOperationQueue *downloadQueue;
@implementation SDWebImageDownloader
@synthesize url, target, action;
@synthesize url, delegate;
- (void)dealloc
{
@ -20,12 +20,11 @@ static NSOperationQueue *downloadQueue;
[super dealloc];
}
+ (id)downloaderWithURL:(NSURL *)url target:(id)target action:(SEL)action
+ (id)downloaderWithURL:(NSURL *)url delegate:(id<SDWebImageDownloaderDelegate>)delegate
{
SDWebImageDownloader *downloader = [[[SDWebImageDownloader alloc] init] autorelease];
downloader.url = url;
downloader.target = target;
downloader.action = action;
downloader.delegate = delegate;
if (downloadQueue == nil)
{
@ -54,9 +53,9 @@ static NSOperationQueue *downloadQueue;
UIImage *image = [UIImage imageWithData:[NSData dataWithContentsOfURL:url]];
if (!self.isCancelled)
if (!self.isCancelled && [delegate respondsToSelector:@selector(imageDownloader:didFinishWithImage:)])
{
[target performSelector:action withObject:image];
[delegate performSelector:@selector(imageDownloader:didFinishWithImage:) withObject:self withObject:image];
}
[pool release];

View File

@ -0,0 +1,17 @@
/*
* This file is part of the SDWebImage package.
* (c) Olivier Poitrey <rs@dailymotion.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
@class SDWebImageDownloader;
@protocol SDWebImageDownloaderDelegate <NSObject>
@optional
- (void)imageDownloader:(SDWebImageDownloader *)downloader didFinishWithImage:(UIImage *)image;
@end

View File

@ -0,0 +1,17 @@
/*
* This file is part of the SDWebImage package.
* (c) Olivier Poitrey <rs@dailymotion.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
@class SDWebImageManager;
@protocol SDWebImageManagerDelegate <NSObject>
@optional
- (void)imageHelper:(SDWebImageManager *)imageHelper didFinishWithImage:(UIImage *)image;
@end

26
SDWebImageManager.h Normal file
View File

@ -0,0 +1,26 @@
/*
* This file is part of the SDWebImage package.
* (c) Olivier Poitrey <rs@dailymotion.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
#import <UIKit/UIKit.h>
#import "SDWebImageDownloaderDelegate.h"
#import "SDWebImageHelperDelegate.h"
@interface SDWebImageManager : NSObject <SDWebImageDownloaderDelegate>
{
NSMutableArray *delegates;
NSMutableArray *downloaders;
NSMutableDictionary *downloaderForURL;
NSMutableArray *failedURLs;
}
+ (id)sharedManager;
- (UIImage *)imageWithURL:(NSURL *)url;
- (void)downloadWithURL:(NSURL *)url delegate:(id<SDWebImageManagerDelegate>)delegate;
- (void)cancelForDelegate:(id<SDWebImageManagerDelegate>)delegate;
@end

148
SDWebImageManager.m Normal file
View File

@ -0,0 +1,148 @@
/*
* This file is part of the SDWebImage package.
* (c) Olivier Poitrey <rs@dailymotion.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
#import "SDWebImageManager.h"
#import "SDImageCache.h"
#import "SDWebImageDownloader.h"
static SDWebImageManager *instance;
@implementation SDWebImageManager
- (id)init
{
if (self = [super init])
{
delegates = [[NSMutableArray alloc] init];
downloaders = [[NSMutableArray alloc] init];
downloaderForURL = [[NSMutableDictionary alloc] init];
failedURLs = [[NSMutableArray alloc] init];
}
return self;
}
- (void) dealloc
{
[delegates release];
[downloaders release];
[downloaderForURL release];
[failedURLs release];
[super dealloc];
}
+ (id)sharedManager
{
if (instance == nil)
{
instance = [[SDWebImageManager alloc] init];
}
return instance;
}
- (UIImage *)imageWithURL:(NSURL *)url
{
return [[SDImageCache sharedImageCache] imageFromKey:[url absoluteString]];
}
- (void)downloadWithURL:(NSURL *)url delegate:(id<SDWebImageManagerDelegate>)delegate
{
if ([failedURLs containsObject:url])
{
return;
}
// Share the same downloader for identical URLs so we don't download the same URL several times
SDWebImageDownloader *downloader = [downloaderForURL objectForKey:url];
if (!downloader)
{
downloader = [SDWebImageDownloader downloaderWithURL:url delegate:self];
[downloaderForURL setObject:downloader forKey:url];
}
@synchronized(self)
{
[delegates addObject:delegate];
[downloaders addObject:downloader];
}
}
- (void)cancelForDelegate:(id<SDWebImageManagerDelegate>)delegate
{
@synchronized(self)
{
NSUInteger index = [delegates indexOfObjectIdenticalTo:delegate];
if (index == NSNotFound)
{
return;
}
SDWebImageDownloader *downloader = [[downloaders objectAtIndex:index] retain];
[delegates removeObjectAtIndex:index];
[downloaders removeObjectAtIndex:index];
if (![downloaders containsObject:downloader])
{
NSLog(@"cancel download");
// No more delegate are waiting for this download, cancel it
[downloader cancel];
[downloaderForURL removeObjectForKey:downloader.url];
}
[downloader release];
}
}
- (void)imageDownloader:(SDWebImageDownloader *)downloader didFinishWithImage:(UIImage *)image
{
[downloader retain];
@synchronized(self)
{
// Notify all the delegates with this downloader
for (NSInteger index = [downloaders count] - 1; index >= 0; index--)
{
SDWebImageDownloader *aDownloader = [downloaders objectAtIndex:index];
if (aDownloader == downloader)
{
id<SDWebImageManagerDelegate> delegate = [delegates objectAtIndex:index];
if (image && [delegate respondsToSelector:@selector(imageHelper:didFinishWithImage:)])
{
[delegate performSelector:@selector(imageHelper:didFinishWithImage:) withObject:self withObject:image];
}
[downloaders removeObjectAtIndex:index];
[delegates removeObjectAtIndex:index];
}
}
}
if (image)
{
// Store the image in the cache
[[SDImageCache sharedImageCache] storeImage:image forKey:[downloader.url absoluteString]];
}
else
{
// The image can't be downloaded from this URL, mark the URL as failed so we won't try and fail again and again
[failedURLs addObject:downloader.url];
}
// Release the downloader
[downloaderForURL removeObjectForKey:downloader.url];
[downloader release];
}
@end

View File

@ -7,9 +7,11 @@
*/
#import <UIKit/UIKit.h>
#import "SDWebImageHelperDelegate.h"
@interface UIImageView (WebCache)
@interface UIImageView (WebCache) <SDWebImageManagerDelegate>
- (void)setImageWithURL:(NSURL *)url;
- (void)setImageWithURL:(NSURL *)url placeholderImage:(UIImage *)placeholder;
@end

View File

@ -7,48 +7,42 @@
*/
#import "UIImageView+WebCache.h"
#import "UIImageViewHelper.h"
#import "SDWebImageManager.h"
@implementation UIImageView (WebCache)
- (void)setImageWithURL:(NSURL *)url
{
UIImageViewHelper *helper = nil;
[self setImageWithURL:url placeholderImage:nil];
}
if ([self.subviews count] > 0)
{
helper = [self.subviews objectAtIndex:0];
}
if (helper == nil)
{
helper = [[[UIImageViewHelper alloc] initWithDelegate:self] autorelease];
[self addSubview:helper];
}
- (void)setImageWithURL:(NSURL *)url placeholderImage:(UIImage *)placeholder
{
SDWebImageManager *manager = [SDWebImageManager sharedManager];
// Remove in progress downloader from queue
[helper cancel];
// Save the placeholder image in order to re-apply it when view is reused
if (helper.placeHolderImage == nil)
{
helper.placeHolderImage = self.image;
}
else
{
self.image = helper.placeHolderImage;
}
UIImage *cachedImage = [helper imageWithURL:url];
[manager cancelForDelegate:self];
UIImage *cachedImage = [manager imageWithURL:url];
if (cachedImage)
{
self.image = cachedImage;
}
else
{
[helper downloadWithURL:url];
if (placeholder)
{
self.image = placeholder;
}
[manager downloadWithURL:url delegate:self];
}
}
- (void)imageHelper:(SDWebImageManager *)imageHelper didFinishWithImage:(UIImage *)image
{
self.image = image;
}
@end

View File

@ -1,26 +0,0 @@
/*
* This file is part of the SDWebImage package.
* (c) Olivier Poitrey <rs@dailymotion.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
#import <UIKit/UIKit.h>
#import "SDWebImageDownloader.h"
@interface UIImageViewHelper : UIView
{
UIImageView *delegate;
SDWebImageDownloader *downloader;
UIImage *placeHolderImage;
}
@property (nonatomic, retain) UIImage *placeHolderImage;
- (id)initWithDelegate:(UIImageView *)aDelegate;
- (UIImage *)imageWithURL:(NSURL *)url;
- (void)downloadWithURL:(NSURL *)url;
- (void)cancel;
@end

View File

@ -1,56 +0,0 @@
/*
* This file is part of the SDWebImage package.
* (c) Olivier Poitrey <rs@dailymotion.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
#import "UIImageViewHelper.h"
#import "SDImageCache.h"
@implementation UIImageViewHelper
@synthesize placeHolderImage;
- (id)initWithDelegate:(UIImageView *)aDelegate
{
if (self = [super init])
{
delegate = aDelegate;
self.hidden = YES;
}
return self;
}
- (UIImage *)imageWithURL:(NSURL *)url
{
return [[SDImageCache sharedImageCache] imageFromKey:[url absoluteString]];
}
- (void)downloadWithURL:(NSURL *)url
{
downloader = [[SDWebImageDownloader downloaderWithURL:url target:self action:@selector(downloadFinishedWithImage:)] retain];
}
- (void)cancel
{
[downloader cancel];
[downloader release];
downloader = nil;
}
- (void)downloadFinishedWithImage:(UIImage *)anImage
{
// Apply image to the underlaying UIImageView
delegate.image = anImage;
// Store the image in the cache
[[SDImageCache sharedImageCache] storeImage:anImage forKey:[downloader.url absoluteString]];
// Free the downloader
[downloader release];
downloader = nil;
}
@end