6

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;
        });
    });
}
Vikings
  • 2,527
  • 32
  • 45

4 Answers4

12

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.

Community
  • 1
  • 1
Rob
  • 415,655
  • 72
  • 787
  • 1,044
  • I would love to see the code, maybe I can see what i'm doing wrong – Vikings Jul 16 '12 at 12:07
  • @Vikings I've updated with my code. I've included everything but the database routines that I use to retrieve the data from my sqlite database (as I don't think that's relevant to the immediate discussion and I don't want to make this more confusing than necessary). – Rob Jul 16 '12 at 14:12
  • I am using a NSFetchedResultsController to fetch the results, i'm not sure if that makes a difference, but i'm getting memory warnings, and the table crashes. Maybe I should just fetch the results, at startup instead. You dont' have problems with large amount of data? – Vikings Jul 16 '12 at 14:40
  • @Vikings No, with 100+ thumbnail images I do not have memory problems, though if I ever did, `didReceiveMemoryWarning` would handle it. I'm sure if I had hundreds/thousands of thumbnail-sized, though, I'd eventually have a problem, but in that case I'd probably add some logic to limit the cache to some reasonable number of the most recently cached thumbnail images. Regarding the performance issue, I would have thought that caching the images in Core Data would have imposed a non-trivial performance hit, though I don't know if `NSFetchedResultsController` will do caching in memory of its own. – Rob Jul 16 '12 at 14:52
  • The fact that you're getting memory warnings is both surprising (because I don't see what would cause it in your code) and worrying. I thought we were solving just a performance problem, but it seems that you must have something else going on. I assume you've already looked for leaks, run it through the static analyzer, etc.? And are your images nice, small thumbnails? – Rob Jul 16 '12 at 14:58
  • thank a lot! You right, my code to size my images was not working correctly causing it to load the full size image, which resulted in bad performance, and memory warnings. – Vikings Jul 16 '12 at 15:26
  • @Vikings FYI, on http://stackoverflow.com/questions/11511548/best-way-to-cache-images-on-ios-app/11511798#11511798 I was introduced to `NSCache`. It's a great little class and have updated my code sample above accordingly. I'm glad you got me on this little jag, as I'm much happier with my image caching! – Rob Jul 16 '12 at 20:57
1

The missing step is to update the model with the fetched image. As it is, you're doing a new load for every cell every time. The model is the right place to cache the result of the relatively expensive load. Can you add a Beer.image property?

Then, your config code would look like this:

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];
}

Moved the loader logic here for clarity ...

- (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;
        });
    });
}
danh
  • 62,181
  • 10
  • 95
  • 136
  • 1
    Btw, you needn't use the core data model as the place to cache the image, but I think it's a good idea. That way, other parts of the app will have access to it. But if that's impractical for some reason, you can add a mutable dictionary to your view controller. The keys can be image paths and the values can be images. – danh Jul 14 '12 at 21:07
  • This Beer.image property, what is that a BOOL or an ImageView? – Vikings Jul 14 '12 at 22:19
  • he's just checking whether beer.image exists or not. By doing if (beer.image) you're essentially checking if (beer.image != nil) – shabbirv Jul 14 '12 at 22:44
  • Yeah. Will edit that to be more clear. What I mean for you to save the actual UIImage. You're actually saving the image that gets created, its the allocation and init of that image from disk where the speed cost is. – danh Jul 14 '12 at 22:44
  • @danh I added a property to my core data model for the UIImage, I updated my code with a edit. I'm a little confuse on how this works if I re-use cells – Vikings Jul 14 '12 at 22:50
  • You do reuse cells, and that's a little awkward with how you've got it written. The asynch block retains that cell even though it might have scrolled away by the time the image loads. The better way is to retain the indexPath, then reload ask for cell again. I'll edit my answer to illustrate. – danh Jul 14 '12 at 23:12
  • That edit was done without the benefit of a compiler. I hope it works. – danh Jul 14 '12 at 23:20
  • @danh this does make the table faster, but the cells get mixed up, pictures don't match up, its just not working correctly – Vikings Jul 15 '12 at 15:06
  • My sense is that you're much closer than it might appear. The table's cell reuse will make things appear scrambled. Here's a thought: you need a placeholder for images while the correct one loads. Will edit the answer in the else block of the configure cell method... – danh Jul 15 '12 at 17:29
1

Not much you can do here for the initial load, you're about as fast as it gets. If it's still too slow, try loading smaller images if you can.

A couple of things:

First, be careful with -imageWithContentsOfFile, it won't cache anything. You're taking the full hit every time you load the image, as opposed to -imageNamed that'll keep the image warm in some cache. You can of course cache that in your domain object, but I'd personally strongly advice against that. Your memory footprint is going to go through the roof, forcing you to implement your own cache expiration mechanism, while Apple has a very good image cache through -imageNamed. I'd be surprised if you can do a better job than apple on all 3 family of devices :)

Then, you're breaking the flyweight pattern of the UITableView here:

dispatch_sync(dispatch_get_main_queue(), ^{
            cell.beerImage.image = image;
            beer.image = image;
            [cell setNeedsLayout];
        });

Ask the table view to give your the cell at a given index rather than capture the cell in the block: by the time the image is loaded, that cell instance might actually have been reused for another index path, and you'll be displaying the image in the wrong cell.

And no need for -setNeedsLayout here, just changing the image is enough.

Edit: whoops! I missed the obvious thing with images in table view. What size are your images, what size is the image view, and what is the content mode on the image? If your images are of a very different size than the image view and you're asking the imageview to resize, this will happen on the main thread and you'll take a massive performance hit there. Resize the image to the image view off thread, after loading (a quick google search will give you the core graphics code to do that).

kra
  • 214
  • 2
  • 9
  • +1 for your note about `imageNamed` vs `imageWithContentsOfFile`. Quite right. When I suggested the custom caching, I was going from other comments that Vikings made on another discussion about storing images in Core Data versus a local file. But if you're using images stored in the Documents folder or the bundle, then `imageNamed` is best, eliminating the need for custom caching. -1 for just using the cell variable in your `dispatch_sync` without (a) re-retrieving it with `cellForRowAtIndexPath` and then (b) checking to see if it's not null. – Rob Jul 16 '12 at 15:18
  • On http://stackoverflow.com/questions/11511548/best-way-to-cache-images-on-ios-app/11511798#11511798, I was introduced to `NSCache` which offers the clever caching similar to `imageNamed`, but you can be intelligent about inquiring as to whether something is cached or not. So, if you use `NSCache`, you don't need to (and shouldn't) use `imageNamed`, but you enjoy the benefit of a well engineered cache! – Rob Jul 16 '12 at 20:54
  • @RobertRyan yeah, reusing the captured cell was precisely my point ;-) I was telling him not to do that. I wouldn't even bother to check for nil, it's safe anyways... – kra Jul 17 '12 at 03:01
  • And yeah, NSCache is good to use, but you need that in a central location, typically an ImageService that will be your entry point to any asset, may it be local or remote. Since he's reading images from disk directly in his controller, that sounds like a little bit overkill here. – kra Jul 17 '12 at 03:03
  • If you're doing asynchronous UI updates, you need UITableView's `cellForRowAtIndexPath` (not to be confused with `UITableViewDataSource` protocol's `tableView:cellForRowAtIndexPath`, which is entirely different) in case the row in question has scrolled off the screen and the cell has been reused for another row. If the row has scrolled off the screen, UITableView's `cellForRowAtIndexPath` absolutely can be `nil`. Maybe I'm not understanding your point. But I agree that if you're doing imageWithContentsOfFile, it's probably overkill (unless you're doing some image processing). – Rob Jul 17 '12 at 03:13
  • I was precisely telling him NOT to use the code I quoted, which came from his original post. In other words, we're saying the exact same thing: don't capture the cell in the block, since by the time the block will run, the cell will very likely have been reused for another index path/ – kra Jul 17 '12 at 17:36
  • Thank you for the enlightenment! Both the answer and the comments here have been incredibly helpful. I am lazy loading too and I had no idea capturing the cell in a block was a bad idea. – Andy Ibanez Oct 23 '13 at 19:14
-1

You can have a look on this question ,previously answered at stack overflow.

UIImage in uitableViewcell slowdowns scrolling table

or else try this code

- (void)configureCell:(BeerCell *)cell 
          atIndexPath:(NSIndexPath *)indexPath 
{
    Beer *beer = (Beer *) [self.fetchedResultsController objectAtIndexPath:indexPath];
    cell.displayBeerName.text = beer.name;

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

            cell.beerImage.image = image;
            [cell setNeedsLayout];
        }
Community
  • 1
  • 1