Cocoa Touch Tutorial: Using Grand Central Dispatch for Asynchronous Table View Cells

One of the problems that an iOS developer will often face is the performance of table view cells. Table view cells are loaded on-demand by the UITableView that they’re a part of; the system calls ‑cellForRowAtIndexPath: on the table view’s dataSource property to fetch a new cell in order to display it. Since this method is called (several times) while scrolling a table view, it needs to be very performant. You don’t have very much time to provide the system with a table view cell; take too long, and the application will appear to stutter to your users. This kills the immersion of your application and is an instant sign to users that the application is poorly-written. I guess what I’m saying is that this code needs to be fast. But what if something you need to do to display the table view cell takes a long time—say, loading an image?

In my MobiDevDay presentation a couple of weeks ago, I illustrated a solution to this problem: Grand Central Dispatch. GCD, Apple’s new multiprocessing API in Mac OS X Snow Leopard and iOS 4, is the perfect solution for this problem. Let’s take a look at how it works.

Grand Central Dispatch operates using queues. Queues are a C typedef: dispatch_queue_t. To get a new global queue, we call dispatch_get_global_queue(), which takes two arguments: a long for priority and an unsigned long for options, which is unused, so we’ll pass 0ul. Here’s how we get a high-priority queue:

[sourcecode language=”objc” gutter=”false”]dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0ul);[/sourcecode]

It’s pretty straightforward. To use this queue, we add blocks of code onto it. Typically this is done with blocks (Apple’s new code encapsulation extension to the C language), though it can be done with C functions. To submit a block onto a queue for execution, use the functions dispatch_sync and dispatch_async. They both take a queue and a block as parameters. dispatch_async returns immediately, running the block asynchronously, while dispatch_sync blocks execution until the provided block returns (though you cannot use its return value). Here’s how we schedule some code onto a queue (we’ll assume this code runs after our previous example, so queue is already defined):

[sourcecode language=”objc” gutter=”false”]dispatch_async(queue, ^{
NSLog(@"Hello, World!");
});[/sourcecode]

It’s very easy to forget the ); at the end of that line, so be careful.

How does this apply to table view cells? Let’s take a look at a typical scenario for loading images from disk:

[sourcecode language=”objc” firstline=”33″]- (UITableViewCell *)tableView:(UITableView *)tableView
cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
static NSString *CellIdentifier = @"ExampleCell";

UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:CellIdentifier];
if (cell == nil) {
cell = [[[UITableViewCell alloc] initWithStyle:UITableViewCellStyleDefault
reuseIdentifier:CellIdentifier] autorelease];
}

// Get the filename to load.
NSString *imageFilename = [imageArray objectAtIndex:[indexPath row]];
NSString *imagePath = [imageFolder stringByAppendingPathComponent:imageFilename];

[[cell textLabel] setText:imageFilename];
UIImage *image = [UIImage imageWithContentsOfFile:imagePath];
[[cell imageView] setImage:image];

return cell;
}[/sourcecode]

The problem with that code is that creating image blocks until ‑imageWithContentsOfFile: returns. If the images are especially large, this is catastrophic. Modifying this code to use Grand Central Dispatch is simple:

[sourcecode language=”objc” firstline=”33″]- (UITableViewCell *)tableView:(UITableView *)tableView
cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
static NSString *CellIdentifier = @"Cell";

UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:CellIdentifier];
if (cell == nil) {
cell = [[[UITableViewCell alloc] initWithStyle:UITableViewCellStyleDefault
reuseIdentifier:CellIdentifier] autorelease];
}

// Get the filename to load.
NSString *imageFilename = [imageArray objectAtIndex:[indexPath row]];
NSString *imagePath = [imageFolder stringByAppendingPathComponent:imageFilename];

[[cell textLabel] setText:imageFilename];

dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0ul);

dispatch_async(queue, ^{
UIImage *image = [UIImage imageWithContentsOfFile:imagePath];

dispatch_sync(dispatch_get_main_queue(), ^{
[[cell imageView] setImage:image];
[cell setNeedsLayout];
});
});

return cell;
}[/sourcecode]

First, we create our image asynchronously by using dispatch_async(). Once we have it, however, we have to come back to the main thread in order to update our table view cell’s UI (all UI updates should be on the main thread, unless you like reading crash reports). GCD has a function to get the main queue—analogous to the main thread—called dispatch_get_main_queue(). We can dispatch a block to that thread to update the UI.

By making this simple modification, we can very easily improve the performance of our table view. There are a few steps remaining, however, and this method has one serious shortcoming: if the cell is re-used by the time the image loads, it can load the wrong image into the cell. To get around this, it would be better to cache the images in an array or a dictionary (just be sure to release it in your view controller’s ‑didReceiveMemoryWarning: method). That said, this is an example of something you can do quite easily to improve the performance of your application. The better it performs, the more your users will like it, and that’s the ultimate goal.

The code used in this post is available as a GitHub repository.

Published by

Jeff Kelley

I make iOS apps for Detroit Labs.

23 thoughts on “Cocoa Touch Tutorial: Using Grand Central Dispatch for Asynchronous Table View Cells”

  1. I’m using this approach for asynchronously loading images from the web into my UITableViews and it runs _very- fast. Plus, it’s less effort to implement these few lines of code than using frameworks like HJ or EGO.

    1. Does it make sense to use this approach in case you load images from web? NSURLConnection is already asynchronous, isn’t it? Or is it faster because of putting connections in queue?

      1. The main thing is displaying the rest of the UI quickly. One thing you’ll want to do if loading from the web—especially with long lists—is use something you can cancel, like an NSOperation.

  2. I used your approach on my test app and it’s now running very FAST!!!
    Thank you very much for sharing.

  3. I don’t know what these guys are talking about but this is very, very, very slow when you scroll.

    1. It all depends on what’s in the cell when you load it. This post is about how to load the contents of a cell asynchronously. To make it load faster, you’ll need to analyze your code and find the best optimizations to use.

  4. Does anybody know why without [cell setNeedsLayout]; cell doesn’t update inside dispatch_async(mainQueue…) ?

    1. Andrey, I suspect that without an image, once the cell is rendered, its image view has a size of 0, or is never created. The image view, then, may be re-rendered, but the cell doesn’t know to re-layout its contents when the image is modified. I think I’ll file a bug recommending Apple to add some KVO to UITableViewCell that watches the image property of its image view and calls -setNeedsLayout as necessary.

  5. The best way I have found to ensure that the correct cell is updated asyncronously, isc to do something like:

    UITableViewCell * correctCell = [self.tableView cellForRowAtIndexPath:indexPath];
    [[correctCell imageView] setImage:image];
    [correctCell setNeedsLayout];

    I’m happy with the performance.

    1. Won’t that have the side effect of creating unneeded cells if the user has scrolled off-screen? Might want to look at the -visibleCells property of the table view instead.

  6. Great post; succinct and useful!

    I noticed that you’re creating possibly many one-shot queues for the async call.

    Is there an advantage to creating only one queue, so that each table entry is processed sequentially and you avoid an excessive multi-threading penalty?

    jpap

    1. My apologies: it appears you are in fact using one queue–the global queue–so my query is moot. (My fault for asking before having a full understanding of GCD!)

      Thanks again for your post! :D

  7. Hi, Thank for your effort but Is there any way to load asynchronous image in coverflow sequentially. I load images on coverflow but its out of order. I have put its url in array also but still problem occurs to load image in sequentially. Do you have any idea what to do for this then please share with me. Thanks again.

    1. There definitely is. What you want to do is create your own custom queue:

      dispatch_queue_t myQueue = dispatch_queue_create(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0ul);

      Then, when you dispatch blocks to this queue, they will execute one-at-a-time and in order. Just make sure when you’re done with the queue you release it:

      dispatch_release(myQueue);

      iOS 5 has the ability to create custom concurrent queues, but that wouldn’t guarantee that the operations execute in order.

Comments are closed.