I have an old project (started on iOS 7) with this simple code:
in AppDelegate I create the managedObjectContext:
- (NSManagedObjectContext *)managedObjectContext {
// Returns the managed object context for the application (which is already bound to the persistent store coordinator for the application.)
if (_managedObjectContext != nil) {
return _managedObjectContext;
}
NSPersistentStoreCoordinator *coordinator = [self persistentStoreCoordinator];
if (!coordinator) {
return nil;
}
_managedObjectContext = [[NSManagedObjectContext alloc] init];
[_managedObjectContext setPersistentStoreCoordinator:coordinator];
return _managedObjectContext;
}
Then, I perform an update operation in a view controller:
[context performBlock:^{
[context deleteObject:anObject];
NSError *error = nil;
if (![context save:&error])
{
NSLog(#"Error saving context");
}
}];
I'm sure that this code was working correctly on iOS 7.0, but crashes on the performBlock: call on iOS 8 with this error:
Can only use -performBlockAndWait: on an NSManagedObjectContext that was created with a queue.
I can solve the error changing the init method of the NSManagedObjectContext instance like this:
_managedObjectContext = [[NSManagedObjectContext alloc] initWithConcurrencyType:NSMainQueueConcurrencyType];
But I don't understand why this behavior changed. In the documentation of NSManagedObjectContext you can read:
A consequence of this is that a context assumes the default owner is the thread or queue that allocated it—this is determined by the thread that calls its init method.
So, in my first example, using a simple init call, the queue owner of the context is the main thread.
The performBlock: call was made on the main thread too, so I don't understand why this error. Am I missing something or it's a bug in iOS 8?
The call -[NSManagedObjectContext init] is just a wrapper for -[NSManagedObjectContext initWithConcurrencyType:] with the argument NSConfinementConcurrencyType. This creates an instance of NSManagedObjectContext that uses the obsolete thread confinement model - which does not uses a queue. Contexts created using init or initWithConcurrencyType: with the value NSConfinementConcurrencyType passed are not compatible with the queue methods performBlock: or performBlockAndWait:.
Create your context using -[NSManagedObjectContext initWithConcurrencyType:] and pass either NSPrivateQueueConcurrencyType or NSMainQueueConcurrencyType as appropriate. The context that is created as a result is compatible with performBlock: and performBlockAndWait: and will uses the queue confinement model that was introduced in iOS 5.
Related
So I am making a framework to hold all the code used to manipulate a core data database. What I have ended up having an issue with is this.
I have a method which returns a new item
- (NSManagedObject *)createItem;
once they have modified that item the would call
- (void)save;
This has to be able to be executed in multiple threads so the managed object I return from my method has to have an NSManagedObjectContext consistent inside a thread. My solution to solve this was to create a NSMutableDictionary to hold a references to NSManagedObjectContexts using
[NSThread hash]
as the key. This works great. The only problem is that I cannot get rid of the contexts once their threads have finished.
Does anyone have any idea on how I could detect that?
Here is the code for my managed object context method
// Return Managed Object Context for Framework
- (NSManagedObjectContext *)managedObjectContext
{
// Get Thread Hash Value
NSThread * currentThread = [NSThread currentThread];
NSNumber * hashValue = [NSNumber numberWithUnsignedInteger:[currentThread hash]];
// Get Context From Thread Hash
NSManagedObjectContext * context = [self.managedObjectContexts objectForKey:hashValue];
// Check Context Exists
if (!context)
{
// Create Managed Object Context With Persistent Store Coordinator In Main Thread
NSPersistentStoreCoordinator * coordinator = [self persistentStoreCoordinator];
if (coordinator != nil) {
context = [[NSManagedObjectContext alloc] init];
[context setPersistentStoreCoordinator:coordinator];
}
// Add Context To Available Contexts
[self.managedObjectContexts setObject:context forKey:hashValue];
}
// Return
return context;
}
Look at using the threadDictionary available on each NSThread. In this way you can easily get the MOC for a thread (or know if there isn't one) and the cleanup will be handled for you.
How do you create a NSManagedObjectContext that has a non nil parentContext that runs on a different queue/thread?
UIManagedDocument's managedObjectContext does have this, but I don't know how to replicate it without using UIManagedDocument.
This is the code I'm using, which results in a managedObjectContext whose parentContext property is nil.
-(NSManagedObjectContext *)context{
if (_context == nil){
_context = [[NSManagedObjectContext alloc] init];
_context.persistentStoreCoordinator = self.storeCoordinator;
}
return _context;
}
-(NSPersistentStoreCoordinator *) storeCoordinator{
if (_storeCoordinator == nil) {
_storeCoordinator = [[NSPersistentStoreCoordinator alloc] initWithManagedObjectModel:self.model];
NSError *err = nil;
if (![_storeCoordinator addPersistentStoreWithType:NSSQLiteStoreType
configuration:nil
URL:self.dbURL
options:nil
error:&err ]) {
NSLog(#"Error while adding a Store: %#", err);
return nil;
}
}
return _storeCoordinator;
}
-(NSManagedObjectModel *) model{
if (_model == nil) {
_model = [[NSManagedObjectModel alloc] initWithContentsOfURL:self.modelURL];
}
return _model;
}
You create the child context by just allocating it and setting its parent:
NSManagedObjectContext *childContext = [[NSManagedObjectContext alloc] init];
[childContext setParentContext:[self managedObjectContext]];
The persistent store coordinator is inherited from the parent context, so this is all you need to create the child. But the parent context must use one of the queue-based concurrency types. That means that your code above creating the context would have to change to something like:
_context = [[NSManagedObjectContext alloc] initWithConcurrencyType: NSMainQueueConcurrencyType];
There's also NSPrivateQueueConcurrencyType. Which is better depends on your app's design.
The runs on a different queue/thread thing is a different matter though. This is never automatic. Your code runs on whatever queue or thread you call it from. Core Data's direct support is limited to using one of the queue-based concurrency types-- but then you need to make sure to use performBlock: or performBlockAndWait: to ensure that the context's operations actually happen on the right queue.
After creating the NSManagedObjectContext you have to assign the parent context if you need it to be a child context.
[newContext setParentContext:mainThreadManagedObjectContext];
In this case you do not even have to assign the persistent store. From the docs:
Rather than specifying a persistent store coordinator for a managed object context, you can now specify a parent managed object context using setParentContext:. This means that fetch and save operations are mediated by the parent context instead of a coordinator.
I'm trying to create a new instance of NSManagedObjectContext so that I can perform a fetch request in a thread other than the main one. As I understand it each thread needs it's own instance although they can share stores.
My app is a core data document based app.
Having read a bit here I've got this code:
NSManagedObjectContext *managedObjectContextForThread = nil;
NSPersistentStoreCoordinator *coordinator = [[NSPersistentStoreCoordinator alloc] initWithManagedObjectModel:self.managedObjectModel];
if (coordinator != nil) {
managedObjectContextForThread = [[NSManagedObjectContext alloc] init];
[managedObjectContextForThread setPersistentStoreCoordinator:coordinator];
[managedObjectContextForThread setUndoManager:nil];
}
It runs but when I perform the fetch I get no results, I suspect because the NSPersistentStoreCoordinator isn't getting setup correctly.
How should I be setting that store coordinator to work with my main store? Or is there something else I'm missing here?
Apple's 'typically recommended approach' is to share one persistent store coordinator among contexts. Ideally you would already have a reference to your app's main managed object context, and use that context's persistent store coordinator.
NSManagedObjectContext *managedObjectContextForThread = [[NSManagedObjectContext alloc] init];;
[managedObjectContextForThread setPersistentStoreCoordinator:myMainContext.persistentStoreCoordinator];
Take a look at "Concurrency With Core Data" from Apple's Core Data Programming Guide
You have to add the persistent store to the store coordinator, then add the persistent store the managed object context.
if ( [coordinator addPersistentStoreWithType:NSSQLiteStoreType configuration:0 URL:storeUrl options:options error:&error] ) {
managedObjectContextForThread = [[NSManagedObjectContext alloc] init];
[managedObjectContextForThread setPersistentStoreCoordinator:coordinator];
}
else {
// investigate 'error'
}
I have a NSOperation subclass,this is the main method:
(void)main
{
NSAutoreleasePool *Pool = [[NSAutoreleasePool alloc] init];
managedObjectContext = [NSManagedObjectContext contextThatNotifiesDefaultContextOnMainThread];
Message *message = (Message *) [managedObjectContext objectWithID:self.messageID];
message.status = [NSNumber numberWithInt:SKMessageSendStateStart];
[message save];
[self send];
[self finish];
[Pool drain];
}
I define the fetchResultController and defaultContext like this:
(BOOL)application:(UIApplication *)applicationdidFinishLaunchingWithOptions:(NSDictionary *)launchOptions
{
[MagicalRecordHelpers setupCoreDataStackWithStoreNamed:#"Shark"];
self.context = [NSManagedObjectContext context];
[NSManagedObjectContext setDefaultContext:self.context];
self.fetchController = [Message fetchRequestAllGroupedBy:nil withPredicate:nil sortedBy:#"text" ascending:YES];
[self.fetchController setDelegate:self];
[self.fetchController performFetch:nil];
}
Everytime i call [message save],the console logout:
-NSManagedObjectContext(MagicalRecord) mergeChangesFromNotification: Merging changes to * DEFAULT
context on Main Thread *
But the NSFetchedResultsControllerDelegate never get called!
Does this mean that I set the FetchedResultsController wrong or what? I
am totally confused.
Thanks in advance.
The reason this isn't working is because MagicalRecord will automatically call performFetch: for you, thus not allowing you to set the delegate ahead of time.
Also, in your applicationDidFinishLaunching: method, you want to delete these lines:
self.context = [NSManagedObjectContext context];
[NSManagedObjectContext setDefaultContext:self.context];
You want to NOT change the default context in this case. MagicalRecord is handling stuff for you when you call setupCoreDataStackWithStoreNamed:...that is, a MOC is already available for use when that method completes, there is no need to toss the one it created for you and set the default context to a new instance in this particular case.
It's also not necessary to hold on to the context if all you're doing is going to use it to pass to one of the fetch methods provided by MagicalRecord. MagicalRecord will create a single context for its use (the 'default' context) and just use that...
I misunderstood what the [NSManagedObjectContext context] means. It create a new context in the main thread. Since the context that the fetchResultController monitor is not the same context that the change merged to, the NSFetchedResultsControllerDelegate will not get called.
Change [NSManagedObjectContext context] to [NSManagedObjectContext defaultContext] solved the problem.
I'm contemplating how to offload the drawing of a very large Core Data tree structure to CATiledLayer. CATiledLayer seems to be awesome because it performs drawing on a background thread and then fades in tiles whenever they're drawn. However, because the information of the drawing comes from a Core Data context that is by design not thread safe, I'm running into race condition issues where the drawing code needs to access the CD context.
Normally, if I need to perform background tasks with Core Data, I create a new context in the background thread and reuse the existing model and persistent store coordinator, to prevent threading issues. But the CATiledLayer does all the threading internally, so I don't know when to create the context, and there needs to be some kind of context sharing, or I can't pass the right entities to the CATiledLayer to begin with.
Is there anyone with a suggestion how I can deal with this scenario?
Cheers,
Eric-Paul.
The easiest solution is to use the dispatch API to lock all of your data access onto a single thread, while still allowing the actual drawing to be multi-threaded.
If your existing managed object context can only be accessed on the main thread, then this is what you do:
- (void)drawInContext:(CGContextRef)context // I'm using a CATiledLayer subclass. You might be using a layer delegate instead
{
// fetch data from main thread
__block NSString *foo;
__block NSString *bar;
dispatch_sync(dispatch_get_main_queue(), ^{
NSManagedObject *record = self.managedObjecToDraw;
foo = record.foo;
bar = record.bar;
});
// do drawing here
}
This is a quick and easy solution, but it will lock your main thread while fetching the data, which is almost certainly going to create "hitches" whenever a new tile is loaded while scrolling around. To solve this, you need to perform all of your data access on a "serial" dispatch queue.
The queue needs to have it's own managed object context, and you need to keep this context in sync with the context on your main thread, which is (presumably) being updated by user actions. The easiest way to do this is to observe a notification that the context has changed, and throw out the one used for drawing.
Define an instance variable for the queue:
#interface MyClass
{
NSManagedObjectContext *layerDataAccessContext;
dispatch_queue_t layerDataAccessQueue;
}
#end
Create it in your init method:
- (id)init
{
layerDataAccessQueue = dispatch_queue_create("layer data access queue", DISPATCH_QUEUE_SERIAL);
[[NSNotificationCenter defaultCenter] addObserver:self selector:#selector(contextDidChange:) name:NSManagedObjectContextDidSaveNotification object:nil]; // you might want to create your own notification here, which is only sent when data that's actually being drawn has changed
}
- (void)contextDidChange:(NSNotification *)notif
{
dispatch_sync(layerDataAccessQueue, ^{
[layerDataAccessContext release];
layerDataAccessContext = nil;
});
[self.layer setNeedsDisplay];
}
And access the context while drawing:
- (void)drawInContext:(CGContextRef)context
{
// fetch data from main thread
__block NSString *foo;
__block NSString *bar;
dispatch_sync(layerDataAccessQueue, ^{
NSManagedObject record = self.managedObjectToDraw;
foo = record.foo;
bar = record.bar;
});
// do drawing here
}
- (NSManagedObject *)managedObjectToDraw
{
if (!layerDataAccessContext) {
__block NSPersistentStoreCoordinator *coordinator;
dispatch_sync(dispatch_get_main_queue(), ^{
coordinator = [self persistentStoreCoordinator];
});
layerDataAccessContext = [[NSManagedObjectContext alloc] init];
[layerDataAccessContext setPersistentStoreCoordinator:coordinator];
}
NSFetchRequest *request = [[[NSFetchRequest alloc] init] autorelease];
NSEntityDescription *entity =
[NSEntityDescription entityForName:#"Employee"
inManagedObjectContext:layerDataAccessContext];
[request setEntity:entity];
NSPredicate *predicate =
[NSPredicate predicateWithFormat:#"self == %#", targetObject];
[request setPredicate:predicate];
NSError *error = nil;
NSArray *array = [layerDataAccessContext executeFetchRequest:request error:&error];
NSManagedObject *record;
if (array == nil || array.count == 0) {
// Deal with error.
}
return [array objectAtIndex:0];
}
I've given up trying to share managed object context instances between CATiledLayer draws and now just alloc/init a new context at every call of drawLayer:inContext: The performance hit is not noticable, for the drawing is already asynchronous.
If there's anyone out there with a better solution, please share!