Question

I have a singleton class that saves JSON objects to a CoreData database in the background using GCD (Grand Central Dispatch) queues. This works perfectly the majority of the time, but on iPad 2 and iPad Mini devices I am experiencing some issues with the process freezing.

My setup is pretty straight forward. I have a background dispatch queue (backgroundQueue) that is set to run serially, and I have a seperate instance of NSManagedObjectContext for the background queue. When I want to save something to the database I call the method that begins the save process, and In that method I use dispatch_async to invoke my save logic on the background thread.

Once all processing logic has ran I save the background MOC, and use NSManagedObjectContextDidSaveNotification to merge the changes from the background MOC to the main thread MOC. The background thread waits for this to complete, and once it's done it'll run the next block in the queue, if any. Below is my code.

dispatch_queue_t backgroundQueue;

- (id)init
{
    if (self = [super init]) 
    {
        // init the background queue
        backgroundQueue = dispatch_queue_create(VS_CORE_DATA_MANAGER_BACKGROUND_QUEUE_NAME, DISPATCH_QUEUE_SERIAL);
    }
    return self;
}

- (void)saveJsonObjects:(NSDictionary *)jsonDict
          objectMapping:(VS_ObjectMapping *)objectMapping
                  class:(__unsafe_unretained Class)managedObjectClass
             completion:(void (^)(NSArray *objects, NSError *error))completion
{
    [VS_Log logDebug:@"**** Queue Save JSON Objects for Class: %@", managedObjectClass];

    // process in the background queue
    dispatch_async(backgroundQueue, ^(void)
    {
        [VS_Log logDebug:@"**** Dispatch Save JSON Objects for Class: %@", managedObjectClass];

        // create a new process object and add it to the dictionary
        currentRequest = [[VS_CoreDataRequest alloc] init];

        // set the thead name
        NSThread *currentThread = [NSThread currentThread];
        [currentThread setName:VS_CORE_DATA_MANAGER_BACKGROUND_THREAD_NAME];

        // if there is not already a background context, then create one
        if (!_backgroundQueueManagedObjectContext)
        {
            NSPersistentStoreCoordinator *coordinator = [self persistentStoreCoordinator];
            if (coordinator != nil)
            {
                // create the background queue
                _backgroundQueueManagedObjectContext = [[NSManagedObjectContext alloc] init];
                [_backgroundQueueManagedObjectContext setPersistentStoreCoordinator:coordinator];
                [_backgroundQueueManagedObjectContext setMergePolicy:NSMergeByPropertyStoreTrumpMergePolicy];
                _backgroundQueueManagedObjectContext.undoManager = nil;

                // listen for the merge changes from context did save notification on the background queue
                [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(mergeChangesFromBackground:) name:NSManagedObjectContextDidSaveNotification object:_backgroundQueueManagedObjectContext];
            }
        }

        // save the JSON dictionary
        NSArray *objects = [self saveJSON:jsonDict objectMapping:objectMapping class:managedObjectClass managedObjectContext:_backgroundQueueManagedObjectContext level:0];

        // save the objects so we can access them later to be re-fetched and returned on the main thread
        if (objects.count > 0)
            [currentRequest.objects addObjectsFromArray:objects];

        // save the object IDs and the completion block to global variables so we can access them after the save
        if (completion)
            currentRequest.completionBlock = completion;

        [VS_Log logDebug:@"**** Save MOC for Class: %@", managedObjectClass];

        // save all changes object context
        [self saveManagedObjectContext];
    });
}

- (void)mergeChangesFromBackground:(NSNotification *)notification
{
    [VS_Log logDebug:@"**** Merge Changes From Background"];

    // save the current request to a local var since we're about to be processing it on the main thread
    __block VS_CoreDataRequest *request = (VS_CoreDataRequest *)[currentRequest copy];
    __block NSNotification *bNotification = (NSNotification *)[notification copy];

    // clear out the request
    currentRequest = nil;

    dispatch_sync(dispatch_get_main_queue(), ^(void)
    {
        [VS_Log logDebug:@"**** Start Merge Changes On Main Thread"];

        // merge changes to the primary context, and wait for the action to complete on the main thread
        [_managedObjectContext mergeChangesFromContextDidSaveNotification:bNotification];

        NSMutableArray *objects = [[NSMutableArray alloc] init];

        // iterate through the updated objects and find them in the main thread MOC
        for (NSManagedObject *object in request.objects)
        {
            NSError *error;
            NSManagedObject *obj = [[self managedObjectContext] existingObjectWithID:object.objectID error:&error];
            if (error)
                [self logError:error];

            if (obj)
                [objects addObject:obj];
        }

        // call the completion block
        if (request.completionBlock)
        {
            void (^saveCompletionBlock)(NSArray *, NSError *) = request.completionBlock;
            saveCompletionBlock(objects, nil);
        }

        [VS_Log logDebug:@"**** Complete Merge Changes On Main Thread"];

        // clear the request
        request = nil;
    });

    [VS_Log logDebug:@"**** Complete Merge Changes From Background"];
}

When the issue occurs everything seems to run fine, and it gets into the mergeChangesFromBackground: method, but the dispatch_async() is not invoked.

As I mentioned above the code runs perfectly in most cases. Only time it has issues running is when I am testing on either an iPad 2 or iPad Mini and saving large objects.

Does anyone out there have any ideas why this is happening?

Thanks

** EDIT **

Since writing this I've learned of nested NSManagedObjectContexts. I have modified my code to use it and it seems to be working well. However, I still have the same issues. Thoughts? Also, any comments on nested MOCs?

- (void)saveJsonObjects:(NSDictionary *)jsonDict
          objectMapping:(VS_ObjectMapping *)objectMapping
                  class:(__unsafe_unretained Class)managedObjectClass
             completion:(void (^)(NSArray *objects, NSError *error))completion
{
    // process in the background queue
    dispatch_async(backgroundQueue, ^(void)
    {
        // create a new process object and add it to the dictionary
        __block VS_CoreDataRequest *currentRequest = [[VS_CoreDataRequest alloc] init];

        // set the thead name
        NSThread *currentThread = [NSThread currentThread];
        [currentThread setName:VS_CORE_DATA_MANAGER_BACKGROUND_THREAD_NAME];

        // if there is not already a background context, then create one
        if (!_backgroundQueueManagedObjectContext)
        {
            NSPersistentStoreCoordinator *coordinator = [self persistentStoreCoordinator];
            if (coordinator != nil)
            {
                // create the background queue
                _backgroundQueueManagedObjectContext = [[NSManagedObjectContext alloc] initWithConcurrencyType:NSPrivateQueueConcurrencyType];
                [_backgroundQueueManagedObjectContext setParentContext:[self managedObjectContext]];
                [_backgroundQueueManagedObjectContext setMergePolicy:NSMergeByPropertyStoreTrumpMergePolicy];
                _backgroundQueueManagedObjectContext.undoManager = nil;
            }
        }

        // save the JSON dictionary starting at the upper most level of the key path
        NSArray *objects = [self saveJSON:jsonDict objectMapping:objectMapping class:managedObjectClass managedObjectContext:_backgroundQueueManagedObjectContext level:0];

        // if no objects were processed, then return with an error
        if (!objects || objects.count == 0)
        {
            currentRequest = nil;

            // dispatch the completion block
            dispatch_async(dispatch_get_main_queue(), ^(void)
            {
                NSError *error = [self createErrorWithErrorCode:100 description:@"No objects were found for the object mapping"];
                [self logMessage:error.debugDescription];
                completion(nil, error);
            });
        }

        // save the objects so we can access them later to be re-fetched and returned on the main thread
        if (objects.count > 0)
            [currentRequest.objects addObjectsFromArray:objects];

        // save the object IDs and the completion block to global variables so we can access them after the save
        if (completion)
            currentRequest.completionBlock = completion;

        // save all changes object context
        NSError *error = nil;
        [_backgroundQueueManagedObjectContext save:&error];
        if (error)
            [VS_Log logError:error.localizedDescription];

        dispatch_sync(dispatch_get_main_queue(), ^(void)
        {
            NSMutableArray *objects = [[NSMutableArray alloc] init];

            // iterate through the updated objects and find them in the main thread MOC
            for (NSManagedObject *object in currentRequest.objects)
            {
                NSError *error;
                NSManagedObject *obj = [[self managedObjectContext] existingObjectWithID:object.objectID error:&error];
                if (error)
                    [self logError:error];

                if (obj)
                    [objects addObject:obj];
            }

            // call the completion block
            if (currentRequest.completionBlock)
            {
                void (^saveCompletionBlock)(NSArray *, NSError *) = currentRequest.completionBlock;
                saveCompletionBlock(objects, nil);
            }
        });

        // clear out the request
        currentRequest = nil;
    });
}

** EDIT **

Hi Everyone,

I'd first like to thank everyone for this input on this, as that is what lead me to finding the solution for my problem. Long story short I did away completely with using the GCD and the custom background queue, and instead am now using nested contexts and the "performBlock" method to perform all saving on the background thread. This is working great and I elevated my hang up issue.

However, I now have a new bug. Whenever I run my app for the first time, whenever I try to save an object that has child object relations, I get the following exception.

-_referenceData64 only defined for abstract class. Define -[NSTemporaryObjectID_default _referenceData64]!

Below is my new code.

- (void)saveJsonObjects:(NSDictionary *)jsonDict
          objectMapping:(VS_ObjectMapping *)objectMapping
                  class:(__unsafe_unretained Class)managedObjectClass
             completion:(void (^)(NSArray *objects, NSError *error))completion
{
    [_backgroundManagedObjectContext performBlock:^(void)
    {
        // create a new process object and add it to the dictionary
        VS_CoreDataRequest *currentRequest = [[VS_CoreDataRequest alloc] init];
        currentRequest.managedObjectClass = managedObjectClass;

        // save the JSON dictionary starting at the upper most level of the key path
        NSArray *objects = [self saveJSON:jsonDict objectMapping:objectMapping class:managedObjectClass managedObjectContext:_backgroundManagedObjectContext level:0];

        if (objects.count == 0)
        {
            currentRequest.error = [self createErrorWithErrorCode:100 description:@"No objects were found for the object mapping"];
            [self performSelectorOnMainThread:@selector(backgroundSaveProcessDidFail:) withObject:currentRequest waitUntilDone:NO];
        }
        else
        {
            // save the objects so we can access them later to be re-fetched and returned on the main thread
            [currentRequest.objects addObjectsFromArray:objects];

            // save the object IDs and the completion block to global variables so we can access them after the save
            currentRequest.completionBlock = completion;

            [_backgroundManagedObjectContext lock];

            @try
            {
                [_backgroundManagedObjectContext processPendingChanges];
                [_backgroundManagedObjectContext save:nil];
            }
            @catch (NSException *exception)
            {
                currentRequest.error = [self createErrorWithErrorCode:100 description:exception.reason];
            }

            [_backgroundManagedObjectContext unlock];

            // complete the process on the main thread
            if (currentRequest.error)
                [self performSelectorOnMainThread:@selector(backgroundSaveProcessDidFail:) withObject:currentRequest waitUntilDone:NO];
            else
                [self performSelectorOnMainThread:@selector(backgroundSaveProcessDidSucceed:) withObject:currentRequest waitUntilDone:NO];

            // clear out the request
            currentRequest = nil;
        }
    }];
}

- (void)backgroundSaveProcessDidFail:(VS_CoreDataRequest *)request
{
    if (request.error)
        [self logError:request.error];

    if (request.completionBlock)
    {
        void (^saveCompletionBlock)(NSArray *, NSError *) = request.completionBlock;
        saveCompletionBlock(nil, request.error);
    }
}

- (void)backgroundSaveProcessDidSucceed:(VS_CoreDataRequest *)request
{
    // get objects from main thread
    NSArray *objects = nil;
    if (request.objects.count > 0)
    {
        NSFetchRequest *fetchReq = [NSFetchRequest fetchRequestWithEntityName:[NSString stringWithFormat:@"%@", request.managedObjectClass]];
        fetchReq.predicate = [NSPredicate predicateWithFormat:@"self IN %@", request.objects];
        objects = [self executeFetchRequest:fetchReq];
    }

    // call the completion block
    if (request.completionBlock)
    {
        void (^saveCompletionBlock)(NSArray *, NSError *) = request.completionBlock;
        saveCompletionBlock(objects, nil);
    }
}

- (NSManagedObjectContext *)managedObjectContext
{
    if (_managedObjectContext != nil) {
        return _managedObjectContext;
    }

    NSPersistentStoreCoordinator *coordinator = [self persistentStoreCoordinator];
    if (coordinator != nil)
    {
        // create the MOC for the backgroumd thread
        _backgroundManagedObjectContext = [[NSManagedObjectContext alloc] initWithConcurrencyType:NSPrivateQueueConcurrencyType];
        [_backgroundManagedObjectContext setPersistentStoreCoordinator:coordinator];
        [_backgroundManagedObjectContext setMergePolicy:NSMergeByPropertyStoreTrumpMergePolicy];
        _backgroundManagedObjectContext.undoManager = nil;

        // create the MOC for the main thread
        _managedObjectContext = [[NSManagedObjectContext alloc] initWithConcurrencyType:NSMainQueueConcurrencyType];
        [_managedObjectContext setParentContext:_backgroundManagedObjectContext];
        [_managedObjectContext setMergePolicy:NSMergeByPropertyStoreTrumpMergePolicy];
    }

    return _managedObjectContext;
}

Do any of you have any idea why this crash is happening?

Was it helpful?

Solution 2

I'd first like to thank everyone for this input on this, as that is what lead me to finding the solution for my problem. Long story short I did away completely with using the GCD and the custom background queue, and instead am now using nested contexts and the "performBlock" method to perform all saving on the background thread. This is working great and I elevated my hang up issue.

However, I now have a new bug. Whenever I run my app for the first time, whenever I try to save an object that has child object relations, I get the following exception.

-_referenceData64 only defined for abstract class. Define -[NSTemporaryObjectID_default _referenceData64]!

Below is my new code.

- (void)saveJsonObjects:(NSDictionary *)jsonDict
          objectMapping:(VS_ObjectMapping *)objectMapping
                  class:(__unsafe_unretained Class)managedObjectClass
             completion:(void (^)(NSArray *objects, NSError *error))completion
{
    [_backgroundManagedObjectContext performBlock:^(void)
    {
        // create a new process object and add it to the dictionary
        VS_CoreDataRequest *currentRequest = [[VS_CoreDataRequest alloc] init];
        currentRequest.managedObjectClass = managedObjectClass;

        // save the JSON dictionary starting at the upper most level of the key path
        NSArray *objects = [self saveJSON:jsonDict objectMapping:objectMapping class:managedObjectClass managedObjectContext:_backgroundManagedObjectContext level:0];

        if (objects.count == 0)
        {
            currentRequest.error = [self createErrorWithErrorCode:100 description:@"No objects were found for the object mapping"];
            [self performSelectorOnMainThread:@selector(backgroundSaveProcessDidFail:) withObject:currentRequest waitUntilDone:NO];
        }
        else
        {
            // save the objects so we can access them later to be re-fetched and returned on the main thread
            [currentRequest.objects addObjectsFromArray:objects];

            // save the object IDs and the completion block to global variables so we can access them after the save
            currentRequest.completionBlock = completion;

            [_backgroundManagedObjectContext lock];

            @try
            {
                [_backgroundManagedObjectContext processPendingChanges];
                [_backgroundManagedObjectContext save:nil];
            }
            @catch (NSException *exception)
            {
                currentRequest.error = [self createErrorWithErrorCode:100 description:exception.reason];
            }

            [_backgroundManagedObjectContext unlock];

            // complete the process on the main thread
            if (currentRequest.error)
                [self performSelectorOnMainThread:@selector(backgroundSaveProcessDidFail:) withObject:currentRequest waitUntilDone:NO];
            else
                [self performSelectorOnMainThread:@selector(backgroundSaveProcessDidSucceed:) withObject:currentRequest waitUntilDone:NO];

            // clear out the request
            currentRequest = nil;
        }
    }];
}

- (void)backgroundSaveProcessDidFail:(VS_CoreDataRequest *)request
{
    if (request.error)
        [self logError:request.error];

    if (request.completionBlock)
    {
        void (^saveCompletionBlock)(NSArray *, NSError *) = request.completionBlock;
        saveCompletionBlock(nil, request.error);
    }
}

- (void)backgroundSaveProcessDidSucceed:(VS_CoreDataRequest *)request
{
    // get objects from main thread
    NSArray *objects = nil;
    if (request.objects.count > 0)
    {
        NSFetchRequest *fetchReq = [NSFetchRequest fetchRequestWithEntityName:[NSString stringWithFormat:@"%@", request.managedObjectClass]];
        fetchReq.predicate = [NSPredicate predicateWithFormat:@"self IN %@", request.objects];
        objects = [self executeFetchRequest:fetchReq];
    }

    // call the completion block
    if (request.completionBlock)
    {
        void (^saveCompletionBlock)(NSArray *, NSError *) = request.completionBlock;
        saveCompletionBlock(objects, nil);
    }
}

- (NSManagedObjectContext *)managedObjectContext
{
    if (_managedObjectContext != nil) {
        return _managedObjectContext;
    }

    NSPersistentStoreCoordinator *coordinator = [self persistentStoreCoordinator];
    if (coordinator != nil)
    {
        // create the MOC for the backgroumd thread
        _backgroundManagedObjectContext = [[NSManagedObjectContext alloc] initWithConcurrencyType:NSPrivateQueueConcurrencyType];
        [_backgroundManagedObjectContext setPersistentStoreCoordinator:coordinator];
        [_backgroundManagedObjectContext setMergePolicy:NSMergeByPropertyStoreTrumpMergePolicy];
        _backgroundManagedObjectContext.undoManager = nil;

        // create the MOC for the main thread
        _managedObjectContext = [[NSManagedObjectContext alloc] initWithConcurrencyType:NSMainQueueConcurrencyType];
        [_managedObjectContext setParentContext:_backgroundManagedObjectContext];
        [_managedObjectContext setMergePolicy:NSMergeByPropertyStoreTrumpMergePolicy];
    }

    return _managedObjectContext;
}

Do any of you have any idea why this crash is happening?

** EDIT **

Ladies and gents I have found the solution. Apparently, even when using nested contexts you still have to merge the changes via the NSManagedObjectContextDidSaveNotification. Once I added that into my code it all started to work perfectly. Below is my working code. Thanks a million guys!

- (NSManagedObjectContext *)managedObjectContext
{
    if (_managedObjectContext != nil)
        return _managedObjectContext;

    NSPersistentStoreCoordinator *coordinator = [self persistentStoreCoordinator];
    if (coordinator != nil)
    {
        // create the MOC for the backgroumd thread
        _backgroundManagedObjectContext = [[NSManagedObjectContext alloc] initWithConcurrencyType:NSPrivateQueueConcurrencyType];
        [_backgroundManagedObjectContext setPersistentStoreCoordinator:coordinator];
        [_backgroundManagedObjectContext setMergePolicy:NSMergeByPropertyStoreTrumpMergePolicy];
        _backgroundManagedObjectContext.undoManager = nil;

        // create the MOC for the main thread
        _managedObjectContext = [[NSManagedObjectContext alloc] initWithConcurrencyType:NSMainQueueConcurrencyType];
        [_managedObjectContext setParentContext:_backgroundManagedObjectContext];
        [_managedObjectContext setMergePolicy:NSMergeByPropertyStoreTrumpMergePolicy];

        [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(mergeChangesFromContextDidSaveNotification:) name:NSManagedObjectContextDidSaveNotification object:_backgroundManagedObjectContext];
    }

    return _managedObjectContext;
}

- (void)saveJsonObjects:(NSDictionary *)jsonDict
          objectMapping:(VS_ObjectMapping *)objectMapping
                  class:(__unsafe_unretained Class)managedObjectClass
             completion:(void (^)(NSArray *objects, NSError *error))completion
{
    // perform save on background thread
    [_backgroundManagedObjectContext performBlock:^(void)
    {
        // create a new process object and add it to the dictionary
        VS_CoreDataRequest *currentRequest = [[VS_CoreDataRequest alloc] init];
        currentRequest.managedObjectClass = managedObjectClass;

        // save the JSON dictionary starting at the upper most level of the key path
        NSArray *objects = [self saveJSON:jsonDict objectMapping:objectMapping class:managedObjectClass managedObjectContext:_backgroundManagedObjectContext level:0];

        if (objects.count == 0)
        {
            currentRequest.error = [self createErrorWithErrorCode:100 description:@"No objects were found for the object mapping"];
            [self performSelectorOnMainThread:@selector(backgroundSaveProcessDidFail:) withObject:currentRequest waitUntilDone:NO];
        }
        else
        {
            // save the objects so we can access them later to be re-fetched and returned on the main thread
            [currentRequest.objects addObjectsFromArray:objects];

            // save the object IDs and the completion block to global variables so we can access them after the save
            currentRequest.completionBlock = completion;

            [_backgroundManagedObjectContext lock];

            @try
            {
                [_backgroundManagedObjectContext processPendingChanges];
                [_backgroundManagedObjectContext save:nil];
            }
            @catch (NSException *exception)
            {
                currentRequest.error = [self createErrorWithErrorCode:100 description:exception.reason];
            }

            [_backgroundManagedObjectContext unlock];

            // complete the process on the main thread
            if (currentRequest.error)
                [self performSelectorOnMainThread:@selector(backgroundSaveProcessDidFail:) withObject:currentRequest waitUntilDone:NO];
            else
                [self performSelectorOnMainThread:@selector(backgroundSaveProcessDidSucceed:) withObject:currentRequest waitUntilDone:NO];

            // clear out the request
            currentRequest = nil;
        }
    }];
}

- (void)mergeChangesFromContextDidSaveNotification:(NSNotification *)notification
{
    [_managedObjectContext mergeChangesFromContextDidSaveNotification:notification];
}

- (void)backgroundSaveProcessDidFail:(VS_CoreDataRequest *)request
{
    if (request.error)
        [self logError:request.error];

    if (request.completionBlock)
    {
        void (^saveCompletionBlock)(NSArray *, NSError *) = request.completionBlock;
        saveCompletionBlock(nil, request.error);
    }
}

- (void)backgroundSaveProcessDidSucceed:(VS_CoreDataRequest *)request
{
    // get objects from main thread
    NSArray *objects = nil;
    if (request.objects.count > 0)
    {
        NSFetchRequest *fetchReq = [NSFetchRequest fetchRequestWithEntityName:[NSString stringWithFormat:@"%@", request.managedObjectClass]];
        fetchReq.predicate = [NSPredicate predicateWithFormat:@"self IN %@", request.objects];
        objects = [self executeFetchRequest:fetchReq];
    }

    // call the completion block
    if (request.completionBlock)
    {
        void (^saveCompletionBlock)(NSArray *, NSError *) = request.completionBlock;
        saveCompletionBlock(objects, nil);
    }
}

OTHER TIPS

You might want to consider this kind of context architecture (untested code):
The mainManagedObjectContext will be initialised in the same manner but with NSMainQueueConcurrencyType and no observer (by the way, remember to remove your observer)

- (void) mergeChangesFromBackground:(NSNotification*)notification
{
    __block __weak NSManagedObjectContext* context = self.managedObjectContext;
    [context performBlockAndWait:^{
        [context mergeChangesFromContextDidSaveNotification:notification];
    }];
}

- (NSManagedObjectContext*) bgContext
{
    if (_bgContext) {
        return _bgContext;
    }

    NSPersistentStoreCoordinator* coordinator = [self persistentStoreCoordinator];
    if (!coordinator) {
        return nil;
    }
    _bgContext = [[NSManagedObjectContext alloc] initWithConcurrencyType:NSPrivateQueueConcurrencyType];
    [_bgContext setPersistentStoreCoordinator:coordinator];
    [_bgContext setMergePolicy:NSMergeByPropertyStoreTrumpMergePolicy];
    [_bgContext setUndoManager:nil];
    [[NSNotificationCenter defaultCenter] addObserver:self
                                             selector:@selector(mergeChangesFromBackground:)
                                                 name:NSManagedObjectContextDidSaveNotification
                                               object:_bgContext];
    return _bgContext;
}

- (void) doSomethingOnBGContextWithoutBlocking:(void(^)(NSManagedObjectContext*))contextBlock
{
    __block __weak NSManagedObjectContext* context = [self bgContext];
    [context performBlock:^{
        NSThread *currentThread = [NSThread currentThread];
        NSString* prevName = [currentThread name];
        [currentThread setName:@"BGContextThread"];

        contextBlock(context);

        [context reset];
        [currentThread setName:prevName];
    }];
}
Licensed under: CC-BY-SA with attribution
Not affiliated with StackOverflow
scroll top