Loading image from CoreData at cellForRowAtIndexPath slows down scrolling

I am working on a UITableView that is very much like the iOS's native Photo app: it has many rows with 4 image thumbnails in each row. (i.e. each UITableViewCell has 4 UIImageViews) All thumbnails loaded from Core Data.

I have revised my implementation multiple times and I can see performance improvements, but it is still unable to scroll as smooth as the Photo app.

I need advise on how to properly cache the photos for best performance. This is what I tried:

1. My first attempt (extremely lag when scrolling)

  • Images are stored with type Transformable in CoreData.
  • In cellForRow function, each image is fetched from CoreData on the fly.

2. Second attempt (faster, but still lag a bit when scrolling)

  • Images are stored with type Binary Data with 'external storage' option ticked in CoreData.
  • In cellForRow function, each image is first loaded from Core Data and then stored into NSCache in memory, so next time cellForRow fires, we will use the UIImage from NSCache directly if available.

After using NSCache to cache images loaded from CoreData, scrolling is visibly faster but since images still have to be loaded from CoreData when it is not yet available in NSCache, scrolling will still be jerky from times to times.

So, there must be a better way, I could preload all the images into memory but since there might be large number or rows of images so I didnt plan to preload the images at all.

What else can I do to load the image faster in cellForRowAtIndexPath?


Solution 1:

To keep your scrolling smooth regardless of where your data comes from, you need to fetch your data on a separate thread and only update the UI when you have the data in memory. Grand Central Despatch is the way to go. Here's a skeleton which assume you have a self.photos dictionary with a text reference to an image file. The image thumbnail may or may not be loaded into a live dictionary; may or may not be in a filesystem cache; otherwise is fetched from an online store. It could use Core Data, but the key to smooth scrolling is that you don't wait around for the data wherever it comes from.

- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
    {
    static NSString *CellIdentifier = @"Photo Cell";
    UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:CellIdentifier];
    //identify the data you are after
    id photo = [self.photos objectAtIndex:indexPath.row];
        // Configure the cell based on photo id

      dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
        //move to an asynchronous thread to fetch your image data
            UIImage* thumbnail = //get thumbnail from photo id dictionary (fastest)
            if (!thumbnail) {    //if it's not in the dictionary
                thumbnail =      //get it from the cache  (slower)
                                 // update the dictionary
                if (!thumbnail) {   //if it's not in the cache
                  thumbnail =       //fetch it from the (online?) database (slowest)
                                    // update cache and dictionary
                    }
                }
            }
            if (thumbnail) {  
                dispatch_async(dispatch_get_main_queue(), ^{
                //return to the main thread to update the UI
                    if ([[tableView indexPathsForVisibleRows] containsObject:indexPath]) {
                    //check that the relevant data is still required
                        UITableViewCell * correctCell = [self.tableView cellForRowAtIndexPath:indexPath];
                        //get the correct cell (it might have changed)
                        [[correctCell imageView] setImage:thumbnail];
                        [correctCell setNeedsLayout];
                    }
                });
            }
        });
    return cell;
    }

If you are using some kind of singleton image store manager, you would expect the manager to deal with the details of cache / database access, which simplifies this example.

This part

            UIImage* thumbnail = //get thumbnail from photo id dictionary (fastest)
            if (!thumbnail) {    //if it's not in the dictionary
                thumbnail =      //get it from the cache  (slower)
                                 // update the dictionary
                if (!thumbnail) {   //if it's not in the cache
                  thumbnail =       //fetch it from the (online?) database (slowest)
                                    // update cache and dictionary
                    }
                }

would be replaced with something like

      UIImage* thumbnail = [[ImageManager singleton] getImage];

(you wouldn't use a completion block as you are effectively providing one in GCD when you return to the main queue)