Finding NSManagedObject inside of NSOperation - ios

I thought that [moc existingObjectWithID:self.objectID error:&error] was the right way to find an existing object inside of a thread, given its objectID, but I can't get it to work, why?
Background / Code
I've got an NSManagedObject which is already saved. I can see all its properties on the main thread. I'm passing the object to my NSOperation in a custom init method and in that method, I'm saving its objectID:
- (instancetype)initWithEntity:(NSManagedObject *)object
{
self = [super init];
if (self) {
self.objectID = object.objectID;
...
}
return self;
}
Then in the main function of my operation I'm looking up my object so that I can use it.
- (void)main
{
if (self.isCancelled) { return; }
NSManagedObjectContext *moc = [NSManagedObjectContext MR_context];
NSError *error = nil;
self.object = [moc existingObjectWithID:self.objectID error:&error];
if (error) {
DDLogError(#"error finding object: %#", error);
}
...
}
MR_context is a MagicalRecord method which simply sets up a new context with concurrency type NSPrivateQueueConcurrencyType with its parent set to the root saving context. All of that looks correct to me.
Errors
When no debugger is set, I simply get a fault for the object. If I try to look up any property on it, I get nil.
With the debugger turned on and -com.apple.CoreData.ConcurrencyDebug 1 set I get some errors:
In the simulator I get EXC_BAD_INSTRUCTION (code=EXC_i386_INVOP, subcode=0x0) on the [moc existingObjectWithID:self.objectID error:&error] line.
On the device, I get the following on that same line:
CoreData`+[NSManagedObjectContext __Multithreading_Violation_AllThatIsLeftToUsIsHonor__]:
-> 0x185946614 <+0>: brk #0x1
So obviously something is wrong, but the objectID isn't temporary, it is the same objectID I see when inspecting the real object on the main thread. So I'm not sure why it isn't working.
I've also tried:
self.object = [moc objectWithID:self.objectID];
But that doesn't work either. Any ideas?
Updates
2015-07-10 - Ok, if I use a block with the moc then I can properly access the object from within that block.
[moc performBlock:^{
NSError *error = nil;
NSManagedObject *o = [moc existingObjectWithID:self.objectID error:&error];
}];
How do I properly extract that object so I can have access to it in the thread that the operation is running? Doing a __block property and then setting it in the block and using it out of the block failed.
Is this an issue with the way the context is setup?

MagicalRecord's context sets up its own queue to perform operations on it. Therefore, if you want to use it to look something up, you have to do it in a block ([context performBlock:] or [context performBlockAndWait:]) so that it can perform it on its own queue and then return back to the NSOperation.
What I ended up doing was creating a couple __block properties and then pulling the needed contents from the object outside of the block.

Related

NSManagedObjects created on background turning into faults on main thread

I'm dealing with a strange case. I'm getting some data from a API, transform the response JSON into NSManagedObjects and saving them, all that in a background thread and using a NSPrivateQueueConcurrencyType context whose parent is a NSMainQueueConcurrencyType.
At the same time I'm creating the NSManagedObjects, I'm placing them inside an array to use it in my view controller via a completion block executed on the main therad. Here's a simplified version of what I'm doing.
- (void)getObjectFromAPIWithCompletion:((^)(NSArray *objects, NSError *error))completion
{
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0), ^{
NSManagedObjectContext *backgroundMoc = [[CoreDataStack sharedManager] newChildContextWithConcurrencyType:NSPrivateQueueConcurrencyType];
NSArray *items = [self processToParseResponseFromAPIAndSaveInCoreDataUsingContext:backgroundContext];
[backgroundMoc save:nil];
dispatch_async(dispatch_get_main_queue(), ^{
//Beyond this point, all properties of items are nil or 0 (if integer)
if(completion) completion(items, error);
});
});
}
The weird thing happens inside the completion block. I put a breakpoint on the first line of that block and print the items array on the console. Everything looks fine, with all the items (50 in this case) and their properties are OK.
The problem is when that after that line (where I'm not changing anything on the items array) all objects turn into faults and I'm not getting any data on the properties.
Don't know what is the cause of this rare behaviour. Any idea?
Thanks.
EDIT: Hal's answer put me on the right track so here's what I did to fix this problem. Just before calling the block that will be executed on the main thread I "move" the managed objects from the background context to the main context, fetching with their objectIDs. Like this:
NSArray *objects = //Objects created on background thread with Private queue
NSMutableArray *objectsIDs = [NSMutableArray array];
for (Object *object in object) {
[objectsIDs addObject:object.objectID];
}
//Save on private managed object context and on completion...
[self.managedObjectContext saveWithCompletion:^(NSError *error) {
NSManagedObjectContext *mainMOC = [[CoreDataManager sharedManager] mainContext];
NSMutableArray *fetchedObjects = [NSMutableArray array];
for (NSManagedObjectID *objectID in objectsIDs) {
[fetchedArticles addObject:[mainMOC objectWithID:objectID]];
}
if (completion) completion(fetchedObjects, pagination, nil);
}];
This way, all objects are not faults on the main thread.
If those objects (the contents of items) have been created on the background thread, you can't use them in the main thread. You can't pass NSManagedObject instances between threads.
You'll have to pass NSManagedObjectIDs (which won't be permanent until you've saved the background MOC), or use an NSFetchedResultsController (pointing to the main MOC) on the main thread. This is one of the cases that NSFetchedResultsController was designed for, so I recommend you go that route.

New thread + NSManagedObjectContext

I'm trying to separate my application work when there is a bigger work to do to optimize performance. My problem is about a NSManagedObjectContext used in another thread than the main one.
I'm calling:
[NSThread detachNewThreadSelector:#selector(test:) toTarget:self withObject:myObject];
On the test method there are some stuff to do and I have a problem here:
NSArray *fetchResults = [moc
executeFetchRequest:request
error:&error];
Here is my test method:
-(void) test:(MyObject *)myObject{
#autoreleasepool {
//Mycode
}
}
The second time I call the test method, my new thread is blocked when the executeFetchRequest is called.
This problem arrived when my test method is called more than one time in succession. I think the problem comes from the moc but I can't really understand why.
Edit:
With #Charlie's method it's almost working. Here is my code to save my NSManagedObjectContext (object created on my new thread).
- (void) saveContext:(NSManagedObjectContext *) moc{
NSError *error = nil;
if ([moc hasChanges] && ![moc save:&error]) {
NSLog(#"Unresolved error %#, %#", error, [error userInfo]);
}
}
This method is called on the new thread. My problem now is that with this save, I have a deadlock and I don't really understand why. Without it's perfectly working.
Edit2
I'm working on this issue but I still can't fix it. I changed my code about the detachNewThreadSelector. Here is my new code:
NSManagedObjectContext* context = [[NSManagedObjectContext alloc]
initWithConcurrencyType:NSPrivateQueueConcurrencyType];
context.persistentStoreCoordinator = self.persistentStoreCoordinator;
context.undoManager = nil;
[context performBlock:^
{
CCImages* cachedImage;
NSManagedObjectContext *childContext = [[NSManagedObjectContext alloc] initWithConcurrencyType:NSPrivateQueueConcurrencyType];
childContext.parentContext = context;
cachedImage=[CCImages getCCImageForKey:path inManagedObjectContext:childContext];
UIImage *image = [self getImageFromCacheWithPath:path andCachedImage:cachedImage atDate:now];
if (image != nil){
if(![weakSelf.delegate respondsToSelector:#selector(CacheCacheDidLoadImageFromCache:)])
[weakSelf setDelegate:appDelegate.callbacksCollector];
//[weakSelf useCallbackCollectorForDelegate:weakSelf inMethod:#"initPaginatorForListMoments"];
[weakSelf.delegate CacheCacheDidLoadImageFromCache:image];
}
}
- (UIImage*) getImageFromCacheWithPath:(NSString*) path andCachedImage:(CCImages *) cachedImage atDate: (NSDate *) now{
NSURL* localURL=[NSURL URLWithString:cachedImage.path relativeToURL:[self imageCacheDirectory]];
UIImage * image;
//restore uiimage from local file system
if (localURL) {
image=[UIImage imageWithContentsOfFile:[localURL path]];
//update cache
[cachedImage setLastAccessedAt:now];
[self saveContext];
if(image)
return image;
}
return nil;
}
Just after that, I'm saving my contexts (manually for now)
[childContext performBlock:^{
NSError *error = nil;
if (![childContext save:&error]) {
DDLogError(#"Error during context saving when getting image from cache : %#",[error description]);
}
else{
[context performBlock:^{
NSError *error = nil;
if (![context save:&error]) {
DDLogError(#"Error during context saving when getting image from cache : %#",[error description]);
}
}];
}
}];
There is a strange problem. My call back method is called without any problem on my controller (which implements the CacheCacheDidLoadImageFromCache: method). On this method I attest the reception of the image (DDLogInfo) and say that I want my spinner to stop. It does not directly but only 15secondes after the callback method was called.
My main problem is that my context (I guess) is still loading my image from the cache while it was already found. I said 'already' because the callback method has been called and the image was present. There is no suspicious activity of the CPU or of the memory. Instruments didn't find any leak.
I'm pretty sure that I'm using wrongly the NSManagedObjectContext but I can't find where.
You are using the old concurrency model of thread confinement, and violating it's rules (as described in the Core Data Concurrency Guide, which has not been updated yet for queue confinement). Specifically, you are trying to use an NSManagedObjectContext or NSManagedObject between multiple threads.
This is bad.
Thread confinement should not be used for new code, only to maintain the compatibility of old code while it's being migrated to queue confinement. This does not seem to apply to you.
To use queue confinement to solve your problem, first you should create a context attached to your persistent store coordinator. This will serve as the parent for all other contexts:
+ (NSManagedObjectContent *) parentContextWithPersistentStoreCoordinator:(NSPersistentStoreCoordinator *)coordinator {
NSManagedObjectContext *result = nil;
result = [[NSManagedObjectContext alloc] initWithConcurrencyType:NSPrivateQueueConcurrencyType];
[result setPersistentStoreCoordinator:coordinator];
return result;
}
Next, you want the ability to create child managed object contexts. You will use these to perform work on the data, wether reading or writing. An NSManagedObjectContext is a scratchpad of the work you are doing. You can think of it as a transaction. For example, if you're updating the store from a detail view controller you would create a new child context. Or if you were performing a multi-step import of a large data set, you would create a child for each step.
This will create a new child context from a parent:
+ (NSManagedObjectContext *) childContextWithParent:(NSManagedObjectContext *)parent {
NSManagedObjectContext *result = nil;
result = [[NSManagedObjectContext alloc] initWithConcurrencyType:NSPrivateQueueConcurrencyType];
[result setParent:parent];
return result;
}
Now you have a parent context, and you can create child contexts to perform work. To perform work on a context, you must wrap that work in performBlock: to execute it on the context's queue. I do not recommend using performBlockAndWait:. That is intended only for re-rentrant methods, and does not provide an autorelease pool or processing of user events (user events are what drives nearly all of Core Data, so they're important. performBlockAndWait: is an easy way to introduce bugs).
Instead of performBlockAndWait: for your example above, create a method that takes a block to process the results of your fetch. The fetch, and the block, will run from the context's queue - the threading is done for you by Core Data:
- (void) doThingWithFetchResults:(void (^)(NSArray *results, NSError *error))resultsHandler{
if (resultsHandler != nil){
[[self context] performBlock:^{
NSArray *fetchResults = [[self context] executeFetchRequest:request error:&error];
resultsHandler(fetchResults, error);
}];
}
}
Which you would call like this:
[self doThingsWithFetchResults:^(NSArray *something, NSError *error){
if ([something count] > 0){
// Do stuff with your array of managed objects
} else {
// Handle the error
}
}];
That said, always prefer using an NSFetchedResultsController over using executeFetch:. There seems to be a belief that NSFetchedResultsController is for powering table views or that it can only be used from the main thread or queue. This is not true. A fetched results controller can be used with a private queue context as shown above, it does not require a main queue context. The delegate callbacks the fetched results controller emits will come from whatever queue it's context is using, so UIKit calls need to be made on the main queue inside your delegate method implementations. The one issue with using a fetched results controller this way is that caching does not work due to a bug.
Again, always prefer the higher level NSFetchedResultsController to executeFetch:.
When you save a context using queue confinement you are only saving that context, and the save will push the changes in that context to it's parent. To save to the store you must recursively save all the way. This is easy to do. Save the current context, then call save on the parent as well. Doing this recursively will save all the way to the store - the context that has no parent context.
Example:
- (void) saveContextAllTheWayBaby:(NSManagedObjectContext *)context {
[context performBlock:^{
NSError *error = nil;
if (![context save:&error]){
// Handle the error appropriately.
} else {
[self saveContextAllTheWayBaby:[context parentContext]];
}
}];
}
You do not, and should not, use merge notifications and mergeChangesFromContextDidSaveNotification: with queue confinement. mergeChangesFromContextDidSaveNotification: is a mechanism for the thread confinement model that is replaced by the parent-child context model. Using it can cause a whole slew of problems.
Following the examples above you should be able to abandon thread confinement and all of the issues that come with it. The problems you are seeing with your current implementation are only the tip of the iceberg.
There are a number of Core Data sessions from the past several years of WWDC that may also be of help. The 2012 WWDC Session "Core Data Best Practices" should be of particular interest.
if you want to use managed object context in background thread, there are two approaches,
1 Create a new context set concurrency type to NSPrivateQueueConcurrencyType and set the parentContext to main thread context
2 Create a new context set concurrency type to NSPrivateQueueConcurrencyType and set persistentStoreCoordinator to main thread persistentStoreCoordinator
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^(void) {
NSManagedObjectContext *privateContext = [[NSManagedObjectContext alloc] initWithConcurrencyType:NSPrivateQueueConcurrencyType];
privateContext.persistentStoreCoordinator = mainManagedObjectContext.persistentStoreCoordinator;
[[NSNotificationCenter defaultCenter] addObserverForName:NSManagedObjectContextDidSaveNotification object:nil queue:nil usingBlock:^(NSNotification* note) {
NSManagedObjectContext *moc = mainManagedObjectContext;
if (note.object != moc) {
[moc mergeChangesFromContextDidSaveNotification:note];
}
}];
// do work here
// remember managed object is not thread save, so you need to reload the object in private context
});
before exist the thread, make sure remove the observer, bad thing can happen if you don't
for more details read http://www.objc.io/issue-2/common-background-practices.html

iOS background save in Core Data

I want to ensure that my main thread never blocks, that's why I want to do my Core Data saves in the background.
I've been reading the Apple docs together with this link (and many others, but I found this one pretty useful): http://www.cocoanetics.com/2012/07/multi-context-coredata/, though I cannot get the architecture right.
In my AppDelegate.m:
- (NSManagedObjectContext *)managedObjectContext
{
if (_managedObjectContext != nil) {
return _managedObjectContext;
}
NSPersistentStoreCoordinator *coordinator = [self persistentStoreCoordinator];
if (coordinator != nil) {
_saveContext = [[NSManagedObjectContext alloc] initWithConcurrencyType:NSPrivateQueueConcurrencyType];
[_saveContext setPersistentStoreCoordinator:coordinator];
_managedObjectContext = [[NSManagedObjectContext alloc] initWithConcurrencyType:NSMainQueueConcurrencyType];
[_managedObjectContext setParentContext:_saveContext];
}
return _managedObjectContext;
}
Then to save, I would do something like this:
// var 'context' is the context coming from method managedObjectContext
// this code is in the same thread 'context' is created in (the main thread)
NSError *error = nil;
if ([context save:&error]) {
[context.parentContext performBlock:^{
NSError *err = nil;
if(![context.parentContext save:&err]) {
NSLog(#"Error while saving context to the persistent store");
}
}];
} else {
// handle error
}
This is what I would get from reading the link I supplied earlier. Saving does not work, once the app is closed and reopened, the changes made to any managed objects are gone: they were never saved to the persisted store.
Makes sense I guess since I created 2 NSManagedObjectContexts in 1 thread, Apple docs clearly state to have only 1 NSManagedObjectContext per thread. So how do I setup the parent/child relation between _managedObjectContext and _saveContext? I know _saveContext needs to be initialised in another thread, but I cannot get this approach to work.
(From the comments)
All the "new" managed object context types (NSMainQueueConcurrencyType, NSPrivateQueueConcurrencyType) manage their own threads, it is not necessary to create the context on a special thread. The only thing to remember is always to use performBlock or performBlockAndWait for operations in the context. This ensures that the operations are executed on the right queue and thread.
So your code is OK.
(As it turned out, the error was that a wrong context was passed to your saving routine and therefore the inner save was not done in the top-level context.)

ios - Core data - app crash if deleting before save finished

My app simply add some users informations (name, birthdate, thumbnail, ...) with Core Data.
I noticed that if I delete a user right after created it, my app just stop working (not a crash, xCode returns no crash log, nothing).
I'm using asynchronous nested context for saving my users informations so I guess that behavior is due to the fact that my delete statement is executing before my save statement.
But since i'm a total beginner with Core Data, i don't really know how to handle that. I don't even know if i declared nested contexts the right way.
Here's my save codes :
NSManagedObjectContext *tmpContext = [[NSManagedObjectContext alloc] initWithConcurrencyType:NSPrivateQueueConcurrencyType];
tmpContext.parentContext = self.backgroundManagedObjectContext;
BSStudent *newStudent = (BSStudent *)[NSEntityDescription insertNewObjectForEntityForName:kBSStudent inManagedObjectContext:tmpContext];
newStudent.firstname = firstname;
newStudent.lastname = lastname;
newStudent.birthdate = birthdate;
newStudent.thumbnail = thumbnail;
newStudent.createdAt = [NSDate date];
[self dismissViewControllerAnimated:YES completion:nil];
[tmpContext performBlock:^{
[tmpContext save:nil];
[self.backgroundManagedObjectContext performBlock:^{
NSError *error;
if (![self.backgroundManagedObjectContext save:&error]) {
NSLog(#"%#", [error localizedDescription]);
}
[self.managedObjectContext performBlock:^{
NSError *error;
if (![self.managedObjectContext save:&error]) {
NSLog(#"%#", [error localizedDescription]);
}
}];
}];
}];
For precision, self.managedObjectContext is a NSPrivateQueueConcurrencyType and self.backgroundManagedObjectContext is a NSMainQueueConcurrencyType. And self.backgroundManagedObject is a child of self.managedObjectContext.
Here's my delete codes :
BSStudent *student = objc_getAssociatedObject(alertView, kDeleteStudentAlertAssociatedKey);
// on supprimer l'objet et on sauvegarde le contexte
[self.managedObjectContext deleteObject:student];
NSError *error;
if(![self.managedObjectContext save:&error]) {
NSLog(#"%#", [error localizedDescription]);
}
Can someone know how to handle this situation properly ?
Your delete is probably using the BSStudent created by a different context than you are deleting with. The following code will fix that.
NSManagedObjectContext * deleteContext = student.managedObjectContext;
[deleteContext deleteObject:student];
If you really want to use the other context, refetch the student using ObjectID
NSManagedObject * studentToDelete = [self.managedObjectContext objectWithID:student.objectID];
[self.managedObjectContext deleteObject:studentToDelete];
Nested contexts tips
Your contexts are probably okay, but I see a lot of people throwing around performBlock unnecessarily. With nested contexts, the QueueConcurrencyType refers to the thread it will do Core Data operations on, not the thread it was created on. So doing an operation like save on itself inside its performBlock is unnecessary and can lead to deadlocks.
When you save a child context, the parent is automatically synced with the changes. If you want to save upwards to the next higher parent automatically, I would recommend registering the parent for NSManagedObjectContextDidSaveNotification of the child saves. You can make this easier by having your AppDelegate have a factory method for creating the child contexts.
- (NSManagedObjectContext *)createChildContext
{
NSManagedObjectContext *tmpContext = [[NSManagedObjectContext alloc] initWithConcurrencyType:NSPrivateQueueConcurrencyType];
tmpContext.parentContext = self.managedObjectContext;
//Register for NSManagedObjectContextDidSaveNotification
return tmpContext;
}
if you wrap your delete in a performBlock call it can't execute at the same time as the saving performBlock.
e.g.:
BSStudent *student = objc_getAssociatedObject(alertView, kDeleteStudentAlertAssociatedKey);
// on supprimer l'objet et on sauvegarde le contexte
[self.managedObjectContext performBlock:^{
[self.managedObjectContext deleteObject:student];
NSError *error;
if(![self.managedObjectContext save:&error]) {
NSLog(#"%#", [error localizedDescription]);
}
}];
This is the "preferred" way of dealing with contexts as it serializes access to the context and keeps all those operations on the contexts thread,
I assume you are getting the crash because the objectID is becoming invalid or changing before the save completes, near the top of the call stack you'll see something about "hash64" or such

Saving Core Data using AlertView and immediately in Cell

When I push a cell, an AlertView with Prompt is popping up. My problem: I want to show the entered text from the prompt in to the selected cell. (and in the meantime save the text to Core Data). Can anyone push me in the right direction ?
You need to do the save in a background thread, if you want it to happen at the same time as the alert is showing.
The easiest approach is using nested contexts, and just saving from the main context.
Wherever you are creating your managed object context, replace the alloc/init part with...
NSManagedObjectContext *parentMoc = [[NSManagedObjectContext alloc] initWithConcurrencyType:NSPrivateQueueConcurrencyType];
parentMoc.persistentStoreCoordinator = persistentStoreCoordinator;
self.managedObjectContext = [[NSManagedObjectContext alloc] initWithConcurrencyType:NSMainQueueConcurrencyType];
self.managedObjectContext.parentContext = parenetMoc;
Now, you have the same MOC you were using before, except it is a main queue MOC, with a parent context running in a background queue.
You will have to use a method to save both contexts though. The second save, on the parent, happens in a background thread, so you do not have to wait.
- (void)saveData {
NSError *error = nil;
NSManagedObjectContext *moc = self.managedObjectContext;
if ([moc save:&error]) {
moc = moc.parentContext;
[moc performBlock:^{
NSError *error = nil;
if (![moc save:&error]) {
// Handle the actual save error
}
}];
} else {
// Handle the error of saving up into the parent context...
}
}
Now, instead of calling [managedObjectContext save:&error] directly, replace it with a message of saveData, and the method will return almost immediately, and the actual save will happen in a background thread.
None of your other code in your app (except for the save calls) should have to change at all.
In your case, right before you throw up the alert, call save, and the save will happen while the alert is being displayed.

Resources