Skoota
Skoota

Reputation: 5290

Crash when loading Core Data images into UITableViewCell on background thread

I have a series of profile pictures stored in Core Data as Binary Data (with the Allows External Storage option enabled, so don't jump on me for storing images in Core Data :)

Each image is being displayed in a UITableViewCell. At the moment there is a slight delay when the user taps to display the table view, presumably because it is loading them from Core Data which has a sufficient enough performance implication to lock-up the UI when loading them on the main thread.

I would like to put the image loading onto a separate background thread, so that the table view appears immediately and the images show when they have been loaded from Core Data.

I have tried the solution in this post with the following code in the -cellForRowAtIndexPath: method:

    // Create the cell
    MyCell *cell = [tableView dequeueReusableCellWithReuseIdentifier:@"Cell" forIndexPath:indexPath];

    dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_BACKGROUND, 0), ^(void) {

        // Get a reference to the object for this cell (an array obtained from Core Data previously in the code)
        MyObject *theObject = [self.objectArray objectAtIndex:indexPath.item];

        // Load the photo from Core Data relationship (ProfilePhoto entity)
        UIImage *profilePhoto = [UIImage imageWithData:theObject.profilePhoto.photo];

        // Set the name
        cell.nameLabel.text = theObject.name;

        dispatch_sync(dispatch_get_main_queue(), ^(void) {

            // Set the photo
            cell.photoImageView.image = profilePhoto;

        });

    });

    // Return the cell
    return cell;

However, the app crashes with the following error (repeated for each row in the table view):

CoreData: error: exception during fetchRowForObjectID: statement is still active with userInfo of (null)

Any help on understanding and resolving this issue would be appreciated.

Upvotes: 2

Views: 1909

Answers (4)

Skoota
Skoota

Reputation: 5290

The solution which Marcus Zarra provided gave me an idea about how to approach solving this problem, but I thought it would be worthwhile posting my implemented code for anyone else who comes to this question looking for a solution. So, here is my new code for the -cellForRowAtIndexPath: method:

// Create the cell
MyCell *cell = [tableView dequeueReusableCellWithReuseIdentifier:@"Cell" forIndexPath:indexPath];

// Get a reference to the object for this cell
MyObject *theObject = [self.objectArray objectAtIndex:indexPath.item];

// Reset the image
cell.imageView.image = nil;

// Set the name
cell.nameLabel.text = theObject.name;

// Check if the object has a photo
if (theObject.profilePhoto != nil) {

    // Create a new managed object context for the background thread
    NSManagedObjectContext *backgroundContext = [[NSManagedObjectContext alloc] initWithConcurrencyType:NSPrivateQueueConcurrencyType];
    backgroundContext.parentContext = self.managedObjectContext;

    // Perform the operations on the new context
    [backgroundContext performBlockAndWait:^{

        // Fetch the object
        NSError *error;
        ProfilePhoto *profilePhotoObject = (ProfilePhoto *)[backgroundContext existingObjectWithID:theObject.profilePhoto.objectID error:&error];

        // Create the image
        UIImage *thePhoto = [UIImage imageWithData:myObject.profilePhoto];

        dispatch_async(dispatch_get_main_queue(), ^(void) {

            // Set the photo
            cell.imageView.image = thePhoto;

        });

    }];

}

return cell;

You also need to change the line in the App Delegate file which creates the managed object context from:

_managedObjectContext = [[NSManagedObjectContext alloc] init];

to

_managedObjectContext = [[NSManagedObjectContext alloc] initWithConcurrencyType:NSMainQueueConcurrencyType];

otherwise the .parentContext assignment will not work.

Hope this helps anyone else encountering this issue!

Upvotes: 3

codercat
codercat

Reputation: 23271

dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{


        dispatch_async(dispatch_get_main_queue(), ^{
            self.imageView.image = image;
         });

});

The dispatch_get_global_queue gets you a background queue upon which you can dispatch background tasks that are run asynchronously (main idea is won't block your user interface).

But you are not allowed to perform user-interface process in the background, so the dispatch_async to the dispatch_get_main_queue lets that background queue dispatch the user interface updates back to the main queue.

Upvotes: 0

Marcus S. Zarra
Marcus S. Zarra

Reputation: 46718

You are violating the threading rules of Core Data. A NSManagedObjectContext and all of its associated objects can only be accessed on the thread that created it (or the thread is is assigned to).

It appears you are accessing instances of NSManagedObject inside of that block which is on another thread and therefore violating the thread containment rules.

What are you trying to accomplish by moving these things onto a background queue?

Update

First, are you certain that the UIImage creation is the slow part? Have you profiled this code? How big are these images? Are they too big and the cost is in them being resized? Where is the real cost?

Second, the creation of the UIImage can be done on another thread but you cannot access a context from multiple threads. That is a hard line that you cannot cross with any degree of stability.

Third, I do not recommend storing images in Core Data. Storing binary data in a SQLite file is not recommended. I will assume that you are using the external record storage option.

The way around this, assuming that the loading and the creation of the UIImage is the actual problem would be to:

  1. Create a context on the background queue (or create a private queue context)
  2. Re-load the NSManagedObject from that context (you can pass the objectID around as it is thread safe.
  3. Retrieve the image from this second instance of the NSManagedObject.
  4. Continue with your code.

If this sounds like a lot of work you would be right! Hence the suggestion of profiling this code first and make sure what is actually slow and try and find out why it is slow. Simply loading and initializing a UIImage should not be that slow.

Upvotes: 2

Tom Harrington
Tom Harrington

Reputation: 70936

You're using the same managed object context on multiple queues, and as a result your app crashes.

When you call dispatch_get_global_queue you get a concurrent queue. When you queue up a bunch of blocks on that queue, they may run in parallel. In the block, you use Core Data objects that all come from the same managed object context-- which is not thread safe. This is pretty much a recipe for a crash.

What you should do instead is make use of NSManagedObjectContext queue confinement. Create the context using either NSPrivateQueueConcurrencyType or NSMainQueueConcurrencyType (private is better here since you're trying to get work off of the main queue). Then use performBlockAndWait to work with the Core Data objects. This would be something like

[self.context performBlockAndWait:^{
    MyObject *theObject = [self.objectArray objectAtIndex:indexPath.item];
    ... etcetera ...
    dispatch_async(dispatch_get_main_queue(), ^(void) {

        // Set the photo
        cell.photoImageView.image = profilePhoto;

    });

}];

The blocks will execute sequentially, so there's no threading issue. They'll run on the context's private queue, which won't be the main queue. Note that I changed the UI update block to use dispatch_async above-- there's no reason to block the managed object context queue here by doing a synchronous call.

Upvotes: 1

Related Questions