核心数据在后台问题中保存对象

时间:2012-10-02 14:44:58

标签: ios core-data grand-central-dispatch

我正在尝试简单地说,我正在使用后台队列将从Web服务中提取的JSON对象保存到Core Data Sqlite3数据库。保存发生在我通过GCD创建的序列化后台队列中,并保存到为该后台队列创建的NSManagedObjectContext的辅助实例中。保存完成后,我需要使用新创建/更新的对象更新主线程上的NSManagedObjectContext实例。我遇到的问题是主线程上的NSManagedObjectContext实例无法找到保存在后台上下文中的对象。以下是我正在使用代码示例执行的操作列表。对我做错了什么的想法?

  • 通过GCD创建后台队列,运行所有预处理逻辑,然后在该线程上保存后台上下文:

// process in the background queue
dispatch_async(backgroundQueue, ^(void){

    if (savedObjectIDs.count > 0) {
        [savedObjectIDs removeAllObjects];
    }
    if (savedObjectClass) {
        savedObjectClass = nil;
    }

    // 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) {
            _backgroundQueueManagedObjectContext = [[NSManagedObjectContext alloc] init];
            [_backgroundQueueManagedObjectContext setPersistentStoreCoordinator:coordinator];
        }
    }

    // save the JSON dictionary starting at the upper most level of the key path, and return all created/updated objects in an array
    NSArray *objectIds = [self saveJSON:jsonDict objectMapping:objectMapping class:managedObjectClass managedObjectContext:_backgroundQueueManagedObjectContext level:0];

    // save the object IDs and the completion block to global variables so we can access them after the save
    if (objectIds) {
        [savedObjectIDs addObjectsFromArray:objectIds];
    }
    if (completion) {
        saveCompletionBlock = completion;
    }
    if (managedObjectClass) {
        savedObjectClass = managedObjectClass;
    }

    // save all changes object context
    [self saveManagedObjectContext];
});
  • “saveManagedObjectContext”方法基本上查看正在运行的线程并保存适当的上下文。我已经确认此方法工作正常,因此我不会在此处放置代码。

  • 所有这些代码都驻留在一个单例中,在单例的init方法中,我为“NSManagedObjectContextDidSaveNotification”添加了一个监听器,它调用了mergeChangesFromContextDidSaveNotification:方法

// merge changes from the context did save notification to the main context
- (void)mergeChangesFromContextDidSaveNotification:(NSNotification *)notification
{
    NSThread *currentThread = [NSThread currentThread];

    if ([currentThread.name isEqual:VS_CORE_DATA_MANAGER_BACKGROUND_THREAD_NAME]) {

        // merge changes to the primary context, and wait for the action to complete on the main thread
        [_managedObjectContext performSelectorOnMainThread:@selector(mergeChangesFromContextDidSaveNotification:) withObject:notification waitUntilDone:YES];

        // on the main thread fetch all new data and call the completion block
        dispatch_async(dispatch_get_main_queue(), ^{

            // get objects from the database
            NSMutableArray *objects = [[NSMutableArray alloc] init];
            for (id objectID in savedObjectIDs) {
                NSError *error;
                id object = [_managedObjectContext existingObjectWithID:objectID error:&error];
                if (error) {
                    [self logError:error];
                } else if (object) {
                    [objects addObject:object];
                }
            }

            // remove all saved object IDs from the array
            [savedObjectIDs removeAllObjects];
            savedObjectClass = nil;

            // call the completion block
            //completion(objects);
            saveCompletionBlock(objects);

            // clear the saved completion block
            saveCompletionBlock = nil;
        });
    }
}

正如您在上面的方法中所看到的,我在主线程上调用“mergeChangesFromContextDidSaveNotification:”,并且我已将操作设置为等到完成。根据apple文档,后台线程应该等到该操作完成后再继续该调用下面的其余代码。正如我上面提到的,一旦我运行此代码,一切似乎都有效,但是当我尝试将获取的对象打印到控制台时,我没有得到任何回报。似乎合并实际上没有发生,或者可能在我的其余代码运行之前没有完成。是否有其他通知我应该监听以确保合并已完成?或者我需要在合并之后但在fecth之前保存主对象上下文?

另外,我为错误的代码格式化道歉,但似乎SO的代码标签不喜欢方法定义。

谢谢你们!

更新:

我已经做了下面推荐的更改,但仍然遇到了同样的问题。以下是我的更新代码。

这是调用后台线程保存过程的代码

// process in the background queue
dispatch_async(backgroundQueue, ^(void){

    if (savedObjectIDs.count > 0) {
        [savedObjectIDs removeAllObjects];
    }
    if (savedObjectClass) {
        savedObjectClass = nil;
    }

    // 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) {
            _backgroundQueueManagedObjectContext = [[NSManagedObjectContext alloc] init];
            [_backgroundQueueManagedObjectContext setPersistentStoreCoordinator:coordinator];
        }
    }

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

    // save the object IDs and the completion block to global variables so we can access them after the save
    if (objectIds) {
        [savedObjectIDs addObjectsFromArray:objectIds];
    }
    if (completion) {
        saveCompletionBlock = completion;
    }
    if (managedObjectClass) {
        savedObjectClass = managedObjectClass;
    }

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

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

这是由NSManagedObjectContextDidSaveNotification通知调用的代码

    // merge changes from the context did save notification to the main context
- (void)mergeChangesFromBackground:(NSNotification *)notification
{
    // kill the listener
    [[NSNotificationCenter defaultCenter] removeObserver:self name:NSManagedObjectContextDidSaveNotification object:_backgroundQueueManagedObjectContext];

    NSThread *currentThread = [NSThread currentThread];

    // merge changes to the primary context, and wait for the action to complete on the main thread
    [[self managedObjectContext] performSelectorOnMainThread:@selector(mergeChangesFromContextDidSaveNotification:) withObject:notification waitUntilDone:YES];

    // dispatch the completion block
    dispatch_async(dispatch_get_main_queue(), ^{

        // get objects from the database
        NSMutableArray *objects = [[NSMutableArray alloc] init];
        for (id objectID in savedObjectIDs) {
            NSError *error;
            id object = [[self managedObjectContext] existingObjectWithID:objectID error:&error];
            if (error) {
                [self logError:error];
            } else if (object) {
                [objects addObject:object];
            }
        }

        // remove all saved object IDs from the array
        [savedObjectIDs removeAllObjects];
        savedObjectClass = nil;

        // call the completion block
        //completion(objects);
        saveCompletionBlock(objects);

        // clear the saved completion block
        saveCompletionBlock = nil;
    });
}

更新:

所以我找到了解决方案。事实证明,我在后台线程上保存对象ID然后尝试在主线程上使用它们来重新获取它们的方式并没有成功。所以我最终从userInfo字典中提取插入/更新的对象,该字典与NSManagedObjectContextDidSaveNotification通知一起发送。以下是我现在正在使用的更新代码。

如前所述,此代码启动预先存储和保存逻辑

// process in the background queue
dispatch_async(backgroundQueue, ^(void){

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

    [self logMessage:[NSString stringWithFormat:@"(%@) saveJSONObjects:objectMapping:class:completion:", [managedObjectClass description]]];

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

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

    // save the object IDs and the completion block to global variables so we can access them after the save
    if (completion) {
        saveCompletionBlock = completion;
    }

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

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

这是处理NSManagedObjectContextDidSaveNotification

的修改方法
- (void)mergeChangesFromBackground:(NSNotification *)notification
{
    // kill the listener
    [[NSNotificationCenter defaultCenter] removeObserver:self name:NSManagedObjectContextDidSaveNotification object:_backgroundQueueManagedObjectContext];

    // merge changes to the primary context, and wait for the action to complete on the main thread
    [[self managedObjectContext] performSelectorOnMainThread:@selector(mergeChangesFromContextDidSaveNotification:) withObject:notification waitUntilDone:YES];

    // dispatch the completion block
    dispatch_async(dispatch_get_main_queue(), ^{

        // pull the objects that were saved from the notification so we can get them on the main thread MOC
        NSDictionary *userInfo = [notification userInfo];
        NSMutableArray *modifiedObjects = [[NSMutableArray alloc] init];
        NSSet *insertedObject = (NSSet *)[userInfo objectForKey:@"inserted"];
        NSSet *updatedObject = (NSSet *)[userInfo objectForKey:@"updated"];

        if (insertedObject && insertedObject.count > 0) {
            [modifiedObjects addObjectsFromArray:[insertedObject allObjects]];
        }
        if (updatedObject && updatedObject.count > 0) {
            [modifiedObjects addObjectsFromArray:[updatedObject allObjects]];
        }

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

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

        modifiedObjects = nil;

        // call the completion block
        saveCompletionBlock(objects);

        // clear the saved completion block
        saveCompletionBlock = nil;
    });
}

3 个答案:

答案 0 :(得分:25)

我要把它扔出去。 停止遵循“核心数据编程指南”中列出的最佳并发实践。由于添加了更容易使用的嵌套上下文,因此Apple尚未对其进行更新。此视频详细介绍:https://developer.apple.com/videos/wwdc/2012/?id=214

设置主要上下文以使用主线程(适用于处理UI):

NSManagedObjectContext * context = [[NSManagedObjectContext alloc] initWithConcurrencyType:NSMainQueueConcurrencyType];
[context setPersistentStoreCoordinator:yourPSC];

对于您创建的可能正在执行并发操作的任何对象,请创建要使用的专用队列上下文

NSManagedObjectContext * backgroundContext = [[NSManagedObjectContext alloc] initWithConcurrencyType:NSPrivateQueueConcurrencyType];
[backgroundContext setParentContext:context];
//Use backgroundContext to insert/update...
//Then just save the context, it will automatically sync to your primary context
[backgroundContext save:nil];

QueueConcurrencyType指的是上下文将对其进行获取(保存和获取请求)操作的队列。 NSMainQueueConcurrencyType上下文在主队列上完成所有工作,这使其适用于UI交互。 NSPrivateQueueConcurrencyType在它自己的专用队列上执行。因此,当您在backgroundContext上调用save时,它会自动合并使用performBlock调用parentContext的私有数据。您不希望在私有队列上下文中调用performBlock,以防它恰好位于将导致死锁的主线程上。

如果你想变得非常花哨,你可以创建一个主要上下文作为私有队列并发类型(适用于后台保存),只有你的UI的主队列上下文,然后你的主队列上下文的子上下文后台操作(如进口)。

答案 1 :(得分:7)

我看到你找到了一个适合你的答案。但我一直有类似的问题,想分享我的经验,看看它对你或其他人是否有帮助。

多线程核心数据的内容总是让人感到有些困惑,所以如果我误读你的代码,请原谅。但似乎可能有一个更简单的答案。

您在第一次尝试时遇到的核心问题是您将托管对象ID(可能是可在线程之间传递的对象标识符)保存到全局变量以供主线程使用。你在后台线程上做到了这一点。问题是您在保存到后台线程的托管对象上下文之前执行了此操作。在保存之前,对象ID传递给另一个线程/上下文对是不安全的。你保存时他们可以改变。请参阅objectID文档中的警告:NSManagedObject reference

您通过通知后台线程保存来修复此问题,并在该线程内部,从通知对象中获取现在安全使用 - 因为上下文已保存的对象ID。这些被传递给主线程,并且实际更改也通过调用mergeChangesFromContextDidSaveNotification合并到主线程中。这是您可以保存一两步的地方。

您正在注册以在后台主题上听到NSManagedObjectContextDidSaveNotification。您可以注册以在主题上听到相同的通知。在该通知中,您将拥有可在主线程上安全使用的相同对象ID。可以使用mergeChangesFromContextDidSaveNotification和传递的通知对象安全地更新主线程MOC,因为该方法旨在以这种方式工作:mergeChanges docs。只要将moc与调用完成块的线程匹配,就可以从任一线程调用完成块现在是安全的。

因此,您可以在主线程上执行所有主线程更新,完全分离线程,避免必须打包和重新打包更新的内容,或者对持久存储执行相同更改的双重保存。

要清楚 - 发生的合并是在托管对象上下文及其内存状态 - 主线程上的moc更新以匹配后台线程上的moc,但是因为您不需要新的保存ALREADY将这些更改保存到后台线程上的存储中。您可以通过线程安全访问通知对象中的任何更新对象,就像在后台线程中使用它时一样。

我希望你的解决方案对你有用,而且你不必重新考虑因素 - 但是想为那些可能会看到这个问题的人添加我的想法。如果我误解了您的代码,请告诉我,我会修改。

答案 2 :(得分:4)

在你的情况下,因为你写的后台moc mergeChangesFromContextDidSaveNotification的通知将在后台moc上,而不是前景moc。

因此您需要在后台线程中注册后台moc对象的通知。

当你收到那个电话时,你可以向主线程moc发送一条消息给mergeChangesFromContextDidSaveNotification。

安德鲁

更新: 这是一个应该起作用的样本

    //register for this on the background thread
    NSNotificationCenter *nc = [NSNotificationCenter defaultCenter];
    [nc addObserver:self selector:@selector(mergeChanges:) name:NSManagedObjectContextDidSaveNotification object:backgroundMOC];

- (void)mergeChanges:(NSNotification *)notification {
    NSManagedObjectContext *mainThreadMOC = [singleton managedObjectContext];

    //this tells the main thread moc to run on the main thread, and merge in the changes there
    [mainThreadMOC performSelectorOnMainThread:@selector(mergeChangesFromContextDidSaveNotification:) withObject:notification waitUntilDone:YES];
}