6

简而言之,我正在尝试使用后台队列将从 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:”,并且我已将操作设置为等到完成。根据苹果文档,后台线程应该等到该操作完成,然后才能继续执行该调用下面的其余代码。正如我上面提到的,一旦我运行此代码,一切似乎都可以正常工作,但是当我尝试将获取的对象打印到控制台时,我什么也得不到。似乎合并实际上并没有发生,或者可能在我的其余代码运行之前没有完成。是否还有其他通知我应该收听以确保合并已完成?或者我是否需要在合并之后但在 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,然后尝试在主线程上使用它们重新获取它们的方式并没有奏效。所以我最终从与 NSManagedObjectContextDidSaveNotification 通知一起发送的 userInfo 字典中提取插入/更新的对象。以下是我现在正在运行的更新代码。

和之前一样,这段代码开始预处理和保存逻辑

// 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;
    });
}
4

3 回答 3

26

我要把这个扔出去。停止遵循 Core Data Programming Guide 中列出的并发最佳实践。自从添加了更易于使用的嵌套上下文以来,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,然后您的主队列上下文的子上下文用于后台操作(像进口一样)。

于 2013-01-25T17:15:55.040 回答
7

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

多线程核心数据的东西读起来总是有点混乱,所以如果我误读了你的代码,请原谅。但似乎对你来说可能有一个更简单的答案。

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

您通过通知后台线程保存并在该线程内从通知对象中获取 now-safe-to-use-because-the-context-has-been-saved 对象 ID 来解决此问题。这些被传递到主线程,并且实际的更改也通过调用mergeChangesFromContextDidSaveNotification 被合并到主线程中。在这里您可以节省一两步。

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

因此,您可以在主线程上执行所有主线程更新内容,干净地分离线程并避免打包和重新打包更新的内容或将相同的更改双重保存到持久存储中。

需要明确的是 - 发生的合并发生在托管对象上下文及其内存状态 - 主线程上的 moc 已更新以匹配后台线程上的 moc,但不需要新的保存,因为您已经保存了这些在后台线程上更改存储。您可以线程安全地访问通知对象中的任何这些更新对象,就像您在后台线程上使用它时所做的那样。

我希望您的解决方案对您有用,并且您不必重新考虑-但想为可能看到此问题的其他人添加我的想法。如果我误解了您的代码,请告诉我,我会修改。

于 2013-01-25T16:43:40.350 回答
5

在您的情况下,因为您写入后台 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];
}
于 2012-10-02T14:54:07.143 回答