Replace the NSOperation based downloader by a simple async NSURLConnection (read-on to understand why)
I finally found the reason behind the download not started while UITableView is manipulated: the default NSURLConnection runloop mode. Its default mode is NSEventTrackingRunLoopMode which is interrupted by UI events. Changing default NSURLConnection runloop mode to NSRunLoopCommonModes just fix this good old responsiveness issue. I thus decided to replace the current NSOperation based implementation by this finding, as NSOperation is far more expensive than simple async connections. Additionally, moving aways from NSOperation here fix an odd lagging issue with iOS 4, an issue I can't explain at the moment. Note that `SDWebImageDownloader`'s `setMaxConcurrentDownloads:` method is now a no-op as I didn't implemented the NSOperation queuing system with async connections. I don't think it still necessary as thread-less async connectaions are very lightweight. If you think there is a real need of this, I may reconsider and implement it in the future. In the meantime, this method does nothing and its usage is declared as deprecated.
This commit is contained in:
parent
467be16671
commit
e0e369659e
45
README.md
45
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
|
||||
[Joe Hewitt]: http://www.joehewitt.com
|
||||
|
|
|
@ -9,16 +9,23 @@
|
|||
#import <Foundation/Foundation.h>
|
||||
#import "SDWebImageDownloaderDelegate.h"
|
||||
|
||||
@interface SDWebImageDownloader : NSOperation
|
||||
@interface SDWebImageDownloader : NSObject
|
||||
{
|
||||
@private
|
||||
NSURL *url;
|
||||
id<SDWebImageDownloaderDelegate> delegate;
|
||||
NSURLConnection *connection;
|
||||
NSMutableData *imageData;
|
||||
}
|
||||
|
||||
@property (retain) NSURL *url;
|
||||
@property (assign) id<SDWebImageDownloaderDelegate> delegate;
|
||||
@property (nonatomic, retain) NSURL *url;
|
||||
@property (nonatomic, assign) id<SDWebImageDownloaderDelegate> delegate;
|
||||
|
||||
+ (id)downloaderWithURL:(NSURL *)url delegate:(id<SDWebImageDownloaderDelegate>)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
|
||||
|
|
|
@ -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<SDWebImageDownloaderDelegate>)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
|
||||
|
|
|
@ -13,5 +13,6 @@
|
|||
@optional
|
||||
|
||||
- (void)imageDownloader:(SDWebImageDownloader *)downloader didFinishWithImage:(UIImage *)image;
|
||||
- (void)imageDownloader:(SDWebImageDownloader *)downloader didFailWithError:(NSError *)error;
|
||||
|
||||
@end
|
||||
@end
|
||||
|
|
|
@ -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<SDWebImageManagerDelegate>)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<SDWebImageManagerDelegate> delegate = [delegates objectAtIndex:idx];
|
||||
|
||||
if (image && [delegate respondsToSelector:@selector(webImageManager:didFinishWithImage:)])
|
||||
{
|
||||
id<SDWebImageManagerDelegate> 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];
|
||||
}
|
||||
}
|
||||
|
||||
|
|
Loading…
Reference in New Issue