Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Table View Scrolling Async

I am loading an image to a table view cell, each cell has an image. I've adapter a couple tutorials to the code below, but I am still having slow down.

I am loading these images from the documents directory. Any tips or ideas on how to speed this process up?

Edit Revised Code:

Beer *beer = (Beer *) [self.fetchedResultsController objectAtIndexPath:indexPath];
cell.displayBeerName.text = beer.name;

// did we already cache a copy of the image?
if (beer.image != nil) {
    // good.  use it.  this will run quick and this will run most of the time
    cell.beerImage.image = beer.image;
} else {
    // it must be the first time we've scrolled by this beer.  do the expensive
    // image init off the main thread

    cell.beerImage.image  = nil;   // set a default value here.  nil is good enough for now

    [self loadImageForBeer:beer atIndexPath:indexPath];
}
- (void)loadImageForBeer:(Beer *)beer atIndexPath:(NSIndexPath *)indexPath {

    dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0ul);
    dispatch_async(queue, ^{

        UIImage *image = [UIImage imageWithContentsOfFile:beer.imagePath];
        beer.image = image;

        dispatch_sync(dispatch_get_main_queue(), ^{
            UITableViewCell *cell = [self.tableView cellForRowAtIndexPath:indexPath];
            cell.beerImage.image = image;
        });
    });
}
like image 377
Vikings Avatar asked Jul 14 '12 19:07

Vikings


1 Answers

Your algorithm looks pretty good. You've avoided many of the typical pitfalls. If you're still having UI performance problems, I'd suggest a couple of things:

  1. You should try caching your images in memory. You could use NSMutableArray or NSMutableDictionary, but at Best way to cache images on ios app? Caleb discusses the merits of the NSCache class, which simplifies the process. If you do cache images, make sure you respond to memory pressure and purge the cache if necessary. You can respond to didReceiveMemoryWarning or add yourself as an observer to the notification center's UIApplicationDidReceiveMemoryWarningNotification.

  2. Make sure your cached images are thumbnail sized or else you'll always have a little stuttering in your UI (if you need a resizing algorithm, let us know) and it will use up memory unnecessarily;

  3. When you dispatch your image update back to the main queue, you should do so asynchronously (why have that background queue hang around and tie up resources as it waits for the the block to be sent back to the main queue to finish ... this is especially an issue once you have a couple of images backed up during a fast scroll); and

  4. When you dispatch back to the main queue, you should check to make sure cell you get from cellForRowAtIndexPath is not nil (because if cell loading logic gets too backed up (esp on slower devices), you could theoretically have the cell in question having scrolled off the screen and your algorithm could crash).

I use an algorithm very much like yours, with almost the same GCD structure (with the above caveats) and it's pretty smooth scrolling, even on older devices. If you want me to post code, I'm happy to.

If you're still having troubles, the CPU profiler is pretty great for identifying the bottlenecks and letting you know where you should focus your attention. There are some great WWDC sessions available online which focus on how to use Instruments to identify performance bottlenecks, and I found them to be very helpful to gain proficiency with Instruments.

Here is my code. In viewDidLoad, I initialize my image cache:

- (void)initializeCache
{
    self.imageCache = [[NSCache alloc] init];
    self.imageCache.name = @"Custom Image Cache";
    self.imageCache.countLimit = 50;
}

And then I use this in my tableView:cellForRowAtIndexPath:

- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
    static NSString *CellIdentifier = @"ilvcCell";
    UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:CellIdentifier];

    // set the various cell properties

    // now update the cell image

    NSString *imagename = [self imageFilename:indexPath]; // the name of the image being retrieved

    UIImage *image = [self.imageCache objectForKey:imagename];

    if (image)
    {
        // if we have an cachedImage sitting in memory already, then use it

        cell.imageView.image = image;
    }
    else
    {
        cell.imageView.image = [UIView imageNamed:@"blank_image.png"];

        // the get the image in the background

        dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{

            // get the UIImage

            UIImage *image = [self getImage:imagename];

            // if we found it, then update UI

            if (image)
            {
                dispatch_async(dispatch_get_main_queue(), ^{

                    // if the cell is visible, then set the image

                    UITableViewCell *cell = [self.tableView cellForRowAtIndexPath:indexPath];
                    if (cell)
                        cell.imageView.image = image;

                    [self.imageCache setObject:image forKey:imagename];
                });
            }
        });
    }

    return cell;
}

and

- (void)didReceiveMemoryWarning
{
    [super didReceiveMemoryWarning];

    [self.imageCache removeAllObjects];
}

As an aside, one further optimization that you might contemplate would be to preload your cached images in a separate queue, rather than loading images in a separate thread just-in-time. I don't think it's necessary, as this seems to be more than fast enough for me, but it's one more option to speed up the UI.

like image 171
Rob Avatar answered Oct 02 '22 15:10

Rob