I am working with concurrency in coreData using threads, I followed sample example by apple, the link is here https://developer.apple.com/library/ios/samplecode/ThreadedCoreData/Introduction/Intro.html
I have few doubts regarding this sample project
I am using a class call PullOperation which is subClass of NSOperation, so when pull happens,
I want to notify my main MOC, about the changes so that It will be updated and shown on tableView
I want to know how to do this?
By following the sample app, I wrote this code in my appdelegate.m
- (NSManagedObjectContext *)managedObjectContext {
if (_managedObjectContext != nil) {
return _managedObjectContext;
}
NSPersistentStoreCoordinator *coordinator = [self persistentStoreCoordinator];
if (coordinator != nil) {
_managedObjectContext = [NSManagedObjectContext new];
[_managedObjectContext setPersistentStoreCoordinator:coordinator];
}
// observe the ParseOperation's save operation with its managed object context
[[NSNotificationCenter defaultCenter] addObserver:self
selector:#selector(mergeChanges:)
name:NSManagedObjectContextDidSaveNotification
object:nil];
return _managedObjectContext;
}
// merge changes to main context,fetchedRequestController will automatically monitor the changes and update tableview.
- (void)updateMainContext:(NSNotification *)notification {
assert([NSThread isMainThread]);
[self.managedObjectContext mergeChangesFromContextDidSaveNotification:notification];
}
// this is called via observing "NSManagedObjectContextDidSaveNotification" from our APLParseOperation
- (void)mergeChanges:(NSNotification *)notification {
if (notification.object != self.managedObjectContext) {
[self performSelectorOnMainThread:#selector(updateMainContext:) withObject:notification waitUntilDone:NO];
}
}
Here we can see there are notifications to merger changes.
But my app gets stuck and doesnot respond and I get message saying app stopped due to memory issues.
So I want to know where I am going wrong.
Please help
Regards
Ranjit
If your app halts when there is a managed object context change, I guess that is because both of your managed object context observe each other(by listening to NSManagedObjectContextDidSaveNotification), thus when there is a change, it will form an endless recursive call.
But I don't have enough code, so I'm just guessing, I suggest that you put a break point at this line:
[self performSelectorOnMainThread:#selector(updateMainContext:) withObject:notification waitUntilDone:NO];
and trigger a change to see if this line is entered many many times. If it is, then my guess is right.
EDIT:
By chatting, I got more information and the problem is because PO is using GAI(Google Analytics SDK for iOS), GAI uses core data for data persistent and GAI has its own core data stack, when GAI saves its context, it will post NSManagedObjectContextDidSaveNotification, and this notification goes globally, triggering -mergeChanges:, in -mergeChanges:, app will try to merge GAI's context with app's context, this two context is using different persistent store coordinator, which causes the problem.
To solve this, we need to check if the source context is using a same coordinator with the destination context, if not, no merging.
Related
I am using a Private Managed Object Context to create some new objects into the persistent store, then after saving the private MOC, merging them into the main MOC using mergeChangesFromContextDidSaveNotification. This works fine, and updates the UI as required, and the NSManagedObjectContextWillSaveNotification is NOT invoked here for the mainMOC.
Then I make some changes to the mainMOC using the UI, and listen to NSManagedObjectContextWillSaveNotification. The notification is posted, but it contains not only the edits I made, but also the objects that were merged in from the PrivateMOC using mergeChangesFromContextDidSaveNotification.
Is there a way to ignore the changes that were merged in from another context into the mainContext, on subsequent contextDidChange notifications?
Here is the setup:
- (void) loadData {
privateContext = [[NSManagedObjectContext alloc] initWithConcurrencyType: NSPrivateQueueConcurrencyType];
privateContext.persistentStoreCoordinator = self.mainContext.persistentStoreCoordinator;
[[NSNotificationCenter defaultCenter] addObserver:self
selector:#selector(contextWillSave:)
name:NSManagedObjectContextWillSaveNotification
object: self.mainContext];
NSManagedObject *object = [NSEntityDescription insertNewObjectForEntityForName:record.recordType inManagedObjectContext: self.privateContext];
// fill in object
if ([self.privateContext hasChanges]) {
[self savePrivateContextAndMergeWithMainContext: self.privateContext];
}
}
- (void) savePrivateContextAndMergeWithMainContext: (NSManagedObjectContext *) privateContext {
[[NSNotificationCenter defaultCenter] addObserver:self selector:#selector(privateContextDidChange:) name:NSManagedObjectContextDidSaveNotification object:privateContext];
__block NSError *error = nil;
[privateContext performBlockAndWait:^{
NSLog(#"PrivateContext saved");
[privateContext save:&error];
}];
[[NSNotificationCenter defaultCenter] removeObserver:self name:NSManagedObjectContextDidSaveNotification object:privateContext];
if (error) {
NSLog(#"error = %#", error);
}
}
- (void) privateContextDidChange: (NSNotification *) notification{
[self.mainContext performBlockAndWait:^{
NSLog(#"merged into mainContext");
[self.mainContext mergeChangesFromContextDidSaveNotification:notification];
}];
}
This works fine and saving the private context and merging into the mainContext doesn't trigger a contextWillSave notification. But on editing the data from the UI (on the main MOC) triggers the notification and includes the data that was previously saved using the private MOC.
Hope that's clear. Let me know if I should include anything else.
-- UPDATE --
Seems like the problem is with specifically deleting objects from the private context. After deleting from the private context, and calling mergeChangesFromContextDidSaveNotification on the main MOC, the mainMoc's deletedObjects set still shows the object that was deleted. This doesn't happen with inserts or updates in the private context. Is this documented anywhere? What could be the workaround?
Modifying privateContextDidChange like this ...
- (void) privateContextDidChange: (NSNotification *) notification{
if (notification.object == PrivateMOC) {
[self.mainContext performBlockAndWait:^{
NSLog(#"merged into mainContext");
[self.mainContext mergeChangesFromContextDidSaveNotification:notification];
}];
}
}
... where PrivateMOC is the reference to the Private Managed Object Context?
Core Data has been around for a number of years now, and over that time the concurrency model has evolved.
The "thread confinement" and "locking" concurrency models used notifications to communicate changes between contexts. When a context was saved a notification would be broadcast with information about the changes. That information could be used to merge the changes from one context into others. This never scaled particularly well and was often a major pain point for applications. The "locking" and "thread confinement" models have been obsolete for a number of years now.
When "queue confinement" was introduced the concept of "nested contexts" was introduced along with it. A context could have a parent context, and when the child context is saved those changes are automatically communicated to the parent (and no further). This is the recommended way to communicate changes between contexts when using queue confinement, which you are.
It is not recommended that you use mergeChangesFromContextDidSaveNotification: with a NSPrivateQueueConcurrencyType context.
Core Data performs a lot of internal bookkeeping and change tracking on your behalf. Normally during a save operation that information is aggregated and coalesced - this is part of the processPendingChanges operation. When the save operation is performed inside a performBlockAndWait: this may not happen, or may not be complete - which results in the changes (and any change notification) being incomplete or not happening at all. performBlockAndWait: is reentrant, which is not compatible with the processPendingChanges process. Using performBlock: instead will result in more consistent behavior.
If you use nested contexts to communicate changes rather than notifications your issue as you describe in your question will be solved.
I am working with Core Data and learning how things are done. I am currently reading all data from the main context and saving all data on the background context. I registered for notifications for when the background context is saved. When I merge the saved changes that were on the private context, the ui stops for split second. Is there any way to prevent that little glitch?
Here is what my context saved looks like:
#synchronized(self) {
[self.mainManagedObjectContext performBlock:^{
[self.mainManagedObjectContext mergeChangesFromContextDidSaveNotification:notification];
[[NSNotificationCenter defaultCenter] postNotificationName:ModelDidUpdateNotification
object:self.mainManagedObjectContext
userInfo:[notification userInfo]];
}];
}
I've tried taking out multiple lines of code, and the glitch stops when I take out the merge. But if I take that out. I don't get the changes I need on the main context.
Here is the background context save:
[self.backgroundMainManagedObjectContext performBlock:^
{
saveBlock(self.backgroundMainManagedObjectContext);
if([[NSThread currentThread] isMainThread])
{
NSLog(#"Saving on main thread!!");
}
if([self.backgroundMainManagedObjectContext hasChanges])
{
[self.backgroundMainManagedObjectContext save:&error];
}
}];
I don't really know what I'm doing as far as Core Data is concerned. So any help would be great!
I came across intriguing behaviour when using NSManagedObjectContext's performBlock: with notification center.
From the main UI thread I trigger asynchronous data download (using NSURLConnection's connectionWithRequest:). When data arrive the following delegate method is called:
- (void)downloadCompleted:(NSData *)data
{
NSArray *new_data = [NSJSONSerialization JSONObjectWithData:data options:0 error:nil];
self.backgroundObjectContext = [[NSManagedObjectContext alloc]
initWithConcurrencyType:NSPrivateQueueConcurrencyType];
self.backgroundObjectContext.persistentStoreCoordinator = self.persistentStoreCoordinator;
[self.backgroundObjectContext performBlockAndWait:^{
[self saveToCoreData:new_data];
}];
}
The savetoCoreData: method is simply saving new data to the background context:
- (void)saveToCoreData:(NSArray*)questionsArray
{
for (NSDictionary *questionDictionaryObject in questionsArray) {
Question *newQuestion = [NSEntityDescription
insertNewObjectForEntityForName:#"Question"
inManagedObjectContext:self.backgroundObjectContext];
newQuestion.content = [questionDictionaryObject objectForKey:#"content"];
}
NSError *savingError = nil;
[self.backgroundObjectContext save:&savingError];
}
In the view controller, in viewDidLoad I add observer to the notification center:
[[NSNotificationCenter defaultCenter] addObserver:self selector:#selector(contextChanged:)
name:NSManagedObjectContextDidSaveNotification
object:nil];
And then in the contexChanged: I merge the background context with the main context so that my NSFetchedResultsController's delegate methods are called where my view can be updated:
- (void)contextChanged:(NSNotification*)notification
{
if ([notification object] == self.managedObjectContext) return;
[self.managedObjectContext mergeChangesFromContextDidSaveNotification:notification];
}
It all seems to work well, but there is one thing that bothers me. When in downloadCompleted: method I use performBlock: instead of performBlockAndWait: the notification seems to be delayed. It takes noticeable (around 5s) amount of time from the moment the background thread does save: till the moment NSFetchedResultsController calls its delegate. When I use performBlockAndWait: I do not observe any visible delay - the delegate is called as fast as if I called saveToCoreData: inside _dispatch_async_.
Does anyone saw that before and know if this is normal or am I abusing something?
As pointed out by Dan in one of the comments, the merge operation should happen on the main thread. This can be easily observed by changing the contextChanged: method to do the following:
- (void)contextChanged:(NSNotification*)notification
{
if ([notification object] == self.managedObjectContext) return;
if (![NSThread isMainThread]) {
[self performSelectorOnMainThread:#selector(contextChanged:)
withObject:notification
waitUntilDone:YES];
return;
}
[self.managedObjectContext mergeChangesFromContextDidSaveNotification:notification];
}
With this change, both performBlock: and performBlockAndWait: are working.
As long as this explains to some extent why the problems were occurring in the first place, I still do not understand why performBlock: and performBlockAndWait: perform differently from the threading perspective. Apple documentation says:
performBlock: and performBlockAndWait: ensure the block operations are executed on the queue specified for the context. The performBlock: method returns immediately and the context executes the block methods on its own thread. With the performBlockAndWait: method, the context still executes the block methods on its own thread, but the method doesn’t return until the block is executed.
This indicates, that if the true root cause of the problem described in the question is that merging is happening in the background thread, then I should observe identical behaviour regardless of which method I am calling: performBlock: and performBlockAndWait: - both are executing in a sperate thread.
As a side note, since the NSURLConnection calls the delegate method on the same thread that started the asynchronous load operation - main thread in my case - using background context seems to be not necessary at all. NSURLConnections deliver its events in the run loop anyway.
quick question. I have a core data stack(child/parent)contexts.
The child fetches json objs and parses them then saves them up to the parent,and when the count is 20 parent gets main thread and saves ...All works fine. However in my tableview i end up having to refetch the whole db every time! My fetchcount and duration is huge do to this, can any one give me some ideas? Iam all out thanks in advance! Also for some reason [[[SharedStore ShareStoreManager]getMasterContext] reset] works fine ... just not mergeChangesFromContext!
NSNotificationCenter *mergeNotification = [NSNotificationCenter defaultCenter];
[mergeNotification addObserver:self
selector:#selector(mergeChanges:)
name:NSManagedObjectContextDidSaveNotification
object:[[SharedStore ShareStoreManager]getMasterContext]]
-(void)mergeChanges:(NSNotification *)notification {
[[[SharedStore ShareStoreManager]getMasterContext] mergeChangesFromContextDidSaveNotification:notification];
[self.tableView layoutIfNeeded];
[self.tableView reloadData];
}
EDIT: I even went in to the context object and saw Inserted items that were not being merged so i went in there forced it but still no luck HELP!!!
for (User *user in [[notification.userInfo objectForKey:#"inserted"] allObjects]) {
[[[SharedStore ShareStoreManager]getMasterContext] refreshObject:user mergeChanges:YES];
}
[self.tableView reloadSections:[NSIndexSet indexSetWithIndex:0] withRowAnimation:UITableViewRowAnimationFade];
}
I'd reconsider your design and use a NSFetchedResultsController to do the fetching of your objects. What's great about using that class for your data source is that it will automatically get notifications when things change in its managed object context. By implementing the delegate callbacks for it, you can have your tableview respond to changes in the fetched results controller's data by inserting, deleting, moving, modifying the appropriate rows in the table view.
Here's a tutorial outlining step by step how to hook it all up.
Edit:
Looking at your code, when you add the observer, you are only listening for saves that occur with your master context. If you are using a separate context to do your background processing, this notification will only get published for that background context, thus not triggering your observer. The only way your main context will trigger that notification is if you merge the background context with the main thread context and save the main thread context.
Your core data stack class should have its own observer, listening for all save events:
[[NSNotificationCenter defaultCenter] addObserver:sharedController
selector:#selector(contextDidSave:)
name:NSManagedObjectContextDidSaveNotification
object:nil];
It should then merge the changes occurring on different threads and contexts on the main thread:
- (void)contextDidSave:(NSNotification *)notification {
if (![NSThread isMainThread]) {
dispatch_async(dispatch_get_main_queue(), ^{
[self contextDidSave:notification];
});
} else {
[self.managedObjectContext mergeChangesFromContextDidSaveNotification:notification];
}
}
Note here that we're making sure that we perform the merge on the main thread, since the main context was created on the main thread and is not thread safe. Once the merge is completed, notifications will go out to observers, such as an NSFetchedResultsController which will then trigger its own delegate callbacks, giving you the opportunity to refresh the UI.
I'm getting a weird zombie object issue when I'm trying to merge my changes from a background NSOperation:
(controller:didChangeObject:atIndexPath:forChangeType:newIndexPath:]: message sent to deallocated instance)
I have a ViewController that pushes other controllers on a navController stack in my AppDelegate in the didSelectRowForIndexPath like so:
ABCViewController *myVC = [[ABCViewController alloc] initWithNibName:#"ABCViewController" bundle:nil];
ABCEvent *selectedEvent = [_fetchedResultsController objectAtIndexPath:indexPath];
[myVC setManagedObjectContext:[self managedObjectContext]];
[myVC setTitle:#"Title"];
[myVC setEvent:selectedEvent];
ABCAppDelegate *appDelegate = (ABCAppDelegate *)[[UIApplication sharedApplication] delegate];
[[appDelegate navController] pushViewController:myVC animated:YES];
[myVC release];
Then in my ViewDidLoad for ABCViewContoller I'm creating an operation queue, and adding my background operation to it:
_operationQueue = [[NSOperationQueue alloc] init];
As well as hooking up a notification so I can merge the changes:
[[NSNotificationCenter defaultCenter] addObserver:self
selector:#selector(contextDidSave:)
name:NSManagedObjectContextDidSaveNotification
object:nil];
All standard stuff I believe. The issue is if I select the row in the main VC, then go back to it, and hit the row again, I get that: message sent to deallocated instance message. Now, if I turn off the notification for NSManagedObjectContextDidSaveNotification, then I don't get the error, so that's definitely the culprit. My dealloc in my second controller is like this:
- (void)dealloc
{
[super dealloc];
[[NSNotificationCenter defaultCenter] removeObserver:self];
[_operationQueue release];
}
So I'm unhooking that class as an observer. In my mind it shouldn't be trying to merge changes on any unallocated instances. I believe somehow though it's trying to merge changes back in on the first instance of my controller when I touch the row again, even though I stopped subscribing to that notification.
I'm pretty stumped on this. Any help would be much appreciated.
Are you using a fetchedResultsControllerDelegate at all?
If you are, make sure to also set that to nil when the view disappears or unloads, otherwise it will continue to receive messages about changes in your data, regardless of removing the observer.
One quick correction that should help is your [super dealloc] should always be the last statement in your dealloc.
Also, since you are doing CoreData operations on a background thread (via NSOperationQueue), make sure you are creating your NSManagedObjectContext for your background thread object while in the background thread. Do not create it on one thread and assign it to an object that will run in a different thread, or you will have all sorts of issues.
When you get your NSManagedObjectContextDidSaveNotification notification, you need to do the merge on the same thread that the context you are merging into belongs on. More than likely this will be your main context in (it sounds like) your app delegate, which means it is on the main thread.