I have two UIViewControllers in a Tab Bar
In one of the TabBar I am making an api call using AFNetworking and this api call is saving data in CoreData.
Here is my code
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
for (int i = 0; i < cartList.count; i++)
{
NSDictionary *dict = [cartList objectAtIndex:i];
NSFetchRequest *request = [Orders fetchRequest];
request.predicate = [NSPredicate predicateWithFormat:#"orderId = %#", [dict objectForKey:kiD]];
NSError *error = nil;
NSArray *itemsList = context executeFetchRequest:request error:&error];
if (itemsList.count == 0)
{
Orders *order = [NSEntityDescription insertNewObjectForEntityForName:#"Orders" inManagedObjectContext:appDel.persistentContainer.viewContext];
[order updateWithDictionary:dict];
order.isNew = NO;
}
else
{
Orders *order = [itemsList objectAtIndex:0];
[order updateWithDictionary:dict];
order.isNew = NO;
}
}
dispatch_async(dispatch_get_main_queue(), ^{
[appDel saveContext];
[self refreshValues:NO];
});
});
In second VIewController I am doing something like that. If I switch the tab controllers very fast the app crashes at
[appDel saveContext];
most probably because the last time viewContext was used by other UIviewController in Background thread.
What is the work around I can adopt to fix this problem
If this is correctly implemented
[appDel.persistentContainer performBackgroundTask:^(NSManagedObjectContext * _Nonnull context)
{
NSFetchRequest *request = [Categories fetchRequest];
NSBatchDeleteRequest *deleteReq = [[NSBatchDeleteRequest alloc] initWithFetchRequest:request];
NSError *deleteError = nil;
[appDel.persistentContainer.viewContext executeRequest:deleteReq error:&deleteError];
for (int i = 0; i < dataArr.count; i++)
{
Categories *category = [NSEntityDescription insertNewObjectForEntityForName:#"Categories" inManagedObjectContext:appDel.persistentContainer.viewContext];
[category updateWithDictionary:[dataArr objectAtIndex:i]];
}
#try {
NSError *error = nil;
[context save:(&error)];
} #catch (NSException *exception)
{
}
[self getCategoryItems];
}];
Core-data is not thread-safe, neither for reading for for writing. If you violate this ever core-data can fail in unexpected ways. So even if it appears to work you can find core-data suddenly crashing for no apparent reasons. In other words, accessing core-data from the wrong thread is undefined.
There are a few possible solutions:
1) only use the main thread for reading and writing to core-data. This is an OK solution for simple apps that don't do a lot of data import or export and have relatively small data sets.
2) Wrap NSPersistentContainer's performBackgroundTask in an operation queue and only write to core-data through that method and never write to the viewContext. When you use performBackgroundTask the method gives you a context. You should use the context to fetch any objects that you need, modify them, save the context and then discard the context and the objects.
If you try to write using both performBackgroundTask and writing directly to the viewContext you can get write conflicts and lose data.
Create a child NSManagedObjectContext object with NSPrivateQueueConcurrencyType your data processing in background queue.
Read Concurrency guide for more info.
Related
SETUP (You can read this later and skip to the scenario section first)
It's an old app, with manually setup CoreData stack like this:
+ (NSManagedObjectContext *)masterManagedObjectContext
{
if (_masterManagedObjectContext) {
return _masterManagedObjectContext;
}
NSPersistentStoreCoordinator *coordinator = [self createPersistentStoreCoordinator];
if (coordinator != nil) {
_masterManagedObjectContext = [[NSManagedObjectContext alloc] initWithConcurrencyType:NSPrivateQueueConcurrencyType];
_masterManagedObjectContext.retainsRegisteredObjects = YES;
_masterManagedObjectContext.mergePolicy = NSOverwriteMergePolicy;
_masterManagedObjectContext.persistentStoreCoordinator = coordinator;
}
return _masterManagedObjectContext;
}
+ (NSManagedObjectContext *)managedObjectContext
{
if (_managedObjectContext) {
return _managedObjectContext;
}
NSManagedObjectContext *masterContext = [self masterManagedObjectContext];
if (masterContext) {
_managedObjectContext = [[NSManagedObjectContext alloc] initWithConcurrencyType:NSMainQueueConcurrencyType];
_managedObjectContext.retainsRegisteredObjects = YES;
_managedObjectContext.mergePolicy = NSOverwriteMergePolicy;
_managedObjectContext.parentContext = masterContext;
}
return _managedObjectContext;
}
+ (NSManagedObjectContext *)newManagedObjectContext
{
__block NSManagedObjectContext *newContext = nil;
NSManagedObjectContext *parentContext = [self managedObjectContext];
if (parentContext) {
newContext = [[NSManagedObjectContext alloc] initWithConcurrencyType:NSPrivateQueueConcurrencyType];
newContext.parentContext = parentContext;
}
return newContext;
}
And then save context recursively:
+ (void)saveContext:(NSManagedObjectContext *)context
{
[context performBlockAndWait:^{
if (context.hasChanges && context.persistentStoreCoordinator.persistentStores.count) {
NSError *error = nil;
if ([context save:&error]) {
NSLog(#"saved context: %#", context);
// Recursive save parent context.
if (context.parentContext) [self saveContext:context.parentContext];
}
else {
// do some real error handling
NSLog(#"Could not save master context due to %#", error);
}
}
}];
}
SCENARIO
The app load lots of data from a server, then perform update inside newContext first, then merge into mainContext -> masterContext -> persistentStore.
Because lots of data, the sync process has been divided into about 10 async threads => we have 10 newContext at a time.
Now, the data is complicated, with things like parents <-> children (same class). 1 parent can have many children, and a child can have a mother, father, god father, step mother..., so it's n-n relationship. First, we fetch parent, then perform fetch child and then set the child to parent, and so on.
The server is kinda stupid, it can't send disabled objects. However the customer would like to control the display of app's objects from the back end, so I have 2 properties to do that:
hasUpdated: At the beginning of loading process, perform a batch update, set all object's hasUpdated to NO. When got data from the server, update this property to YES.
isActive: When all loading was done, perform batch update this property to NO if hasUpdate == NO. Then, I have a filter that won't show object with isActive == NO
ISSUE
Customers complain why some objects being missing even if they're enable in the backend. I've struggle and debugging for so long after got to this strange issue:
newContext.updatedObjects : { obj1.ID = 100, hasUpdated == YES }
"saved newContext"
mainContext.updatedObjects: {obj1.ID = 100, hasUpdated == NO }
// I'll stop here. Obviously, master got updated = NO and finally isActive will set to no, which cause missing objects.
If it happened every time, then probably easier to fix (¿maybe?). However, it occurs like this:
First time running (by first time, I mean app start from where appDidFinishLaunch... got called): all correct
2nd time: missing (153 objects)
3rd time: all correct
4th time: missing (153 objects) (again? exactly those with multiple parents, I believe so!)
5th time: correct again
... so on.
Also, it looks like this happened for objects which have the same context (same newContext). Unbelievable.
QUESTIONS
Why is this happening? How do I fix this? If those objects don't have children, my life would be easier!!!!
BONUS
In case you'd like to know how the batch update is, it's below. Note:
Download requests are in async queue: _shareInstance.apiQueue = dispatch_queue_create("product_request_queue", DISPATCH_QUEUE_CONCURRENT);
Parse response and update properties are syncronous in a queue: _shareInstance.saveQueue = dispatch_queue_create("product_save_queue", DISPATCH_QUEUE_SERIAL);
Whenever parse complete, I perform save newContext and call for updateProductActiveStatus: in the same serial queue. If all requests are finished, then perform batch update status. Since request are done in concurent queue, it's always finished earlier than save (serial) queue, so it's pretty much fool proof process.
Code:
// Load Manager
- (void)resetProductUpdatedStatus
{
NSBatchUpdateRequest *request = [NSBatchUpdateRequest batchUpdateRequestWithEntityName:NSStringFromClass([Product class])];
request.propertiesToUpdate = #{ #"hasUpdated" : #(NO) };
request.resultType = NSUpdatedObjectsCountResultType;
NSBatchUpdateResult *result = (NSBatchUpdateResult *)[self.masterContext executeRequest:request error:nil];
NSLog(#"Batch update hasUpdated: %#", result.result);
[self.masterContext performBlockAndWait:^{
[self.masterContext refreshAllObjects];
[[CoreDataUtil managedObjectContext] performBlockAndWait:^{
[[CoreDataUtil managedObjectContext] refreshAllObjects];
}];
}];
}
- (void)updateProductActiveStatus:(SyncComplete)callback
{
if (self.apiRequestList.count) return;
NSBatchUpdateRequest *request = [NSBatchUpdateRequest batchUpdateRequestWithEntityName:NSStringFromClass([Product class])];
request.predicate = [NSPredicate predicateWithFormat:#"hasUpdated = NO AND isActive = YES"];
request.propertiesToUpdate = #{ #"isActive" : #(NO) };
request.resultType = NSUpdatedObjectsCountResultType;
NSBatchUpdateResult *result = (NSBatchUpdateResult *)[self.masterContext executeRequest:request error:nil];
NSLog(#"Batch update isActive: %#", result.result);
[self.masterContext performBlockAndWait:^{
[self.masterContext refreshAllObjects];
NSManagedObjectContext *maincontext = [CoreDataUtil managedObjectContext];
NSLog(#"Refreshed master");
[maincontext performBlockAndWait:^{
[maincontext refreshAllObjects];
NSLog(#"Refreshed main");
// Callback
if (callback) dispatch_async(dispatch_get_main_queue(), ^{ callback(YES, nil); });
}];
}];
}
mergePolicy is evil. The only correct mergePolicy is NSErrorMergePolicy any other policy is asking core-data to silently fail and not update when you expect it too.
I suspect that your problem is that you are writing simultaneously to core-data with the background contexts. (I know that you say you have a serial queue - but if you call performBlock inside the queue then each block is executed simultaneously). When there is a conflict stuff gets overwritten. You should only write to core-data in one synchronous way.
I wrote an answer on how to accomplish this with a NSPersistentContainer:
NSPersistentContainer concurrency for saving to core data and I would suggest that you migrate your code to it. It really should not be that hard.
If you want to keep the code as close to what is currently is as possible that also is not that hard.
Make a serial operation queue:
_persistentContainerQueue = [[NSOperationQueue alloc] init];
_persistentContainerQueue.maxConcurrentOperationCount = 1;
And do all writing using this queue:
- (void)enqueueCoreDataBlock:(void (^)(NSManagedObjectContext* context))block{
void (^blockCopy)(NSManagedObjectContext*) = [block copy];
[self.persistentContainerQueue addOperation:[NSBlockOperation blockOperationWithBlock:^{
NSManagedObjectContext* context = [CoreDataUtil newManagedObjectContext];
[context performBlockAndWait:^{
blockCopy(context);
[CoreDataUtil saveContext:context];
}];
}]];
}
Also it could be that the objects ARE updated, but you aren't seeing it because you are relying on a fetchedResultsController to be updated. And fetchedResultsController don't update from batch update requests.
According to apple guideline, In order to use managed objects on the main thread, they need to be fetched by a context confined to the main thread only ,Ok thats fine. Below is my code...
AppDelegate *del = [[UIApplication sharedApplication] delegate];
dispatch_queue_t queue1 = dispatch_queue_create("com.MyApp.AppTask",NULL);
dispatch_queue_t main = dispatch_get_main_queue();
dispatch_async(queue1, ^{
NSManagedObjectContext *workerContext = [[NSManagedObjectContext alloc] initWithConcurrencyType:NSPrivateQueueConcurrencyType];
workerContext.persistentStoreCoordinator = del.persistentStoreCoordinator;
NSFetchRequest *fetchRequest = [NSFetchRequest fetchRequestWithEntityName:#"Person"];
NSArray *managedObject = [workerContext executeFetchRequest:fetchRequest error:nil];
dispatch_async(main, ^{
NSLog(#"%#",managedObject);
Person *objperson = [managedObject objectAtIndex:0];
objperson.firstname = #“test”;
BOOL s = [workerContext save:nil];
if (s) {
NSLog(#"done");
}
});
});
Now as per the guideline I can not modify or save managed object context created by another thread. But above code works fine and modify and save my object without any error. Hence I am able to modify the MO which is fetched by another thread and even I can save MOC which is created by another thread.
Please let me know if my way of doing this is wrong or not because ideally i could not save MOC of Background thread from main thread.
thank you.
Its wrong because its thread-unsafe NOT thread impossible to cross threads with contexts and managed objects.
So your trivial example might work some of the time but not all of time in all situations. Sooner or later you are going to get a crash with that pattern.
if you wish to access objects between threads you must send the objectID across the thread.
When you create a context with NSPrivateQueueConcurrencyType it creates and manages its own queue.
Your example is expressed better as
AppDelegate *delegate = [[UIApplication sharedApplication] delegate];
NSManagedObjectContext *workerContext = [[NSManagedObjectContext alloc] initWithConcurrencyType:NSPrivateQueueConcurrencyType];
//set the parent NOT the persistent store coordinator
workerContext.parentContext = delegate.managedObjectContext;
[workerContext performBlock:^{
NSFetchRequest *fetchRequest = [NSFetchRequest fetchRequestWithEntityName:#"Person"];
NSArray *results = [workerContext executeFetchRequest:fetchRequest error:nil];
if (results.count) {
Person *person = [results objectAtIndex:0];
person.firstname = #“test”;
BOOL success = [workerContext save:nil];
if (success) {
NSLog(#"done");
}
//you pass ObjectID's NOT managed objects across threads
NSManagedObjectID *objectID = [person objectID];
dispatch_async(dispatch_get_main_queue(), ^{
//update your UI here
Person *thePerson = (Person *)[[delegate managedObjectContext] objectWithID:objectID];
self.myUIElement.text = person.firstname;
});
}
}];
I've got an iOS app that uses restkit to handle json responses to map things into core data. Anytime I perform a request through RKObjectManager's get/post/put/delete methods, it works great, and I never run into any issues.
The app I'm developing also supports socket updates, for which I'm using SocketIO to handle. SocketIO also is working flawlessly, and every event the server sends out I receive without fail, unless the app isn't running at that time.
The issue occurs when I try to take the json data from the socket event, and map it to core data. If the socket event comes in at the same time a a response comes back from a request I made through RKObjectManager, and they are both giving me the same object for the first time, they often both end up making 2 copies of the same ManagedObject in coredata, and I get the following warning in console:
Managed object Cache returned 2 objects for the identifier configured for the [modelObjectName] entity, expected 1.
Here is the method I've made containing the code for making the RKMapperOperation:
+(void)createOrUpdateObjectWithJSONDictionary:(NSDictionary*)jsonDictionary
{
RKManagedObjectStore* managedObjectStore = [CMRAManager sharedInstance].objectManager.managedObjectStore;
NSManagedObjectContext* context = managedObjectStore.mainQueueManagedObjectContext;
[context performBlockAndWait:^{
RKEntityMapping* modelEntityMapping = [self entityMappingInManagedObjectStore:managedObjectStore];
NSDictionary* modelPropertyMappingsByDestinationKeyPath = modelEntityMapping.propertyMappingsByDestinationKeyPath;
NSString* modelMappingObjectIdSourceKey = kRUClassOrNil([modelPropertyMappingsByDestinationKeyPath objectForKey:NSStringFromSelector(#selector(object_Id))], RKPropertyMapping).sourceKeyPath;
NSString* modelObjectId = [jsonDictionary objectForKey:modelMappingObjectIdSourceKey];
CMRARemoteObject* existingObject = [self searchForObjectOfCurrentClassWithId:modelObjectId];
RKMapperOperation* mapperOperation = [[RKMapperOperation alloc]initWithRepresentation:jsonDictionary mappingsDictionary:#{ [NSNull null]: modelEntityMapping }];
[mapperOperation setTargetObject:existingObject];
RKManagedObjectMappingOperationDataSource* mappingOperationDataSource = [[RKManagedObjectMappingOperationDataSource alloc]initWithManagedObjectContext:context cache:managedObjectStore.managedObjectCache];
[mappingOperationDataSource setOperationQueue:[NSOperationQueue new]];
[mappingOperationDataSource setParentOperation:mapperOperation];
[mappingOperationDataSource.operationQueue setMaxConcurrentOperationCount:1];
[mappingOperationDataSource.operationQueue setName:[NSString stringWithFormat:#"%# with operation '%#'", NSStringFromSelector(_cmd), mapperOperation]];
[mapperOperation setMappingOperationDataSource:mappingOperationDataSource];
NSError* mapperOperationError = nil;
BOOL mapperOperationSuccess = [mapperOperation execute:&mapperOperationError];
NSAssert((mapperOperationError == nil) && (mapperOperationSuccess == TRUE), #"Execute mapperOperation error");
if (mapperOperationError || (mapperOperationSuccess == FALSE))
{
NSLog(#"mapperOperationError: %#",mapperOperationError);
}
NSError* contextSaveError = nil;
BOOL contextSaveSuccess = [context saveToPersistentStore:&contextSaveError];
NSAssert((contextSaveError == nil) && (contextSaveSuccess == TRUE), #"Save context error");
}];
}
In the above code, I first try and fetch the existing managed object if it currently exists to set it to the mapper request's target object. The method to find the object (searchForObjectOfCurrentClassWithId:) looks like the following:
+(instancetype)searchForObjectOfCurrentClassWithId:(NSString*)objectId
{
NSManagedObjectContext* context = [CMRAManager sharedInstance].objectManager.managedObjectStore.mainQueueManagedObjectContext;
__block CMRARemoteObject* fetchedObject = nil;
[context performBlockAndWait:^{
NSFetchRequest* fetchRequest = [self fetchRequestForCurrentClassObjectWithId:objectId];
NSError* fetchError = nil;
NSArray *entries = [context executeFetchRequest:fetchRequest error:&fetchError];
if (fetchError)
{
NSLog(#"fetchError: %#",fetchError);
return;
}
if (entries.count != 1)
{
return;
}
fetchedObject = kRUClassOrNil([entries objectAtIndex:0], CMRARemoteObject);
if (fetchedObject == nil)
{
NSAssert(FALSE, #"Should be of this class");
return;
}
}];
return fetchedObject;
}
My best guess at the issue here is that it's probably due to the managed object contexts, and their threads. I don't have the best understanding of how they should necessarily be working, as I've been able to depend on Restkit's correct usage of it. I've done my best to copy how Restkit set up these mapping operations, but am assuming I've made an error somewhere in the above code.
I'm willing to post any other code if it would be helpful. I didn't post my RKEntityMapping code, because I'm pretty sure the error doesn't lie there - after all, Restkit has been successfully mapping these objects when it does the mapper operation itself, even when there's redundant JSON objects/data to map.
Another reason I think the issue must be my doing a bad implementation of the managed object contexts and their threads, is because I'm testing on an iPhone 5c, and an iPod touch, and the issue doesn't happen on the iPod touch, which I believe only has 1 core, but the iPhone 5c does sometimes encounter the issue, and I believe it has multiple cores. I should emphasize that I'm not sure of the statements I've made in this paragraph are necessarily true, so don't assume I know what I'm talking about with the device cores, it's just something I think I've read before.
try changing:
RKManagedObjectMappingOperationDataSource* mappingOperationDataSource = [[RKManagedObjectMappingOperationDataSource alloc]initWithManagedObjectContext:context cache:managedObjectStore.managedObjectCache];
to:
RKManagedObjectMappingOperationDataSource* mappingOperationDataSource = [[RKManagedObjectMappingOperationDataSource alloc]initWithManagedObjectContext:context cache:[RKFetchRequestManagedObjectCache new]];
And this for good measure before saving the persistent context:
// Obtain permanent objectID
[[RKObjectManager sharedManager].managedObjectStore.mainQueueManagedObjectContext obtainPermanentIDsForObjects:[NSArray arrayWithObject:mapperOperation.targetObject] error:nil];
EDIT #1
Try removing these lines:
[mappingOperationDataSource setOperationQueue:[NSOperationQueue new]];
[mappingOperationDataSource setParentOperation:mapperOperation];
[mappingOperationDataSource.operationQueue setMaxConcurrentOperationCount:1];
[mappingOperationDataSource.operationQueue setName:[NSString stringWithFormat:#"%# with operation '%#'", NSStringFromSelector(_cmd), mapperOperation]];
EDIT #2
Take a look at this unit test from RKManagedObjectMappingOperationDataSourceTest.m. Have you set identificationAttributes to prevent duplicates? It might not be necessary to find and set the targetObject, I thought RestKit tries to find an existing object if unset. Also try performing the object mapping on a private context created using [store newChildManagedObjectContextWithConcurrencyType:NSPrivateQueueConcurrencyType tracksChanges:NO], after the context is saved, changes should be pushed to the main context.
- (void)testThatMappingObjectsWithTheSameIdentificationAttributesAcrossTwoContextsDoesNotCreateDuplicateObjects
{
RKManagedObjectStore *managedObjectStore = [RKTestFactory managedObjectStore];
RKInMemoryManagedObjectCache *inMemoryCache = [[RKInMemoryManagedObjectCache alloc] initWithManagedObjectContext:managedObjectStore.persistentStoreManagedObjectContext];
managedObjectStore.managedObjectCache = inMemoryCache;
NSEntityDescription *humanEntity = [NSEntityDescription entityForName:#"Human" inManagedObjectContext:managedObjectStore.persistentStoreManagedObjectContext];
RKEntityMapping *mapping = [RKEntityMapping mappingForEntityForName:#"Human" inManagedObjectStore:managedObjectStore];
mapping.identificationAttributes = #[ #"railsID" ];
[mapping addAttributeMappingsFromArray:#[ #"name", #"railsID" ]];
// Create two contexts with common parent
NSManagedObjectContext *firstContext = [managedObjectStore newChildManagedObjectContextWithConcurrencyType:NSPrivateQueueConcurrencyType tracksChanges:NO];
NSManagedObjectContext *secondContext = [managedObjectStore newChildManagedObjectContextWithConcurrencyType:NSPrivateQueueConcurrencyType tracksChanges:NO];
// Map into the first context
NSDictionary *objectRepresentation = #{ #"name": #"Blake", #"railsID": #(31337) };
// Check that the cache contains a value for our identification attributes
__block BOOL success;
__block NSError *error;
[firstContext performBlockAndWait:^{
RKManagedObjectMappingOperationDataSource *dataSource = [[RKManagedObjectMappingOperationDataSource alloc] initWithManagedObjectContext:firstContext
cache:inMemoryCache];
RKMapperOperation *mapperOperation = [[RKMapperOperation alloc] initWithRepresentation:objectRepresentation mappingsDictionary:#{ [NSNull null]: mapping }];
mapperOperation.mappingOperationDataSource = dataSource;
success = [mapperOperation execute:&error];
expect(success).to.equal(YES);
expect([mapperOperation.mappingResult count]).to.equal(1);
[firstContext save:nil];
}];
// Check that there is an entry in the cache
NSSet *objects = [inMemoryCache managedObjectsWithEntity:humanEntity attributeValues:#{ #"railsID": #(31337) } inManagedObjectContext:firstContext];
expect(objects).to.haveCountOf(1);
// Map into the second context
[secondContext performBlockAndWait:^{
RKManagedObjectMappingOperationDataSource *dataSource = [[RKManagedObjectMappingOperationDataSource alloc] initWithManagedObjectContext:secondContext
cache:inMemoryCache];
RKMapperOperation *mapperOperation = [[RKMapperOperation alloc] initWithRepresentation:objectRepresentation mappingsDictionary:#{ [NSNull null]: mapping }];
mapperOperation.mappingOperationDataSource = dataSource;
success = [mapperOperation execute:&error];
expect(success).to.equal(YES);
expect([mapperOperation.mappingResult count]).to.equal(1);
[secondContext save:nil];
}];
// Now check the count
objects = [inMemoryCache managedObjectsWithEntity:humanEntity attributeValues:#{ #"railsID": #(31337) } inManagedObjectContext:secondContext];
expect(objects).to.haveCountOf(1);
// Now pull the count back from the parent context
NSFetchRequest *fetchRequest = [NSFetchRequest fetchRequestWithEntityName:#"Human"];
fetchRequest.predicate = [NSPredicate predicateWithFormat:#"railsID == 31337"];
NSArray *fetchedObjects = [managedObjectStore.persistentStoreManagedObjectContext executeFetchRequest:fetchRequest error:nil];
expect(fetchedObjects).to.haveCountOf(1);
}
This is the solution we went with. Ensure identificationAttributes have been set in the mapping. Use RKMappingOperation without setting its destinationObject and RestKit will try to find an existing entity to map to by its identificationAttributes. We're also using RKFetchRequestManagedObjectCache as a precaution as we found the in-memory cache was unable to correct fetch the entities sometimes thus creating a duplicate entity..
NSManagedObjectContext *firstContext = [[NSManagedObjectContext alloc] initWithConcurrencyType:NSPrivateQueueConcurrencyType];
firstContext.parentContext = [RKObjectManager sharedInstance].managedObjectStore.mainQueueManagedObjectContext;
firstContext.mergePolicy = NSOverwriteMergePolicy;
RKEntityMapping* modelEntityMapping = [self entityMappingInManagedObjectStore:[CMRAManager sharedInstance].objectManager.managedObjectStore];
RKMappingOperation *operation = [[RKMappingOperation alloc] initWithSourceObject:jsonDictionary destinationObject:nil mapping:modelEntityMapping];
// Restkit memory cache sometimes creates duplicates when mapping quickly across threads
RKManagedObjectMappingOperationDataSource *mappingDS = [[RKManagedObjectMappingOperationDataSource alloc] initWithManagedObjectContext:firstContext
cache:[RKFetchRequestManagedObjectCache new]];
operation.dataSource = mappingDS;
NSError *mappingError;
[operation performMapping:&mappingError];
[operation waitUntilFinished];
if (mappingError || !operation.destinationObject) {
return; // ERROR
}
[firstContext performBlockAndWait:^{
[firstContext save:nil];
}];
Please give this a try, use RKMappingOperation without setting the destination object, RestKit will try to find an existing object for you (if one exists) based on its identificationAttributes.
#pragma mark - Create or Update
+(void)createOrUpdateObjectWithJSONDictionary:(NSDictionary*)jsonDictionary
{
RKEntityMapping* modelEntityMapping = [self entityMappingInManagedObjectStore:[CMRAManager sharedInstance].objectManager.managedObjectStore];
// Map on the main MOC so that we receive the proper update notifications for anything
// observing relationships and properties on this model
RKMappingOperation *operation = [[RKMappingOperation alloc] initWithSourceObject:jsonDictionary
destinationObject:nil
mapping:modelEntityMapping];
RKManagedObjectMappingOperationDataSource *mappingDS = [[RKManagedObjectMappingOperationDataSource alloc] initWithManagedObjectContext:[CMRAManager sharedInstance].objectManager.managedObjectStore.mainQueueManagedObjectContext
cache:[RKFetchRequestManagedObjectCache new]];
operation.dataSource = mappingDS;
NSError *mappingError;
[operation performMapping:&mappingError];
if (mappingError || !operation.destinationObject) {
return; // ERROR
}
// Obtain permanent objectID
[[RKObjectManager sharedManager].managedObjectStore.mainQueueManagedObjectContext performBlockAndWait:^{
[[RKObjectManager sharedManager].managedObjectStore.mainQueueManagedObjectContext obtainPermanentIDsForObjects:[NSArray arrayWithObject:operation.destinationObject] error:nil];
}];
}
I am experiencing issues with Core Data which I cannot resolve. I've learned about concurrency issues in Core Data the hard way, thus I am really careful and only perform any core data operations in performBlock: and performBlockAndWait: blocks.
Here goes my code:
/// Executes a fetch request with given parameters in context's block.
+ (NSArray *)executeFetchRequestWithEntityName:(NSString *)entityName
predicate:(NSPredicate *)predicate
fetchLimit:(NSUInteger)fetchLimit
sortDescriptor:(NSSortDescriptor *)sortDescriptor
inContext:(NSManagedObjectContext *)context{
NSCAssert(entityName.length > 0,
#"entityName parameter in executeFetchRequestWithEntityName:predicate:fetchLimit:sortDescriptor:inContext:\
is invalid");
__block NSArray * results = nil;
NSPredicate * newPredicate = [CWFCoreDataUtilities currentUserPredicateInContext:context];
if (predicate){
newPredicate = [NSCompoundPredicate andPredicateWithSubpredicates:#[newPredicate, predicate]];
}
[context performBlockAndWait:^{
NSFetchRequest * request = [NSFetchRequest fetchRequestWithEntityName:entityName];
request.fetchLimit = fetchLimit;
request.predicate = newPredicate;
if (sortDescriptor) {
request.sortDescriptors = #[sortDescriptor];
}
NSError * error = nil;
results = [context executeFetchRequest:request error:&error];
if (error){
#throw [NSException exceptionWithName:NSInternalInconsistencyException
reason:#"Fetch requests are required to succeed."
userInfo:#{#"error":error}];
NSLog(#"ERROR! %#", error);
}
NSCAssert(results != nil, #"Fetch requests must succeed");
}];
return results;
}
When I enter this method concurrently from two different threads and pass two different contexts, I result in a deadlock on this row: results = [context executeFetchRequest:request error:&error];
Which is interesting: it seems like both threads cannot acquire some lock on the Persistent Store Coordinator in order to execute a fetch request.
All of my contexts are NSPrivateQueueConcurrencyType.
I can't put my finger on, why am I locking the app and what should I do differently. My research on Stack Overflow gave me nothing, since most of the people fixed all the locks by dispatching the fetch requests on the MOC's queue, which I already do.
I will appreciate any information on this issue. Feel free to provide documentation links and other long reads: I am eager to learn more about all kind of concurrency problems and strategies.
Which is interesting: it seems like both threads cannot acquire some lock on the Persistent Store Coordinator in order to execute a fetch request.
The persistent store coordinator is a serial queue. If one context is accessing it another context will be blocked.
From Apple Docs:
Coordinators do the opposite of providing for concurrency—they serialize operations. If you want to use multiple threads for different write operations you use multiple coordinators. Note that if multiple threads work directly with a coordinator, they need to lock and unlock it explicitly.
If you need to perform multiple background fetch request concurrently you will need multiple persistent store coordinators.
Multiple coordinators will make your code only slightly more complex but should be avoided if possible. Do you really need to do multiple fetches at the same time? Could you do one larger fetch and then filter the in-memory results?
If you are interested in learning more about Core Data (and threading), the following website will be extremely helpful. I attended Matthew Morey's talk at the Atlanta CocoaConf 2013.
High Performance Core Data (http://highperformancecoredata.com)
The companion sample code to the website is available at: https://github.com/mmorey/MDMHPCoreData
All you need to do in your app, is to have a Singleton instance (somewhere) of the MDMPersistenceStack class.
As far as your problem/issue, even though the Apple documentation for NSManagedObjectContext class, does allow for code to be written the way that you have (with the Core Data operations performed on an NSManagedObjectContext instance in a block) and to some extent encourages that - I would like to point out that is not the only way.
Whenever I have fixed apps that have Core Data concurrency issues (locking), the simplest thing, in my opinion, is to create a private NSManagedObjectContext inside the thread or block that I want to execute Core Data operations on.
It may not be the most elegant approach, but it's never failed me. One is always guaranteed that the NSManagedObjectContext is created and executed in the same thread, because the NSManagedObjectContext is explicitly created.
dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
dispatch_async(queue, ^{
/*
Create NSManagedObjectContext
Concurrency of Managed Object Context should be set to NSPrivateQueueConcurrencyType
If you use the MDMPersistenceStack class this is handled for you.
*/
NSManagedObjectContext *managedObjectContext = ....
/*
Call the method that you have listed in your code.
Let's assume that this class method is in MyClass
Remove the block that you have in your method, as it's not needed
*/
[MyClass executeFetchRequestWithEntityName: ......] // rest of parameters
});
I did it this way. It fixed the issue for me. I was experiencing a lot of deadlocks too. See if it works for you.
+ (NSArray *)getRecordsForFetchRequest:(NSFetchRequest *)request inContext:(NSManagedObjectContext *)context
{
#try
{
__weak __block NSError *error = nil;
__block __weak NSArray *results = nil;
[context performBlockAndWait:^{
[context lock];
results = [context executeFetchRequest:request error:&error];
[context processPendingChanges];
[context unlock];
}];
[self handleErrors:error];
request = nil;
context = nil;
return results;
}
#catch (NSException *exception)
{
if([exception.description rangeOfString:#"Can only use -performBlockAndWait: on an NSManagedObjectContext that was created with a queue"].location!=NSNotFound)
{
NSError *error = nil;
[context lock];
__weak NSArray *results = [context executeFetchRequest:request error:&error];
[context processPendingChanges];
[context unlock];
[self handleErrors:error];
request = nil;
context = nil;
return results;
}
return nil;
}
}
I have a tableview in my app that contains a NSFetchedResultsController to load in some CoreData objects.
As the table builds in cellForRowAtIndexPath:, for each cell I must do a fetch to get some other info from another object.
The table is filled with UserTasks, and I must get some info from a UserSite (UserTask contains a siteID attribute)
I am getting the UserSite info in a background thread, and using a temporary context. It works fine, but it still wants to lag the UI a bit when scrolling.
Site *site = [_scannedSites objectForKey:task.siteID];
if(!site)
{
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_BACKGROUND, 0), ^{
AppDelegate *ad = [AppDelegate sharedAppDelegate];
NSManagedObjectContext *temporaryContext = [[NSManagedObjectContext alloc] initWithConcurrencyType:NSConfinementConcurrencyType];
temporaryContext.persistentStoreCoordinator = ad.persistentStoreCoordinator;
Site *site2 = [task getSiteWithContext:temporaryContext];
if(site2)
{
[ad.managedObjectContext performBlock:^{
Site *mainContextObject = (Site *)[ad.managedObjectContext objectWithID:site2.objectID];
[_scannedSites mainContextObject forKey:task.siteID];
}];
dispatch_async(dispatch_get_main_queue(), ^{
Site *newSite = [_scannedSites objectForKey:task.siteID];
cell.lblCustName.text = newSite.siteName;
cell.lblAddr.text = [NSString stringWithFormat:#"%# %#, %#", newSite.siteAddressLine1, newSite.siteCity, newSite.siteState];
cell.lblPhone.text = [self formatPhoneNum:newSite.phone];
});
}
else
{
dispatch_async(dispatch_get_main_queue(), ^{
cell.lblCustName.text = #"";
cell.lblAddr.text = #"";
cell.lblPhone.text = #"";
});
}
});
}
else
{
cell.lblCustName.text = site.siteName;
cell.lblAddr.text = [NSString stringWithFormat:#"%# %#, %#", site.siteAddressLine1, site.siteCity, site.siteState];
cell.lblPhone.text = [self formatPhoneNum:site.phone];
}
As you can see, if you dont already have the UserSite info for a task in _scannedSites, a background thread gets kicked off which gets the UserSite for that task, stores it, and then on the main thread fills in the details.
Like I said there is a pretty annoying lag when scrolling... which I hoped to avoid by doing the work in the background.
Am I going about this the wrong way?
Thanks, any advice is appreciated.
EDIT
I created a relationship in CoreData and I am now using that in cellForRowAtIndexPath. If it does not exist yet, I create it. This is working much better.
Site *site = task.site;
if(!site)
{
AppDelegate *ad = [AppDelegate sharedAppDelegate];
NSManagedObjectContext *temporaryContext = [[NSManagedObjectContext alloc] initWithConcurrencyType:NSPrivateQueueConcurrencyType];
temporaryContext.persistentStoreCoordinator = ad.persistentStoreCoordinator;
[temporaryContext performBlock:^{
Site *tempContextSite = [task getSiteWithContext:temporaryContext];
[ad.managedObjectContext performBlock:^{
Site *mainManagedObject = (Site *)[ad.managedObjectContext objectWithID:tempContextSite.objectID];
task.site = mainManagedObject;
NSError *error;
if (![temporaryContext save:&error])
{
}
[ad.managedObjectContext performBlock:^{
NSError *e = nil;
if (![ad.managedObjectContext save:&e])
{
}
dispatch_async(dispatch_get_main_queue(), ^{
cell.lblCustName.text = mainManagedObject.siteName;
cell.lblAddr.text = [NSString stringWithFormat:#"%# %#, %#", mainManagedObject.siteAddressLine1, mainManagedObject.siteCity, mainManagedObject.siteState];
cell.lblPhone.text = [self formatPhoneNum:mainManagedObject.phone];
});
}];
}];
}];
}
else
{
cell.lblCustName.text = site.siteName;
cell.lblAddr.text = [NSString stringWithFormat:#"%# %#, %#", site.siteAddressLine1, site.siteCity, site.siteState];
cell.lblPhone.text = [self formatPhoneNum:site.phone];
}
If UserTask relates to UserSite, the usual Core Data approach would be to create a relationship between the two and then use that relationship at run time. So, UserTask would have a property named site, and you'd just ask a specific instance for the value of that property. An ID attribute might still exist but would only be used when syncing with some external data store (like a server API).
Storing IDs and looking up objects like this is a fundamentally awkward approach that's pretty much designed to do a lot of unnecessary work at run time. It avoids all of the conveniences that Core Data tries to provide, doing things the hard way instead. Doing this work while the table is scrolling is also about the worst possible time, because it's when a performance issue will be most noticeable.
If you must do it this way for some reason, you could optimize things by looking up all of the UserSite instances in advance instead of while the table is scrolling. If you know all of the UserTask instances, go get all the sites in one call when the view loads.
It is a bad idea to send of asynchronous tasks in cellForRowAtIndexPath:. If the user scrolls there are going to be a whole bunch of threads created which are maybe not even necessary.
It would be much better to have a background process that fetches the information you want and then notifies the UI to update itself if needed. This is pretty standard stuff, you will find many examples for solid implementations easily.