2

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.

Community
  • 1
  • 1
Skoota
  • 5,280
  • 9
  • 52
  • 75

4 Answers4

3

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!

Community
  • 1
  • 1
Skoota
  • 5,280
  • 9
  • 52
  • 75
2

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.

Marcus S. Zarra
  • 46,571
  • 9
  • 101
  • 182
  • Thanks Marcus. I want to spin the loading and rendering of the profile pictures onto a background thread, so that the UI doesn't lock-up initially displaying the table view, and so that the table view does not have jerky scroll due to the images being loaded on-demand when the user is scrolling. I would like the images to just appear once they are ready for display. – Skoota Jan 24 '14 at 02:12
  • ...and I would welcome suggestions about any better ways to handle this desired behaviour too! – Skoota Jan 24 '14 at 02:20
  • Thanks Marcus. It was the images causing the slow loading, and I have now fixed the issue following your instructions. I'll post my code as a separate answer for anyone else who finds this question and would like to see the solution (but I will mark your answer as correct). – Skoota Jan 24 '14 at 07:57
1

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.

Tom Harrington
  • 69,312
  • 10
  • 146
  • 170
  • Thanks Tom. I am still a bit unclear on how to use `NSPrivateQueueConcurrencyType` or `NSMainQueueConcurrencyType` as I am fairly new to using Core Data on multiple threads. Could you perhaps provide a generic code example of their usage in this situation? – Skoota Jan 24 '14 at 02:13
0
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.

codercat
  • 22,873
  • 9
  • 61
  • 85