Reputation: 5290
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
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
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
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?
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:
NSManagedObject
from that context (you can pass the objectID
around as it is thread safe.NSManagedObject
.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
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