Monday, May 24, 2010

Downloading Images for a Table without Threads

It is common practice in many networking libraries in many languages to spawn threads to handle asynchronous communications in the background while the rest of the program continues to function. Although this approach does work on the iPhone, it's really not the right way to do it. I've recently run across two different blog postings that show how to download images for display in an iPhone table view, and they both did synchronous network communications using threads.

Here's my Public Service Announcement for today: Don't. Please… just, please don't, okay? Don't spawn threads for asynchronous communications unless you would have used threads in the same situation if there was no network code..
Note: A few people have suggested that this post is misleading because threads are used behind the scenes. Yes, it is true that the iPhone OS will spawn a small number of non-main threads as the result of using certain networking APIs. You still should be using the asynchronous APIs for your network communications and should not be spawning threads so you can make synchronous networking calls without blocking.
I don't like to knock other people's tutorials. In programming, there are multiple ways of doing nearly everything, and often more than one approach has merit in certain situations, so I'm always hesitant to say that the approach someone else uses is "wrong" if it works. But, this is one of those cases where there is a clear and compelling reason not to do it one particular way. However, because the "right" way is not necessarily intuitive, especially for people whose networking experience has all been thread-based, I thought it worth a short tutorial to show how to download asynchronously for display in a table without spawning any threads.

But first, why shouldn't we use threads this way? Well, simply put, threads have overhead. It's not a huge overhead, but it's non-zero and, in fact, it's non-trivial given the processing power and physical memory available on iPhone OS devices. The overhead of threading absoulutely makes sense in some situations; asynchronous networking is not one of those situations. Apple provides asynchronous APIs specifically for this purpose and they're designed to give the best performance in light of available system resources.

But, don't take my word for it. Here is what Apple says on the matter, in a guide written to help Windows programmers make the transition to programming for the Mac (emphasis mine):
One of the complexities of socket use involves the use of separate threads to handle the exchange of data. In a server environment, where hundreds of sockets are open simultaneously, the code that checks all these threads for activity may cause a significant performance hit. In addition, with threads, data synchronization is often an issue, and locks are often required to guarantee that only one thread has access to a given global variable at a time.

[...]

Event-driven code is more complex than code using blocking threads, but it delivers the highest network performance. When you use a run loop to handle multiple sockets in the same thread, you avoid the data synchronization problems associated with a multiple-thread solution.
When Apple says something as specific as this, they usually have a darn good reason for it. This is Apple's polite way of saying "don't use threads for networking, you idiot". They're a lot more polite than I am, obviously.

As you probably know, your application's main thread (the one that gets created automatically when your application is launched) has a loop (contained in UIApplication) that's always running. It's called your application's run loop and it keeps running as long as your application is executing. Every time through this loop, UIApplication does a whole bunch of stuff. It checks for inputs from the hardware and mach messages from the OS, for example, and it looks to see if there are any NSTimers running whose targets need to be called. Among the tasks it does is to check for incoming network data. There are more things the run loop handles, but that last one is the important for today's discussion.

Conceptually speaking, CFNetwork is not the easiest framework to grasp, but it is extremely efficient. It can handle hundreds, maybe even thousands of simultaneous downloads without causing the main thread to seize up. It's considerably more efficient than using synchronous network calls on a thread and not really any harder once you know how to do it.

Let's Try Downloading on the Main Thread



I've written a small iPhone app that downloads images from Deviant Art. You just enter somebody's Deviant Art username into a text field and the application will grab the the images in that person's gallery, download them asynchronously, and display each one in the table once it's finished downloading:

Screen shot 2010-05-24 at 4.28.42 AM.png

Notice that there's an activity indicator showing that some images are in the process of being downloaded. This is all happening on the main thread. I spawn no explicit threads in this application, and yet the UI stays responsive. You can also tap a row to view the image larger, but that's not really important to the process being illustrated.
Note:Although most DeviantArt galleries are SFW, there are many that aren't, so just be aware of that. Edit: Turns out that warning was unnecessary - since we haven't authenticated, DeviartArt filters out all the NSFW images automatically.

Creating the Download Object


In your own applications, you may approach things somewhat differently, but for this sample application, I'm going to put the code to download the image in its own data model class called DeviantDownload. I'm going to override the accessor method for the image so that the first time that accessor is called, it will kick off the download. If I call the image accessor method and get nil, the calling code can make itself the download's delegate to get notified when the download finishes.

In this application, downloads don't get triggered until the first time an image scrolls onto the screen. That may not be what you want in your application, but it will keep things nice and simple for this example. Here's the declaration of our download class:

#import <Foundation/Foundation.h>

#define DeviantDownloadErrorDomain @"Deviant Download Error Domain"
enum
{ DeviantDownloadErrorNoConnection = 1000,
}
;

@class DeviantDownload;
@protocol DeviantDownloadDelegate
- (void)downloadDidFinishDownloading:(DeviantDownload *)download;
- (void)download:(DeviantDownload *)download didFailWithError:(NSError *)error;
@end



@interface DeviantDownload : NSObject
{ NSString

*urlString; UIImage

*image; id <NSObject, DeviantDownloadDelegate> delegate;

@private NSMutableData

*receivedData; BOOL

downloading;
}

@property (nonatomic, retain) NSString *urlString;
@property (nonatomic, retain) UIImage *image;
@property (nonatomic, readonly) NSString *filename;
@property (nonatomic, assign) id <NSObject, DeviantDownloadDelegate> delegate;
@end


Pretty straightforward, right? I define an error domain and an error code for the one error situation our application generates (the rest come from NSURLConnection). After that, I declare a protocol that defines the methods our delegate has to implement. There are only two: one that gets called when the download finishes successfully, and another that gets called if it fails. In the sample application, our error checking is minimal, but you will do real error checking in your apps, right? RIGHT?! Yes, you will, because that's the way you roll. In a real app, you might also add a third delegate method that to inform the delegate what percentage of the download is complete, but I'll leave that one as an exercise for the student.

Next, I declare the DeviantDownload object, which only has four instances variables, one of which is private. urlString is the url of the image to download, image is the downloaded image, and delegate is the object that wants to get notified about the status of the download. Finally, receivedData is just a buffer to hold the downloaded data until I have it all and can create the image from it.

Okay, let's look at the implementation of our download object:

#import "DeviantDownload.h"

@interface DeviantDownload()
@property (nonatomic, retain) NSMutableData *receivedData;
@end


@implementation DeviantDownload
@synthesize urlString;
@synthesize image;
@synthesize delegate;
@synthesize receivedData;
#pragma mark -

- (UIImage *)image
{
if (image == nil && !downloading)
{
if (urlString != nil && [urlString length] > 0)
{
NSURLRequest *req = [[NSURLRequest alloc] initWithURL:[NSURL URLWithString:self.urlString]];
NSURLConnection *con = [[NSURLConnection alloc]
initWithRequest:req
delegate:self
startImmediately:NO]
;
[con scheduleInRunLoop:[NSRunLoop currentRunLoop]
forMode:NSRunLoopCommonModes
]
;
[con start];



if (con)
{
NSMutableData *data = [[NSMutableData alloc] init];
self.receivedData=data;
[data release];
}

else
{
NSError *error = [NSError errorWithDomain:DeviantDownloadErrorDomain
code:DeviantDownloadErrorNoConnection
userInfo:nil
]
;
if ([self.delegate respondsToSelector:@selector(download:didFailWithError:)])
[delegate download:self didFailWithError:error];
}

[req release];

downloading = YES;
}

}

return image;
}

- (NSString *)filename
{
return [urlString lastPathComponent];
}

- (void)dealloc
{
[urlString release];
[image release];
delegate = nil;
[receivedData release];
[super dealloc];
}

#pragma mark -
#pragma mark NSURLConnection Callbacks
- (void)connection:(NSURLConnection *)connection didReceiveResponse:(NSURLResponse *)response
{
[receivedData setLength:0];
}

- (void)connection:(NSURLConnection *)connection didReceiveData:(NSData *)data
{
[receivedData appendData:data];
}

- (void)connection:(NSURLConnection *)connection
didFailWithError:(NSError *)error
{
[connection release];
if ([delegate respondsToSelector:@selector(download:didFailWithError:)])
[delegate download:self didFailWithError:error];
}

- (void)connectionDidFinishLoading:(NSURLConnection *)connection
{
self.image = [UIImage imageWithData:receivedData];
if ([delegate respondsToSelector:@selector(downloadDidFinishDownloading:)])
[delegate downloadDidFinishDownloading:self];

[connection release];
self.receivedData = nil;
}

#pragma mark -
#pragma mark Comparison
- (NSComparisonResult)compare:(id)theOther
{
DeviantDownload *other = (DeviantDownload *)theOther;
return [self.filename compare:other.filename];
}

@end


To make memory management more readable, I first declare a property for receivedData in a class extension. This creates a property that is accessible in my class but that isn't advertised to other classes. After that, I synthesize the four properties (three public declared in the header and one private declared in extension)

The first method I've written is an accessor for image. Remember, @synthesize creates an accessor and mutator if one doesn't already exist in your code, so by providing an accessor but not a mutator, @synthesize will create a mutator for me, but will use this method as its accessor. I don't want to download more than once, so I use the instance variable downloading to keep track of whether I've already kicked off a download. If image is nil and I haven't previously kicked off a download, I kick one off asynchronously.

At the end of the method, I return image, even though it will still be nil until the download is complete. That's okay, that will be the calling code's indication that they should become the download's delegate.

The next method, filename just returns the last part of urlString, which is the filename. I don't want to display the whole URL in the table.

The dealloc method is pretty standard stuff, so we can skip it.

The next four methods are the asynchronous callbacks for NSURLConnection. The first, connection:didReceiveResponse: is called when a connection is made. I set the length of receivedData to zero here because if there's a redirect, this will get called multiple times and I want to flush out the payload from the earlier redirected connections. That extra data, when it exists, would prevent our image from being created.

connection:didReceiveData: gets called periodically during the download, whenever there's data sitting in the buffer for us. All I do here is tack the newly received data onto the end of what's already been received.

In connection:didFailWithError:, I release the connection and notify our delegate. Because this is a data model object, I don't show an alert or anything. Dealing with the error isn't this object's responsibility. Once it reports it, its job is done; it'll let the controller figure out what to do with the error. This keeps our object firmly in the "model" component of the MVC paradigm and maximizes the chances I'll be able to reuse this object at some point.

Finally, in connectionDidFinishLoading:, I create an image from the received data, assign it to the image property and notify our delegate.

The last method, compare: is implemented to let us determine if two DeviantDownload objects represent the same file. I'll use this to look at two downloads and see if they are downloading the same file, which we don't want to do. Deviant Art uses a server farm to serve images, so the same image can have multiple URLs, but for a particular user, the filename must be unique, so I compare that.

Using the Download Object


In the table view controller, I pull down a list of the URLs for the given user. I'm not going to go into detail of the logic that does this, but in short, I asynchronously pull down JSON data for the user's gallery in chunks of 24 images, creating new DeviantDownload objects for each one and adding them to the NSMutableArray that drives our table. In our tableView:cellForRowAtIndexPath:, here's what I do:

- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath 
{

DeviantDisplayCell *cell = (DeviantDisplayCell *)[tableView dequeueReusableCellWithIdentifier:[DeviantDisplayCell reuseIdentifier]];
if (cell == nil)
{
[[NSBundle mainBundle] loadNibNamed:@"DeviantDisplayCell" owner:self options:nil];
cell = loadCell;
self.loadCell = nil;
}

DeviantDownload *download = [downloads objectAtIndex:[indexPath row]];
cell.cellLabel.text = download.filename;
UIImage *cellImage = download.image;
if (cellImage == nil)
{
[cell.cellSpinner startAnimating];
download.delegate = self;
}

else
[cell.cellSpinner stopAnimating];

cell.cellImageView.image = cellImage;
return cell;
}


The first part of this method is standard table view cell stuff, but then I grab the DeviantDownload object for the row being displayed. If the image accessor method returns nil, I show the cell's activity indicator (aka "spinning wait-a s-cond doohickey") and assign the controller as the delegate of the download. If the object doesn't return nil, I make sure the activity indicator isn't animating and stick the download's image into the cell's image view. The first time this gets called for any particular row, image will return nil, which will trigger the download to start.

I also implement the DeviantDownload delegate method that gets called when the download is complete:

- (void)downloadDidFinishDownloading:(DeviantDownload *)download
{
NSUInteger index = [downloads indexOfObject:download];
NSUInteger indices[] = {0, index};
NSIndexPath *path = [[NSIndexPath alloc] initWithIndexes:indices length:2];
[self.tableView reloadRowsAtIndexPaths:[NSArray arrayWithObject:path] withRowAnimation:UITableViewRowAnimationNone];
[path release];
download.delegate = nil;
}


All I do here is figure out the index of the object that I've been notified has finished download and I build an index path based on it. I then tell the table to reload that one cell's row. Doing that will trigger tableView:cellForRowAtIndexPath: to get called again for the row that has completed and this time that method will get a valid image and will stop the activity indicator and show the downloaded image instead.

Ta da! That's it. It's really not any harder than using threads or NSOperationQueue once you get a feel for using the run-loop based approach.

If you want to try it out, you can download the full project right here. As always, the code is free for use without any restrictions or limitations, I'd just ask that you don't republish it unchanged and claim credit for it.



83 comments:

Heroic Autobot said...

Excellent article!

A quick correction, I think.

You say: I declare a static BOOL to keep track of whether I've already kicked off a download. If image is nil and I haven't previously kicked off a download, I kick one off asynchronously.

But the code has 'downloading' declared as a non-static local variable. I think this causes the code to launch more downloads than is necessary.

Jeff LaMarche said...

Whoops! Indeed, it does. I meant to make it static. :) I'll fix and repost.

Mike said...

Nice article. Looks like you are releasing an autoreleased NSError here:

NSError *error = [NSError errorWithDomain:DeviantDownloadErrorDomain
code:DeviantDownloadErrorNoConnection
userInfo:nil];
....
[error release];

Luke Redpath said...

Jeff, nice tutorial and I agree with your assertion about using the async API over threading.

I've been experimenting with CellControllers recently which is a great place to do this downloading, distinct from your model classes. CellControllers are just NSObjects that manage a single model object to be displayed in a cell.

Anyway, I digress; there is one weakness to your code and that is that it doesn't handle cancellation of downloads when the cell goes off-screen. There's no reason to keep image downloads going if the user is flicking through your list rapidly as the results will never be seen.

My workaround to this is two-fold. First of all, I implement a base cell (a subclass of UITableViewCell) that overrides willMoveToSuperview to detect when the cell scrolls offscreen. It also defines a delegate protocol, CellDelegate and notifies its delegate when it moves off screen.

Then, this is where my CellController object comes in. The CellController is responsible for creating the cell for the model object it manages and any custom behaviour relating to that model/cell. When it creates the cell, it registers itself as the delegate. The CellController starts off the download but if its notified by its cell that its gone off-screen, it cancels the download.

I'll see if I can get around to writing my own blog post with the code for the above soon; its working quite nicely for me at the moment in an iPad app I'm developing.

Jeff LaMarche said...

Mike:

Indeed I am, thank you!

Luke Redpath said...

I should mention that CellControllers as a concept is not something I can take credit for, the whole thing was inspired by Matt Gallagher's blog post.

slipkin said...

Very nice, thanks!

Small suggestion: It might be nice to set download.delegate to self *before* starting the download process via download.image. Otherwise--depending on the precise timing and interrupt capabilities of the NSURLConnection callbacks--it might be conceivable that all the networking starts and finishes without the delegate's getting a chance to see downloadDidFinishDownloading come through.

D-A-P said...

how do you handle slow network that your image is still loading and the user scrolled further down/back ?
do you keep downloading? do you cancel the download ?

Jeff LaMarche said...

Luke:

This is a simple example, however, I'm not sure I agree. Why would I want to discard the partially downloaded image because they've scrolled off? They might very well scroll back, in which case, why start all over when I've already pulled down some amount of the data? Now, in some applications, there might be a valid reason to cancel the download when the cell scrolls off, but I don't agree that it's the right approach her or, in fact, in most situations. In fact, in many applications, I'd probably have all the cells kick off their downloads in the background right away so the user never saw the spinner. Again, YMMV, and the specific needs of an app will dictate. Since this app is just an example, there's really isn't a mandate either way.

I'll be honest with you, though, I'm not really a fan of the Matt's Cell Controller approach. I actually inherited an app using that approach and had to maintain it for a while, and I really felt like it wasn't a good tradeoff in the long run.

Unlike using async vs. threads, this is clearly one of those areas where there's more than one right way, and some people (like Matt, I'm sure) will disagree with me. But I think the added complexity of that design pattern doesn't buy you enough to justify the complexity it adds. Cell controllers add additional object creation and deallocation overhead and (more importantly) add significant complexity to the code. Unless you have literally dozens of different distinct cell styles in your application, there's not much benefit to be had IMO, and even if you do, there are usually better ways to refactor. To my eye, it's over-engineering. It's a clever design, but in the long run, it's not a good tradeoff.

Jeff LaMarche said...

D-A-P:

In this case, I just keep them downloading. As I explained in an earlier concept, I don't generally think it's efficient to throw away partially downloaded data and start over. Depending on the needs of your specific application, you might find situations where you did want to do that, but here, I've kept things simple and done what I think will usually be the best approach, which is to keep downloading them once they've started.

Jeff LaMarche said...

slipkin:

Nope, not an issue. Remember, no background threads. It is impossible for that to happen because it won't even check for data until the start of the next run loop, which can't happen while my method is executing. Therefore, the order of the commands really don't matter.

It's a little hard to get used to run loop integration if you're used to threads, but it's actually simpler because things like what you describe can't happen.

Jeff Kelley said...

Very good article. It would be interesting to pit this approach against the threaded approach in terms of performance to see what the advantages of using NSURLConnection callbacks really are.

I'm wondering, too, if there's a better way to update the table view cell when the download is complete, perhaps using KVO? It seems heavy-handed to reload the entire row.

brandon said...

Killer tutorial!

It seems that doing it the way you mention in the intro is like "double dipping" with the threads. Why allocate more threads when NSURLConnection is already async.

Timos said...

Very timely article. I am just getting into using threads myself. I am trying to save large amounts of data and have noticed that when I do this on the main thread it slows down the app. For this application would you recommend using threads?

Luke Redpath said...

Jeff, I guess it depends on your definition of complexity. Like you say, if you have only a few types of cell whose behaviour doesn't vary then it doesn't buy you very much.

But if you do have a large number of different types of cells, each with their own behaviour, then the resulting spaghetti of if statements and a bloated table view controller is a sure sign that you need to refactor IMO.

I'd be interested in hearing how you would go about refactoring this but to me, the CellController approach doesn't add significant complexity (I think it simplifies the code by making each class's responsibilities clearly defined).

Jeff LaMarche said...

Jeff:

I originally used KVO on this, and then changed to using the delegate. KVO requires writing less code, but isn't any better performing (if anything, I'd bet it performs a little worse), and it makes your code harder to read and tends to break down the walls between model and controller code. There's absolutely nothing wrong with using KVO, but after putting some thought into it, this seemed better. Again, one of those 'not clearly a right way' situations.

Don said...

Great article! I've found, though, that the iPhone OS has problems when you spawn too many NSURLConnections. It seems to lose some of the download requests. I found this out when I tried implementing code similar to MultipleDownload.m -- even running it in the simulator, it wouldn't download everything I queued up to download if I requested more than 150ish at once.

Have you tried spawning that many NSURLConnections at once? I'd be curious to see if all the images get downloaded.

Jeff LaMarche said...

Luke:

But if you do have a large number of different types of cells, each with their own behaviour, then the resulting spaghetti of if statements and a bloated table view controller is a sure sign that you need to refactor IMO.

I'm really not trying to pick a fight, here. There are many ways to skin a cat. The idea of a cell controller is fine, I just didn't find Matt's Cell Controller implementation to jive for me and prefer different approaches when I design applications with many types of cells. Usually, I generalize my cells rather than give them controllers. I have used controllers for cells before, but I don't build my architecture around them.

But, you're setting up a false dichotomy. It's not "use Matt's idea cell controllers", or "use dozens of nested if statements". If that approach works for you, great! But it's hardly the only way to deal with that situation, and I've yet to see a situations where I, personally, would choose that approach. It's okay for us all not to agree 100% on the best way of doing everything, you know, and it's always possible that I will encounter a situation that changes my mind.

Regarding cancelling downloads when the cell disappears, in my experience, if the list is long enough, firing off lots of connections, even if they are async, results in sluggish performance.

Yes, that's a possibility. I'd call that part of Knuth's 3%. Don't anticipate it, fix it if your testing shows it to be a problem. I try hard not to engage in premature optimization in both my client code and my tutorial. Making it work ideally for your situation is your job :)

Jeff LaMarche said...

Don:

I haven't. In situations where I anticipate that volume of connections, I actually create something similar to an NSOperationQueue, but it's not an operation queue, it's a "connection queue" that uses run-loop integration instead of threads, but only allows a certain number of connections to be active at a time.

I intend to write a post on it someday, but haven't gotten around to it. I'm still tweaking the idea, but yeah, depending on bandwidth and memory, there are definitely limits to how many connections you should have going at once.

Jeff LaMarche said...

Don:

I should mention that even with low-level socket programming, there is a limit to how many connections you can have efficiently going at once, it's not necessarily a CFNetwork thing, though being a higher-level implementation, it probably does add some additional overhead.

Jeff LaMarche said...

The static turns out to be a problem. downloading needs to be an instance variable. I'm working on the fix now.

Luke Redpath said...

Jeff, I'm not trying to pick a fight either; as I said on Twitter, its hard to convey what I think makes sense without demonstrating it through code, at which point we're straying way off-topic, so I'll take this to my own blog.

Don said...

Hey Jeff. I'd love to see that "connection queue". I implemented a version of that and found that, even on an iPad, having even just five concurrent NSURLConnections leads to unreliable downloads.

Granted, I was doing some extra processing -- I needed to make sure the photos were saved in the correct order to the camera roll.

Jeff LaMarche said...

Don:

I'm not sure, but I think there may be something that's changed in CFNetwork for the iPad. I've noticed a LOT of programs have networking problems. I don't know what's going on, but it's happening in too many places to be a coincidence.

When I finish the connection queue, I'll do a blog post on it. I'm hoping to find out more about the undocumented CFNetwork changes that have happened before I do that. This post may need updating as well. This post is consistent with the CFNetwork documentation, but apparently it doesn't reflect the current state of reality. :(

Me said...

Would it possibly be a good idea to ship off the actual UIImage creation from the data into an NSOperation?

If the image can be of arbitrary size downloaded from a source you don't control, if you happen to hit a particular large file then that would cause a bit of UI hiccup, no?

That's said of course without really having a good sense for how large it would have to be before it could pose a problem.

Jeff LaMarche said...

Me:

Probably not unless it's a really, really large image. Turning an in-memory chunk of data into an in-memory image is pretty quick, even if it's large. Now, if you're talking uncompressed tiffs from a stock service or something, then maybe. Again, it's 3% stuff. If you find you have problems because of the size of images, then look for a solution. Don't anticipate what might be a problem. Measure and then fix performance problems that actually arise.

Jonathan said...

Did you forget to release con for NSURLConnection? I had static analysis on, it is showing con is owning reference with +1 reference count.

Akuma said...

Thank you for this post.
i wish that code would have been part of "beginning iphone development".
I never understood why you guys did neglect networking in it.

Is this code better than using "NSoperationQueue"? if so, i fail to understand why, because with that i can limit the concurrent downloads to 2(or any number) at a time.

Jeff LaMarche said...

Jonathan:

Nope. Static Analysis gives false positives sometimes, and this is one. Every NSURLConnection will end by calling either connection:didFailWithError: or connection:didFinishLoading:, and in both of those methods, we release the connection.

We could squelch the static analyzer by storing the connection in an instance variable, but the only value to doing that here is to appease the analyzer.

Jeff LaMarche said...

Akuma:

NSOperationQueue does let you limit the number of operations, but it also incurs additional overhead. There's not a one-size-fits-all answer to your question, but as of right now, Apple recommends using run-loop integration rather than threads for networking.

It is possible to create a queue of network connections on the main thread, but it's a little work. It will be the subject of a future posting.

As for why we "neglected" the topic, it was a simple matter of room. We originally planned to do networking, but when the manuscript to our 350 page book got to 525 pages, we had to make some decisions about what to cut, and networking was one of the things that got cut. Although important, it's not as foundational as other things, and covering it properly needed more pages than we had available to us in the first book.

We cover the topic in More iPhone 3 Development, however.

eridius said...

From reading this code, it appears all your networking operations will only run while the runloop is in the default mode. This means no networking will be handled while the user is scrolling the tableview. This seems like an oversight.

Jeff LaMarche said...

Eridius:

Yep, that's a great point. I'll update the code to use common modes.

Thanks for the suggestion!

Ivo said...

Forgive me if this is a silly question for I am much newer to iPhone development than most of the commenters above. If you marked receivedData as @private in the interface, why is it necessary to declare the property in an exteansion in the implementation?

Jeff LaMarche said...

ivo:

It's not "necessary", I just find memory management easier to visualize when I do it always through the declared properties, but I don't want external classes utilizing the property.

jeanphilippe said...

Hi,
I've this error message when i want to test the project.

/Users/G5/Desktop/Deviant Downloader/Classes/DeviantCredentialsViewController.xib:0:0 File does not exist at path 'file://localhost/Users/G5/Desktop/Deviant%20Downloader/Classes/DeviantCredentialsViewController.xib'

Regards

JP

David said...

Great article. Thank you Jeff.

Why when the NSURL connection was set up did you say startImmediatey:NO? And then proceed to add it to the runloop?

NSURLConnection *con = [[NSURLConnection alloc]
initWithRequest:req
delegate:self
startImmediately:NO];

What is the difference, why wouldn't you say startImmediately:YES?

Bob said...

I've found that networking on the main thread doesn't really provide a good user experience. If the user starts fiddling around with the UI, handling the tracking seems to take precedence over handling of the network events.

Ideally, then, I would want to handle all networking calls asynchronous in a background thread, but handling all of the logic to make that work--such as limiting the number of concurrent downloads or canceling downloads that are no longer needed--is a lot of effort. Something like your Connection Queue but running on its own thread seems appealing.

What I've ended up doing is using an NSOperationQueue to handle downloads on a thread per download basis. Yes, this is technically more resource intensive than it ought to be, but I'm going to invoke your 3% rule and say that I've found the theoretical performance impact to be undetectable but the responsiveness improvement from the user's perspective to be noticeable.

Actually, the funny thing is that I make the calls on the dedicated threads asynchronously as well, because you don't get the full control over the download unless you use the asynchronous API. As long as I have a dedicated thread, however, I do try to make some use of it by handling tasks such as parsing JSON responses or converting images or caching results to disk in the background thread as well.

Malcolm Hall said...

FYI the async api does use a thread.

Matt said...

Have downloaded the tutorial and the DeviantCredentialsViewController.xib may be missing. New to iPhone dev so may be doing something wrong or building incorrectly.

Jeff LaMarche said...

Bob:

What run loop modes are you running for? As Eridius points out above, the default behavior of NSURLConnection is to run in the default mode, which doesn't execute during things like table scrolls.

I've changed the code in this post so that it uses all common run loop modes, and that should take care of that particular problem.

There definitely are times where it makes sense to use an operation queue, but the vast majority of networking needs don't require threads.

Jeff LaMarche said...

Matt:

I'll check out the project and fix it if it needs to be fixed.

Jeff

Neil Mix said...

Hey Jeff, good post. For the benefit of your readers I wanted to mention a couple subtle gotchas with NSURLConnection that have bitten me in the past:

1) I too would prefer not to store NSURLConnection in an instance variable, but I've found that this pattern often introduces unexpected memory-related crashes because NSURLConnection doesn't retain its delegate. For example, does your view controller destroy the "downloads" array when it receives a memory warning? If so, you're vulnerable to crash because it's possible for the downloads array (and therefore individual DeviantDownload objects) to be released before a connection has finished loading. In your particular case you're probably OK (or at least crash rarely enough not to really matter), but in my experience this pattern is usually a ticking time bomb. As a result I always store the connection in an instance variable and I make sure to cancel the connection and set its delegate to nil before releasing it. (Redundant but double-sure.) As written, a DeviantDownload object requires its owner not to release the object until the connection is finished, which for me is too subtle behavior to require of an object's owner. (Another solution to this problem is for DeviantDownload to retain itself until the connection completes. I personally don't like to do that, but that's a matter of taste.)

2) If the download results in an HTTP error such as 404 or 500, DeviantDownload will present its delegate with a nil image object. This is because NSURLConnection handles connection errors but treats protocol errors as successful connections. Since 404 and 500 are HTTP protocol errors, DeviantDownload will process the resulting data (an HTML error page) as if it's an image, causing imageWithData to return nil since the data can't be parsed as an image. To handle such errors, I will usually do something like this (sorry for the sucky formatting):

- (void)connection:(NSURLConnection *)aConnection didReceiveResponse:(NSURLResponse *)aResponse {
NSHTTPURLResponse *response = (NSHTTPURLResponse *)aResponse;
if ([response statusCode] >= 400) {
// handle error
} else {
[receivedData setLength:0];
}
}

Joe said...

Thank you thank you thank you! Threads are essential in clothing, but not with async loading.

I was just about to ask the same question David did (about run loop modes, the one Eridius effectively answered).

In turn, I've made some changes in my own apps and - sure enough - that really helps. I encourage anyone else who may have skipped that tweeze in their own code to please look again. Make sure you're taking full advantage of the run loop.

Jeff LaMarche said...

Neil:

Thanks for the comments.

The note about the response codes is a really good one. Probably should have included the response code checks, but didn't occur to me when I was writing. I'll update the code when I get a chance.

As for your other comment, I understand your point, and in more complex situations, I actually usually do have an object responsible for managing (and retaining) the connections. For simple, straightforward situations like this one, I've never had a problem with fire-and-forget on the connection. I can understand why you might choose to retain it, but I've done it this way literally dozens of times without even sporadic problems.

In fact, I just spent 15 minutes trying to generate an error using this app and I wasn't successful, so I think your concerns are valid, but perhaps academic in this particular situation.

Daniel Tull said...

I was using connection controller objects that would download data using an asynchronous NSURLConnection and then handling the parsing of that data. The issue is like Bob explains above, that the parsing then destroys the experience for a second or so.

This led me down the route of using NSOperation to perform this part, so that the connection happens and the operation handles the parsing to not destroy the responsiveness.

At first I was using a synchronous NSURLConnection, but I didn't like the lack of options that gave; Handling the response only when the data had fully downloaded. Finally I found that I could use an async NSURLConnection if I kept the operation's runloop running with CFRunLoopRun().

My question then is, when you make asynchronous NSURLConnections from an operation, does the system handle this on the same runloop as all the other asynchronous NSURLConnections from other operations?

In the article, you mention synchronous NSURLConnections from a thread, but as far as I could see didn't mention running an asynchronous NSURLConnection from a thread. I'm not overly sure how to test my code to see if this is happening.

Jeff LaMarche said...

Daniel:

Once you've received the data from the connection, normal rules apply. If you've got heavy lifting processing to do, then you should do THAT in a thread (preferably using NSOperationQueue). Though the system may spawn background threads, you still need to assume that your callbacks are happening on the same thread that the connection was created on, and you do not want to be blocking the main thread to, for example, create a thumbnail from a large image, or something else processor intensive.

This advice applies just to the actual network connections themselves, and there are plenty of reasons to spawn threads, just don't spawn a thread so that you can use blocking IO. That's inefficient. On the other hand, you don't want to avoid using threads anywhere in your application simply because you've got asynchronous network communications going on.

Bob said...

I *thought* I had scheduled my early experimental code to run in the common run loop modes, but I may have been mistaken. Still, are you sure that the common modes handle all the different modes the main thread is likely to run in?

I found the operation queue to be extremely valuable. The best example is an image gallery where 30 different thumbnails might be visible. You really want to limit the number of simultaneous downloads there. And having the ability to cancel a download request that is just waiting in the queue but hasn't actually started downloading yet when the image scroll off screen is a big performance and efficiency win.

As you mentioned in another response, if you're going to do any post processing on a response you probably want to do that in a background thread. And as long as there is going to eventually be a background thread, you're not really hurting anything by firing it up a bit early and letting it handle your networking calls as well.

Daniel:

It sounds like you're essentially coming to the solution I ended up using. Asynchronous networking calls on an operation thread work really well, although as you noted there's a lot more code to write because you have to keep the run loop going while the download progresses.

As I understand it, all the networking code actually runs on an entirely separate system thread which can handle all outstanding calls very efficiently, but the asynchronous callbacks are dispatched to whatever thread you were on when you started the request. So each operation will get the callbacks on its own thread.

Daniel Tull said...

Jeff:

Yes, however as I recall, NSOperationQueue manages the threads, including reuse so there's not a huge overhead in making threads anyway - especially, as Bob says, we'll likely need them to process the data.

Bob:

Sounds like it. :) I've been playing around and I can't really say if the asynchronous network calls are happening all together as you suggest (and as I suspect) they are.

Jeff, Bob:

I've changed my code slightly, so that inside my operation, I've unscheduled the URL connection from the current run loop and instead scheduled it to run in the main run loop. Would this resolve the issue of having network calls in multiple run loops?

Chris Gummer said...

Thanks for the detailed post Jeff!

Going back to Neil's comment on memory management. By not retaining the connections how would you manage deallocating the containing view controller? For example the sample project will crash when hitting the back navigation button, as I assume the connections are trying to message an invalid delegate reference.

Jeff LaMarche said...

Chris:

Have you actually gotten the application to crash by hitting the back button? I haven't. I think your reasoning misses an important point - NSURLConnection is one of the few classes that actually retains its delegate. As long as the request is active, the delegate won't go anywhere.

The documentation does not make this point obvious, but it is easy to confirm by checking the retain count in the debugger.

Jeff LaMarche said...

As you mentioned in another response, if you're going to do any post processing on a response you probably want to do that in a background thread. And as long as there is going to eventually be a background thread, you're not really hurting anything by firing it up a bit early and letting it handle your networking calls as well.

I'm not sure I agree with this assumption. The iPhone and iPad are both single-core devices, so every spawned threads takes system resources away from the main threads. If you don't start the operation until you have the data, you defer creating the thread until you need it rather than having blocked threads eating up system resources. If you're doing the downloads sequentially on a single thread, the overhead isn't great, but if you're spawning a separate thread per image, then the impact could be substantial even with an operation queue.

The asynchronous APIs are designed to give the best performance. If you look at Apple's Lazy Table Images sample code, they don't do blocking network calls on the thread, they use the asynchronous calls and then afterwards use an threaded operation to the processing afterwards.

Chris Gummer said...

Jeff, yes I have a few times now. It occurs at [DeviantDownload connectionDidFinishLoading:]

2010-05-27 11:24:30.508 Deviant Downloader[2082:207] *** -[DeviantDisplayViewController respondsToSelector:]: message sent to deallocated instance 0x3e26d60

CSI said...

Great How-To Jeff. Thanks a lot for that. I am pretty new to all of this and I am trying to implement it into my code.

As far as I understand you use an array full of DevianDownload objects which have the urlString set.

I actually use an array filled with Dictionaries which contain the URL to the image. I was trying to implement this in my code but I am not sure how I can get downloadDidFinishDownloading: working. It does jump into that method after downloading the image.

This is the code I used

// Download async image
AsyncImageDownload *download = [[AsyncImageDownload alloc] init];
download.urlString = [dict objectForKey:kDBImagePath];

UIImage *cellImage = download.image;

if ( cellImage == nil )
{
// Start loading spinner
[cell.activityIndicator startAnimating];
download.delegate = self;
} else {
[cell.activityIndicator stopAnimating];
}

cell.imageView.image = cellImage;

CSI said...

I got it working...

The only problem I have is that it will not go to

- (void)download:(AsyncImageDownload *)download didFailWithError:(NSError *)error

If the image is not at the specified urlString.

My store said...

The best home based alife shoes sale business to start is one that alife sneakers sale new york gives you the ability to alife sneakers for sale leverage your time. Basically, you want a cheap alife sneakers business that continues to operate alife sneakers and earn income for you even while you’re not working. cheap alife shoes sale This is key if you want to spend more time with alife everybody hi your family. Businesses that lack alife everybody low this feature end up owning you, alife shoes online sale and you’ll most likely have less free time than you did as a 9 to 5 employee.

My store said...

Introduced in 1966, Onitsuka Tiger Mexico 66 were the first shoes with the famous Tiger Stripes. ASICS GEL-KINSEI 2 were worn at the Mexico Olympic Games in 1968. With lace up asics gel kinsei 2 sale front and tracks in asics gel kinsei 2 mens the side and suede asics kinsei 2 for sale effect trim on front and back. Purchase Asics Onitsuka Tiger mini cooper and receive onitsuka tiger mini clubman Learn about running shoe types such as motion control, onitsuka tiger mid runner performance training and stability onitsuka tiger mexico 66 mens shoes and discover which type authentic onitsuka tiger shoes sale best fits your pronation type.The original Kinsei was asics gel stratus mens shoes classified by Asics as a asics gel stratus 2.1 for mens Stability+ shoe. The Kinsei II is asics gel stratus 2.1 sale Neutral+ shoe. If you look at the Asics Shoe Fit Chart

My store said...

These shoes made my trip to onitsuka tiger mexico 66 yellow black Argentina that much more enjoyable. I needed a pair of onitsuka tiger mexico 66 by asics shoes that I could walk great mens onitsuka tiger mexico 66 distances in and stand in for asics onitsuka tiger mexico 66 several hours at a time. These were my answer. They have great support, and give the feeling asics tiger mexico 66 that one is walking on air. They did get dirty, when we went rappelling. onitsuka tiger mini clubman Although you are not supposed to, I washed them on gently cycle in my front loader onituka tiger mini cooper washing machine. No harm, no foul. I highly recommend them.

My store said...

They are your New Balance for sale , so here at New Balance Altoona we know new balance 580 for sale that each athlete has different needs, new balance 574 and different feet. We strive to new balance 996 provide the right shoe and a perfect fit for new balance 993 every customer. All athletes understand the new balance 1500 connection between superior new balance 1300 performance and well-fitted shoes - new balance 1400 that's why we carry New Balance 576 sneakers and footwear in a wide new balance 577 variety of sizes and widths so that we can provide the new balance 999 best fit for the best price.

airshoes said...

I like your blog. Thank you. They are really great .
Some new style Puma Cat shoes is in fashion this year.
The Nike Air Shoes is a best Air Shoes .
Red Shox is good and Cheap Nike Shoes.
If you are a fans of Puma basket,we would offer the good and Cheap Puma Shoes for you .the nike shox torchis best christmas gift now.
Running Shoes
Nike Shox R4
Nike Air Force Ones
The information age is really convenient .

Matt said...

I think this idea is great, but not without problems. First, it (your demo) works great on the simulator but when you run it on the device, all it does is crash when i try to begin scrolling. Not sure where the problem is there.

Before it crashes the scrolling is extremely sticky. I am pretty sure the culprit is

[con scheduleInRunLoop:[NSRunLoop currentRunLoop] forMode:NSRunLoopCommonModes];

which is also a key line for the solution and purpose of the post. This was tested on a 2nd gen ipod touch, which might be the reason, I haven't tested it on a 3rd gen yet. No point if it doesn't work well on second gen.

I'm still not sure how to successfully load and scroll at the same time because if you don't include

[con scheduleInRunLoop:[NSRunLoop currentRunLoop] forMode:NSRunLoopCommonModes];

it will wait till scrolling has essentially stopped to run the nsurlconnection delegate methods.

unfortunately this seems to be an ineffective solution (at least on the 2nd gen). Sorry

ants said...

Hey Jeff,

I have also tried to compile you project without change and DeviantCredentialsViewController.xib seems to be missing... or is it just me?

Ants

Michael Jacksson said...

buy viagra
viagra online
generic viagra

Bryan said...

Just delete the DeviantCredentialsViewController.xib
from your project and this will run.

Igor said...

Just noticed one ting in console: "Unknown class ZoomableImageScrollView in Interface Builder file."

All seems to be working fine, but this files name looks very interesting.

another game said...

another object c++ newbie question.
what happens if user goes away from the table view, (I suppose tableView & controller goes away sometime)
and tries to use your 'delegate' variable?

I see you used "assign" for 'delegate' variable.
When the controller goes away, your 'delegate' variable would point to something that's already destroyed?
or "respondsToSelector" is safe in this regard?

if ([delegate respondsToSelector:@selector(downloadDidFinishDownloading:)])
[delegate downloadDidFinishDownloading:self]

I'm trying to do similar thing but from c++ code & from a separate thread(oops, you are suggesting non thread approach in this article, but I gotta try something).

I wonder if checking [delegate respondsToSelector] is enough to ensure that my application won't broke due to illegal memory access.

Thank you very much for your excellent article & book!

another game said...
This comment has been removed by the author.
App said...

Hi Jeff,

I'm experiencing the same problem as Chris with the app crashing.

Steps to reproduce:

1) Entered "blu-blu" in text field for Deviant user.

2) Table begins to load data.

3) Start scrolling down really fast.

4) Navigate back by hitting, "Deviant Downloader". App will crash, sometimes you don't even have navigate back and it will crash.

I ran it in the debugger and got Program received signal: “EXC_BAD_ACCESS”.

I ran it in the Profiler and the delegate is a Zombie here:

- (void)connectionDidFinishLoading:(NSURLConnection *)connection
{
self.image = [UIImage imageWithData:receivedData];
if ([delegate respondsToSelector:@selector(downloadDidFinishDownloading:)])
[delegate downloadDidFinishDownloading:self];

[connection release];
self.receivedData = nil;
}

I'm using Xcode version 3.2.3 and iPhone Simulator 4.0. Thanks.

BP

King Bayern Munich said...

About the wonderful, very pleased to see this article, learn some things, and view the text is recognized. Thank you for sharing. At the same timei love Butterfly Valves

App said...

Hi Jeff,

I made a small tweak to avoid the Zombie issue described in the previous post I submitted. Is it ok if the *controller variable in RootViewController is made an instance variable and initialized only once in viewDidLoad method, instead of being a local variable inside textFieldShouldReturn which gets instantiated each time the enters a search?

King Bayern Munich said...

Good writing, and I very much agree with your thoughts and insights. Hope that more could write such a good word, I said, to continue coming to visit, thank you for sharing.i love Straightening Machine

King Bayern Munich said...

Are pleased to re-visit your blog, from which I learned a lot of knowledge, and totally agree with your point of view, I hope you can be the exhibitions, once again thank you for sharing such a wonderful text. I will wait to see what's! Thank you!i love Offset Butterfly Valve

fantu❤ said...

hey,christmas gift basket ideas this is one of the best posts that Louis Vuitton Monogram Canvasve ever seen; louis vuitton have an outlet may include some more ideas in the same theme. christmas gift exchange ideas still waiting for some interesting thoughts from your side in your next post louis vuitton ankle boots
.

SEO Services Consultants said...

Nice information, many thanks to the author. It is incomprehensible to me now, but in general, the usefulness and significance is overwhelming. Thanks again and good luck! Web Design Company

marcoz said...

FDA approved mens health medication viagra is not a drug to be taken lighliy you should read all about the pros and cons regarding the medication before you buy viagra!

Wholesale NFL Jerseys said...

Good post! Thanks for your information! Cheap Football Jerseys, Wholesale NFL Jerseys,MLB NHL Hockey Jerseys From China
Cheap NFL Football jerseys,NHL Hockey Jerseys,NBA Jerseys,MLB Jerseys From China,Wholesale Cheap Football jerseys From China
www.nfljerseys101.com

Arizona Cardinals jersey
Atlanta Falcons jersey
Baltimore Ravens jersey
Buffalo Bills jersey
Carolina Panthers jersey
Chicago Bears jersey
Cincinnati Bengals jersey
Cleveland Browns jersey
Dallas Cowboys jersey
Denver Broncos jersey
Detriot Lions jersey
Green Bay Packers jersey
Houston Oilers jersey
Houston Texans jersey
Indianapolis Colts jersey
Jacksonville Jaguars jersey

Wholesale NFL Jerseys said...

Kansas City Chiefs jersey
Miami Dolphins jersey
Minnesota Vikings jersey
New England Patriots jersey
New Orleans Saints jersey
New York Giants jersey
New York Jets jersey
Oakland Raiders jersey
Philadelphia Eagles jersey
Pittsburgh Steelers jersey
San Diego Chargers jersey
San Francisco 49ers jersey
Seattle Seahawks jersey
St.Louis Rams jersey
Tampa Bay Buccaneers jersey
Tennessee Titans jersey
Washington Redskins jersey
Youth NFL jersey
Women's NFL Jerseys

Wholesale NFL Jerseys said...

Atlanta Braves Jersey
Baltimore Orioles Jersey
Boston Red Sox Jersey
Chicago Cubs Jersey
Chicago White sox Jersey
Cincinnati Reds Jersey
Cleveland Indians Jersey
Colorado Rockies Jersey
Detroit Tigers Jersey
Florida Marlins Jersey
Houston Astros Jersey
Houston Colts Jersey
Kansas City Royals Jersey
Los Angeles Angels Jersey
Los Angeles Dodgers Jersey
Milwaukee Brewers Jersey
Minnesota Twins Jersey
Montreal Expos Jersey
New York Mets Jersey

h4ns said...

What youre saying is completely true. I know that everybody must say the same thing, but I just think that you put it in a way that everyone can understand. I also love the images you put in here. They fit so well with what youre trying to say. Im sure youll reach so many people with what youve got to say.

Arsenal vs Huddersfield Town live streaming
Arsenal vs Huddersfield Town live streaming
Wolverhampton Wanderers vs Stoke City Live Streaming
Wolverhampton Wanderers vs Stoke City Live Streaming
Notts County vs Manchester City Live Streaming
Notts County vs Manchester City Live Streaming
Bologna vs AS Roma Live Streaming
Bologna vs AS Roma Live Streaming
Juventus vs Udinese Live Streaming
Juventus vs Udinese Live Streaming
Napoli vs Sampdoria Live Streaming
Napoli vs Sampdoria Live Streaming
Fulham vs Tottenham Hotspur Live Streaming
Fulham vs Tottenham Hotspur Live Streaming
AS Monaco vs Marseille Live Streaming
AS Monaco vs Marseille Live Streaming
Alajuelense vs Perez Zeledon Live Streaming
Alajuelense vs Perez Zeledon Live Streaming
Technology News | News Today | Live Streaming TV Channels

Michael said...

Hi I am not sure if this blog is still open. I have ran the app entered lots of Deviant users and it just doesn't download anything. Is it because I am using the iphone4 simulator?

Devin said...

Its very much impressive blog post.I'll see if I can get around to writing my own blog post with the code for the above soon; its working quite nicely.
generic viagra Online

Hizon said...

Yeah, you really are right about your ideas. Many people have some misconception about this thread thing. Your codes are awesome and there are all useful to us. Your really have a good point of sharing this idea. The codes here could be a big help for us.
skin tags

healthpharmacyrx1 said...

priligy Bestellen Online
cipro auf Rezept
zithromax Kaufen Apotheke
strattera Rezeptfrei Kaufen
lipitor Kaufen
lovegra Rezeptfrei
priligy Bestellen Rezeptfrei
kamagra oral jelly Bestellen
Buy kamagra oral jelly online
Prix accutane Pharmacie
lovegra acheter
lovegra france
kamagra oral jelly Rezeptfrei Kaufen
levitra Bestellen

Dave said...

This was perfect, exactly what I was looking for. It was a huge help, so thank you, thank you, thank you.

I am having a problem though because I have multiple sections in my data for the table. I think it might be because I'm not sure how to handle the DeviantDownload delegate method for the index path because I have multiple sections. What I did was turn "downloads" into an array of arrays, one array per section. Then in the delegate method I iterate through each array in the array of arrays until I get the index path. This seems to work as the correct images are getting loaded.

But there is a problem. It will only lazily update the rows for the first section (let's say I only have 2 cells), and the next section has no images! However, if I scroll the table, cells that come into view have the image loaded, and when I scroll back to the top of the table the cells in the second section that previously had no image, now has the image.

What do you think is going on? I'd love to see an update to this tutorial to handle multiple table sections.