Skip to content

Commit

Permalink
Replace the NSOperation based downloader by a simple async NSURLConne…
Browse files Browse the repository at this point in the history
…ction (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.
  • Loading branch information
Olivier Poitrey committed Jun 9, 2010
1 parent 467be16 commit e0e3696
Show file tree
Hide file tree
Showing 5 changed files with 134 additions and 94 deletions.
47 changes: 22 additions & 25 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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).

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!
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:

> 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
-------------
Expand All @@ -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"

Expand Down Expand Up @@ -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

Expand Down Expand Up @@ -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
15 changes: 11 additions & 4 deletions SDWebImageDownloader.h
Original file line number Diff line number Diff line change
Expand Up @@ -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
98 changes: 71 additions & 27 deletions SDWebImageDownloader.m
Original file line number Diff line number Diff line change
Expand Up @@ -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)
// NOOP
}

- (void)start
{
// In order to prevent from potential duplicate caching (NSURLCache + SDImageCache) we disable the cache for image requests
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 (connection)
{
self.imageData = [NSMutableData data];
}
else
{
downloadQueue = [[NSOperationQueue alloc] init];
if ([delegate respondsToSelector:@selector(imageDownloader:didFailWithError:)])
{
[delegate performSelector:@selector(imageDownloader:didFailWithError:) withObject:self withObject:nil];
}
}
}

downloadQueue.maxConcurrentOperationCount = max;
- (void)cancel
{
if (connection)
{
[connection cancel];
self.connection = nil;
}
}

- (void)main
#pragma mark NSURLConnection (delegate)

- (void)connection:(NSURLConnection *)aConnection didReceiveData:(NSData *)data
{
NSAutoreleasePool* pool = [[NSAutoreleasePool alloc] init];
[imageData appendData:data];
}

// 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]];
- (void)connectionDidFinishLoading:(NSURLConnection *)aConnection
{
UIImage *image = [[UIImage alloc] initWithData:imageData];
self.imageData = nil;
self.connection = nil;

if (!self.isCancelled && [delegate respondsToSelector:@selector(imageDownloader:didFinishWithImage:)])
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
3 changes: 2 additions & 1 deletion SDWebImageDownloaderDelegate.h
Original file line number Diff line number Diff line change
Expand Up @@ -13,5 +13,6 @@
@optional

- (void)imageDownloader:(SDWebImageDownloader *)downloader didFinishWithImage:(UIImage *)image;
- (void)imageDownloader:(SDWebImageDownloader *)downloader didFailWithError:(NSError *)error;

@end
@end
65 changes: 28 additions & 37 deletions SDWebImageManager.m
Original file line number Diff line number Diff line change
Expand Up @@ -67,62 +67,53 @@ - (void)downloadWithURL:(NSURL *)url delegate:(id<SDWebImageManagerDelegate>)del
[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];
NSUInteger idx = [delegates indexOfObjectIdenticalTo:delegate];

if (idx == NSNotFound)
{
return;
}

SDWebImageDownloader *downloader = [[downloaders objectAtIndex:idx] retain];
if (idx == NSNotFound)
{
return;
}

[delegates removeObjectAtIndex:idx];
[downloaders removeObjectAtIndex:idx];
SDWebImageDownloader *downloader = [[downloaders objectAtIndex:idx] retain];

if (![downloaders containsObject:downloader])
{
// No more delegate are waiting for this download, cancel it
[downloader cancel];
[downloaderForURL removeObjectForKey:downloader.url];
}
[delegates removeObjectAtIndex:idx];
[downloaders removeObjectAtIndex:idx];

[downloader release];
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];

This comment has been minimized.

Copy link
@KesionX

KesionX May 31, 2018

hi, Could you talk me why don't use synchronized here ?

@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:)])
{
[delegate performSelector:@selector(webImageManager:didFinishWithImage:) withObject:self withObject:image];
}
id<SDWebImageManagerDelegate> delegate = [delegates objectAtIndex:idx];

[downloaders removeObjectAtIndex:idx];
[delegates removeObjectAtIndex:idx];
if (image && [delegate respondsToSelector:@selector(webImageManager:didFinishWithImage:)])
{
[delegate performSelector:@selector(webImageManager:didFinishWithImage:) withObject:self withObject:image];
}

[downloaders removeObjectAtIndex:idx];
[delegates removeObjectAtIndex:idx];
}
}

Expand Down

4 comments on commit e0e3696

@jogu
Copy link

@jogu jogu commented on e0e3696 Jun 28, 2010

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 connections are very lightweight.

I think 'setMaxConcurrentDownloads' is quite important - my experience suggests if you are on a 3G or slower connection, starting more than 2 or 3 downloads results in poorer performance as the connection gets over saturated. Users are generally a bit happier seeing images appearing gradually rather than all the images suddenly showing up after a long time with nothing.

(This is a great class btw, really helped me get my app going quickly!)

@rs
Copy link
Member

@rs rs commented on e0e3696 Jul 15, 2010

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The fact is connections are cancelled when download is not completed while being reused in UITableView. You shouldn't have more connections than cells visible on screen though.

@qf-zz
Copy link

@qf-zz qf-zz commented on e0e3696 Aug 10, 2010

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I agree with jogu.
Get an iphone 3G (or iphone 2)..and you can make sure that just more than 2 or 3 downloads results in bad performances.
The UI almost freezes til the downloads are finished..even with a WiFi connection.
You shouldn't have more connections than cells visible on screen (because of reusing) : that's a good point. But i think you often get more than 3 cells visible on a screen..
Should we still consider iphone 3G (and 2) ?

I also wonder what happen if you switch between UITableViews of a UITabBarController, i think downloads continue while you're not showing their respective UITableView. Should we cancel these downloads ?

Anyway, thank you for this class.

@rs
Copy link
Member

@rs rs commented on e0e3696 Aug 11, 2010

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm testing the lib with a 3G and I don't experience this lagging issue. About your question on the switch between tabs, I guess when the view will disappear, chances are that the cell will be unloaded and thus corresponding downloads should be cancelled. This really depends on the app actually.

Anyway, I would be happy to merge a patch implementing a simple and effectif concurrent connections limiter though ;)

Please sign in to comment.