diff --git a/README.md b/README.md index 1015481..2f0d5a7 100644 --- a/README.md +++ b/README.md @@ -6,7 +6,7 @@ This library provides a category for UIImageVIew with support for remote images It provides: - An UIImageView category adding web image and cache management to the Cocoa Touch framework -- An asynchronous image downloader using threads (NSOperation) +- An asynchronous image downloader - 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 @@ -39,23 +39,21 @@ time faster than my own servers... WTF?? In fact, my servers were well but a lot of latency was added to the requests, certainly because my application wasn't responsive enough to handle the requests at full speed. At this moment, I -understood something important, asynchronous NSURLConnections are tied to the main runloop (I -guess). It's certainly based on the poll multiplexer system call, which allows a single thread to -handle quite a huge number of simultaneous connections. It works well while nothing blocks in the -loop, but in this loop, there is also the events handling. A simple test to recognize an application -using NSURLConnection to load there remote images is to scroll the UITableView with your finger to -disclose an unloaded image, and to keep your finger pressed on the screen. If the image doesn't load -until you release you finger, the application is certainly using NSURLConnection (try with the -Facebook app for instance). I'm not completely clear about the reason of this blocking, I thought -the iPhone was running a dedicated run-loop for connections, but the fact is, NSURLConnection is -affected by the application events and/or UI handling (or something else I'm not aware of). +understood something important, asynchronous NSURLConnections are tied to the main runloop in the +NSEventTrackingRunLoopMode. As explained in the documentation, this runloop mode is affected by +UI events: -Thus I explored another path and found this marvelous NSOperation class to handle concurrency with -love. I ran some quick tests with this tool and I instantly got enhanced responsiveness of the image -loading in my UITableView by... a lot. Thus I rewrote the [Fraggle][]'s implementation using the -same concept of drop-in remplacement for UIImageView but with this new technic. I thought the result -could benefits to a lot of other applications, thus we decided, with [Fraggle][], to open-sourced -it, et voila! +> Cocoa uses this mode to restrict incoming events during mouse-dragging loops and other sorts of +> user interface tracking loops. + +A simple test to recognize an application using NSURLConnection in its default mode to load there +remote images is to scroll the UITableView with your finger to disclose an unloaded image, and to +keep your finger pressed on the screen. If the image doesn't load until you release you finger, +you've got one (try with the Facebook app for instance). It took me quite some time to understand +the reason for this lagging issue. Actually I first used NSOperation to workaround this issue. + +This technic combined with an image cache instantly gave a lot of responsiveness to my app. +I thought this lib could benefits to a lot of other Cocoa Touch application so I open-sourced it. How To Use It ------------- @@ -64,7 +62,7 @@ How To Use It 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. +handled for you, from async downloads to caching management. #import "UIImageView+WebCache.h" @@ -122,14 +120,13 @@ imageHelper:didFinishWithImage: method from this protocol: ### 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:. +It is possible to use the async image downloader independently. You just have to create an instance +of SDWebImageDownloader using its convenience constructor downloaderWithURL:delegate:. downloader = [SDWebImageDownloader downloaderWithURL:url delegate:self]; -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). +The download will start immediately and the imageDownloader:didFinishWithImage: method from the +SDWebImageDownloaderDelegate protocol will be called as soon as the download of image is completed. ### Using Asynchronous Image Caching Independently @@ -168,4 +165,4 @@ Future Enhancements [Fraggle]: http://fraggle.squarespace.com [Urban Rivals]: http://fraggle.squarespace.com/blog/2009/9/15/almost-done-here-is-urban-rivals-iphone-trailer.html [Three20]: http://groups.google.com/group/three20 -[Joe Hewitt]: http://www.joehewitt.com \ No newline at end of file +[Joe Hewitt]: http://www.joehewitt.com diff --git a/SDWebImageDownloader.h b/SDWebImageDownloader.h index 2608796..ddf258c 100644 --- a/SDWebImageDownloader.h +++ b/SDWebImageDownloader.h @@ -9,16 +9,23 @@ #import #import "SDWebImageDownloaderDelegate.h" -@interface SDWebImageDownloader : NSOperation +@interface SDWebImageDownloader : NSObject { + @private NSURL *url; id delegate; + NSURLConnection *connection; + NSMutableData *imageData; } -@property (retain) NSURL *url; -@property (assign) id delegate; +@property (nonatomic, retain) NSURL *url; +@property (nonatomic, assign) id delegate; + (id)downloaderWithURL:(NSURL *)url delegate:(id)delegate; -+ (void)setMaxConcurrentDownloads:(NSUInteger)max; +- (void)start; +- (void)cancel; + +// This method is now no-op and is deprecated ++ (void)setMaxConcurrentDownloads:(NSUInteger)max __attribute__((deprecated)); @end diff --git a/SDWebImageDownloader.m b/SDWebImageDownloader.m index 5afaf18..93c406f 100644 --- a/SDWebImageDownloader.m +++ b/SDWebImageDownloader.m @@ -8,59 +8,103 @@ #import "SDWebImageDownloader.h" -static NSOperationQueue *downloadQueue; +@interface SDWebImageDownloader () +@property (nonatomic, retain) NSURLConnection *connection; +@property (nonatomic, retain) NSMutableData *imageData; +@end @implementation SDWebImageDownloader +@synthesize url, delegate, connection, imageData; -@synthesize url, delegate; - -- (void)dealloc -{ - [url release], url = nil; - [super dealloc]; -} +#pragma mark Public Methods + (id)downloaderWithURL:(NSURL *)url delegate:(id)delegate { SDWebImageDownloader *downloader = [[[SDWebImageDownloader alloc] init] autorelease]; downloader.url = url; downloader.delegate = delegate; - - if (downloadQueue == nil) - { - downloadQueue = [[NSOperationQueue alloc] init]; - downloadQueue.maxConcurrentOperationCount = 8; - } - - [downloadQueue addOperation:downloader]; - + [downloader start]; return downloader; } + (void)setMaxConcurrentDownloads:(NSUInteger)max { - if (downloadQueue == nil) - { - downloadQueue = [[NSOperationQueue alloc] init]; - } - - downloadQueue.maxConcurrentOperationCount = max; + // NOOP } -- (void)main +- (void)start { - NSAutoreleasePool* pool = [[NSAutoreleasePool alloc] init]; - // In order to prevent from potential duplicate caching (NSURLCache + SDImageCache) we disable the cache for image requests - NSURLRequest *request = [NSURLRequest requestWithURL:url cachePolicy:NSURLRequestReloadIgnoringLocalCacheData timeoutInterval:5]; - UIImage *image = [UIImage imageWithData:[NSURLConnection sendSynchronousRequest:request returningResponse:nil error:NULL]]; + NSURLRequest *request = [[NSURLRequest alloc] initWithURL:url cachePolicy:NSURLRequestReloadIgnoringLocalCacheData timeoutInterval:15]; + self.connection = [[[NSURLConnection alloc] initWithRequest:request delegate:self startImmediately:NO] autorelease]; + // Ensure we aren't blocked by UI manipulations (default runloop mode for NSURLConnection is NSEventTrackingRunLoopMode) + [connection scheduleInRunLoop:[NSRunLoop currentRunLoop] forMode:NSRunLoopCommonModes]; + [connection start]; + [request release]; - if (!self.isCancelled && [delegate respondsToSelector:@selector(imageDownloader:didFinishWithImage:)]) + if (connection) + { + self.imageData = [NSMutableData data]; + } + else + { + if ([delegate respondsToSelector:@selector(imageDownloader:didFailWithError:)]) + { + [delegate performSelector:@selector(imageDownloader:didFailWithError:) withObject:self withObject:nil]; + } + } +} + +- (void)cancel +{ + if (connection) + { + [connection cancel]; + self.connection = nil; + } +} + +#pragma mark NSURLConnection (delegate) + +- (void)connection:(NSURLConnection *)aConnection didReceiveData:(NSData *)data +{ + [imageData appendData:data]; +} + +- (void)connectionDidFinishLoading:(NSURLConnection *)aConnection +{ + UIImage *image = [[UIImage alloc] initWithData:imageData]; + self.imageData = nil; + self.connection = nil; + + if ([delegate respondsToSelector:@selector(imageDownloader:didFinishWithImage:)]) { [delegate performSelector:@selector(imageDownloader:didFinishWithImage:) withObject:self withObject:image]; } - [pool release]; + [image release]; } +- (void)connection:(NSURLConnection *)connection didFailWithError:(NSError *)error +{ + if ([delegate respondsToSelector:@selector(imageDownloader:didFailWithError:)]) + { + [delegate performSelector:@selector(imageDownloader:didFailWithError:) withObject:self withObject:error]; + } + + self.connection = nil; + self.imageData = nil; +} + +#pragma mark NSObject + +- (void)dealloc +{ + [url release], url = nil; + [connection release], connection = nil; + [imageData release], imageData = nil; + [super dealloc]; +} + + @end diff --git a/SDWebImageDownloaderDelegate.h b/SDWebImageDownloaderDelegate.h index f5281ac..82d07d2 100644 --- a/SDWebImageDownloaderDelegate.h +++ b/SDWebImageDownloaderDelegate.h @@ -13,5 +13,6 @@ @optional - (void)imageDownloader:(SDWebImageDownloader *)downloader didFinishWithImage:(UIImage *)image; +- (void)imageDownloader:(SDWebImageDownloader *)downloader didFailWithError:(NSError *)error; -@end \ No newline at end of file +@end diff --git a/SDWebImageManager.m b/SDWebImageManager.m index 8f41fe5..8f97809 100644 --- a/SDWebImageManager.m +++ b/SDWebImageManager.m @@ -67,62 +67,53 @@ static SDWebImageManager *instance; [downloaderForURL setObject:downloader forKey:url]; } - @synchronized(self) - { - [delegates addObject:delegate]; - [downloaders addObject:downloader]; - } + [delegates addObject:delegate]; + [downloaders addObject:downloader]; } - (void)cancelForDelegate:(id)delegate { - @synchronized(self) + NSUInteger idx = [delegates indexOfObjectIdenticalTo:delegate]; + + if (idx == NSNotFound) { - NSUInteger idx = [delegates indexOfObjectIdenticalTo:delegate]; - - if (idx == NSNotFound) - { - return; - } - - SDWebImageDownloader *downloader = [[downloaders objectAtIndex:idx] retain]; - - [delegates removeObjectAtIndex:idx]; - [downloaders removeObjectAtIndex:idx]; - - if (![downloaders containsObject:downloader]) - { - // No more delegate are waiting for this download, cancel it - [downloader cancel]; - [downloaderForURL removeObjectForKey:downloader.url]; - } - - [downloader release]; + return; } + + SDWebImageDownloader *downloader = [[downloaders objectAtIndex:idx] retain]; + + [delegates removeObjectAtIndex:idx]; + [downloaders removeObjectAtIndex:idx]; + + if (![downloaders containsObject:downloader]) + { + // 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 idx = [downloaders count] - 1; idx >= 0; idx--) { - // Notify all the delegates with this downloader - for (NSInteger idx = [downloaders count] - 1; idx >= 0; idx--) + SDWebImageDownloader *aDownloader = [downloaders objectAtIndex:idx]; + if (aDownloader == downloader) { - SDWebImageDownloader *aDownloader = [downloaders objectAtIndex:idx]; - if (aDownloader == downloader) + id delegate = [delegates objectAtIndex:idx]; + + if (image && [delegate respondsToSelector:@selector(webImageManager:didFinishWithImage:)]) { - id delegate = [delegates objectAtIndex:idx]; - - if (image && [delegate respondsToSelector:@selector(webImageManager:didFinishWithImage:)]) - { - [delegate performSelector:@selector(webImageManager:didFinishWithImage:) withObject:self withObject:image]; - } - - [downloaders removeObjectAtIndex:idx]; - [delegates removeObjectAtIndex:idx]; + [delegate performSelector:@selector(webImageManager:didFinishWithImage:) withObject:self withObject:image]; } + + [downloaders removeObjectAtIndex:idx]; + [delegates removeObjectAtIndex:idx]; } }