使用 mergeChangesFromContextDidSaveNotification 时 CoreData 无法完成错误

Posted

技术标签:

【中文标题】使用 mergeChangesFromContextDidSaveNotification 时 CoreData 无法完成错误【英文标题】:CoreData could not fulfill a fault when using mergeChangesFromContextDidSaveNotification 【发布时间】:2014-11-20 01:40:44 【问题描述】:

我在 MagicalRecord 的这一行收到 uncaught exception 'NSObjectInaccessibleException', reason: 'CoreData could not fulfill a fault for '0x9f481f0 <x-coredata://ABF084FE-4BF3-4FC3-918A-BFF043589B8A/Structure/p21316>'

[[self MR_defaultContext] mergeChangesFromContextDidSaveNotification:notification];

我已经调试了 2 天,但仍然不确定发生了什么。

我已将其缩小到我的导入代码部分,该部分删除不只是导入的每个对象(给定实体的)(因此删除旧对象)。它看起来像这样:

- (void)deleteNonImportedEntities

    NSString *entityName = [self.configuration entityName];
    NSPredicate *predicate = [NSPredicate predicateWithFormat:@"NOT (myID IN %@)", self.resourceIds];

    [MagicalRecord saveWithBlockAndWait:^(NSManagedObjectContext *localContext) 
        [localContext MR_setWorkingName:@"deleteNonImportedEntities context"];
        [NSClassFromString(entityName) MR_deleteAllMatchingPredicate:predicate inContext:localContext];
    ];

非常直接。这个错误的问题是它只发生 10 次左右。如果我在这里输入了太多的登录信息,那么它发生的次数就更少了。但我一直在努力缩小范围。我更改了 saveWithBlockAndWait 方法,以显示在该上下文中插入 (i)、更新 (u) 或删除 (d) 的对象数量。

+ (void) saveWithBlockAndWait:(void(^)(NSManagedObjectContext *localContext))block;

    NSManagedObjectContext *savingContext  = [NSManagedObjectContext MR_rootSavingContext];
    NSManagedObjectContext *localContext = [NSManagedObjectContext MR_contextWithParent:savingContext];

    [localContext performBlockAndWait:^
        [localContext MR_setWorkingName:NSStringFromSelector(_cmd)];

        if (block) 
            block(localContext);
        
        MRLogVerbose(@"Saving saveWithBlockAndWait. (i: %i, u: %i, d: %d)", [[localContext insertedObjects] count], [[localContext updatedObjects] count], [[localContext deletedObjects] count]);
        [localContext MR_saveWithOptions:MRSaveParentContexts|MRSaveSynchronously completion:nil];
        MRLogVerbose(@"Finished saveWithBlockAndWait");
    ];

我还更改了rootContextDidSave(因为这是发生异常的地方),因此它会提供应该从上述保存发送的通知中的信息(一旦它上升到根保存上下文)。

+ (void) rootContextDidSave:(NSNotification *)notification

    if ([notification object] != [self MR_rootSavingContext])
    
        return;
    

    if ([NSThread isMainThread] == NO)
    
        dispatch_async(dispatch_get_main_queue(), ^
            [self rootContextDidSave:notification];
        );

        return;
    

    int inserted = [[[notification userInfo] objectForKey:@"inserted"] count];
    int updated = [[[notification userInfo] objectForKey:@"updated"] count];
    int deleted = [[[notification userInfo] objectForKey:@"deleted"] count];
    MRLogVerbose(@"Merging changes from notification to the default context (NSManagedObjectContext+MagicalRecord.m) (i: %i, u: %i, d: %i)", inserted, updated, deleted);
    [[self MR_defaultContext] mergeChangesFromContextDidSaveNotification:notification];

当代码没有崩溃时,日志如下所示:

... [278:21433] Saving saveWithBlockAndWait. (i: 0, u: 0, d: 9)
... [278:21433] → Saving <NSManagedObjectContext (0x191f8b30): deleteNonImportedEntities context> on a background thread
... [278:21433] → Save Parents? YES
... [278:21433] → Save Synchronously? YES
... [278:21433] → Saving <NSManagedObjectContext (0x1558d6e0): MagicalRecord Root Saving Context> on a background thread
... [278:21433] → Save Parents? YES
... [278:21433] → Save Synchronously? YES
... [278:21187] Merging changes from notification to the default context (NSManagedObjectContext+MagicalRecord.m) (i: 0, u: 13, d: 9)
... [278:21433] → Finished saving: <NSManagedObjectContext (0x1558d6e0): MagicalRecord Root Saving Context> on a background thread
... [278:21433] Finished saveWithBlockAndWait

我不确定为什么更新号会随着上下文的变化而增加。

以下是它的终止时间:

... [284:22234] Saving saveWithBlockAndWait. (i: 0, u: 0, d: 8)
... [284:22234] → Saving <NSManagedObjectContext (0xa062210): deleteNonImportedEntities context> on a background thread
... [284:22234] → Save Parents? YES
... [284:22234] → Save Synchronously? YES
... [284:22234] → Saving <NSManagedObjectContext (0x17df7b60): MagicalRecord Root Saving Context> on a background thread
... [284:22234] → Save Parents? YES
... [284:22234] → Save Synchronously? YES
... [284:22234] → Finished saving: <NSManagedObjectContext (0x17df7b60): MagicalRecord Root Saving Context> on a background thread
... [284:22234] Finished saveWithBlockAndWait
All Exceptions - Breakpoint
... [284:22200] *** Terminating app due to uncaught exception 'NSObjectInaccessibleException', reason: 'CoreData could not fulfill a fault for '0x9f481f0 <x-coredata://ABF084FE-4BF3-4FC3-918A-BFF043589B8A/Structure/p21316>''
*** First throw call stack:
(0x2ae8a49f 0x389bec8b 0x2aba47dd 0x2aba3bd1 0x2aba3a35 0x2abb261d 0x2bb118c9 0x2bb1148b 0x2bb11249 0x2bb11001 0x2abb8ac5 0x2abb7cc1 0x2ac8b103 0x2ac187ad 0x2ac1894f 0x2abb7b5d 0x2ae42c61 0x2ad9e6d5 0x2bad0189 0x2abb7acf 0x2ac18433 0x2ac1864d 0x9fca3 0x9fcfb 0xc1f9db 0xc1f9c7 0xc233ed 0x2ae503b1 0x2ae4eab1 0x2ad9c3c1 0x2ad9c1d3 0x321310a9 0x2e3abfa1 0x7e821 0x38f3eaaf)
libc++abi.dylib: terminating with uncaught exception of type _NSCoreDataException

如何正确调试此问题?似乎在调用 delete 方法之前发生的导入工作正常,并且被合并到默认上下文中。为什么在发送通知时插入/更新/删除的内容会发生变化?

我没有任何视图正在监视要更改的数据。而且我检查了我拥有的其他 NSFetchedResultsControllers,我不相信他们被解雇了(我也有日志声明)。

更新:查看发送到链上的通知。当该方法第一次被调用(并且它不在主线程中)时,它只是删除和更新了一些。但是当它到达主线程时,它只是一个巨大的插入列表。在第一个通知中删除的那些在第二个通知中是错误的。现在只是想弄清楚它们为什么不同。

【问题讨论】:

关于这些日志的奇怪之处在于,第二个日志不包含“正在合并通知中的更改...”,这让我怀疑它在尝试合并之前是否崩溃了。 嘿汤姆。我正在使用 CocoaLumberjack 进行日志记录。我只是认为问题在于它需要经过另一个运行周期才能打印该日志消息。你认为不是这样吗?如果优先级更高,我可以将其更改为 NSLog 并重试。 我输入了 NSLog,但它仍然没有打印出来。但这就是异常停止的地方。我不知道如何判断它是否会在合并之前崩溃,而不是查看其他线程上发生的事情并且那里没有发生任何 CoreData。 取出 mergeChangesFromContextDidSaveNotification 可以防止它崩溃(据我所知)。但显然 UI 依赖默认上下文更新的部分不起作用。 添加异常断点会很有用,这样您就可以在异常发生时暂停执行。然后查看回溯,看看它通向哪里。 【参考方案1】:

我已经诊断出这个问题,它与比赛条件有关。

MagicalRecord 的小背景。它使用所有其他上下文用作父级的“保存”上下文。 “默认”上下文是用于 UI 的上下文,因此它是侦听通知的上下文,因此它可以合并更改。当您创建另一个上下文(或使用 MagicalRecord 的保存块)时,它将保存上下文设置为父母。

问题是我的导入类中有两个主要方法。一种是根据 JSON 数据(已保存到文件)导入所有对象,另一种是删除不只是导入的对象。以下是每个的基本概念:

- (void)importEntities

    ...
    [saveResources readJSONResponsesFromDisk:^(NSDictionary *response) 
        [MagicalRecord saveWithBlockAndWait:^(NSManagedObjectContext *localContext) 
            NSArray *resources = [response objectForKey:JSONkey];

            NSArray *array = [NSClassFromString(entityName) MR_importFromArray:resources inContext:localContext];

            [weakSelf.resourceIds addObjectsFromArray:[array valueForKey:@"myID"]];
        ];
    ];


- (void)deleteNonImportedEntities

    ...
    NSPredicate *predicate = [NSPredicate predicateWithFormat:@"NOT (myID IN %@)", self.resourceIds];

    [MagicalRecord saveWithBlockAndWait:^(NSManagedObjectContext *localContext) 
        [NSClassFromString(entityName) MR_deleteAllMatchingPredicate:predicate inContext:localContext];
    ];

现在我每次使用saveWithBlockAndWait 的最初目的是因为内存问题。它循环的每个文件中可能有 500 个实体。由于 MagicalRecord 处理导入的方式,它们每个都可能创建多个其他对象。因此,我想尽可能频繁地从内存中刷新数据(并保存到磁盘),这样它就不会堆积太多。

在了解问题的核心之前,需要了解数据在 CoreData 中的保存方式。由于saveWithBlockAndWait 设置的上下文具有 Saving 上下文的父级,因此在保存时,其更改会自动合并到 Saving 上下文中。然后 Saving 保存上下文发送它的NSManagedObjectContextDidSaveNotification(子上下文也发送它,但默认上下文忽略它)。但是,在默认上下文合并从保存上下文发送的更改之前,deleteNonImportedEntities 会运行并保存。因此,它会在默认上下文之前保存到 Saving 上下文(大约 15 次中的 1 次)。因此,当默认上下文确实尝试合并这些更改时,通知指向的某些对象不会出错(它们已在保存上下文中被删除)。因此它失败了。

现在我知道了,我已经取消了saveWithBlockAndWait 的调用,并且整个班级只使用了一个localContext。我想我得重新使用自动释放池并定期保存。

更新:实际上,这会导致同样的问题。所以我正在尝试找出另一种解决方案。

更新 2:我最终采纳了 Aaron 的以下建议并将其转移到自己的操作中。我还更改了优先级以帮助将它们分开。

【讨论】:

一些想法。首先,在这两种方法中,您都使用self.localContext,我假设您已将其声明为属性。您应该改用 MR 块提供的常规 localContext。其次,您是否总是在-importedEntities返回后调用-deleteNonImportedEntities?如果是这样,听起来您可以使用操作并使用依赖项或将删除设置为完成块。 或者甚至只是在你的-importEntities方法中调用-deleteNonImportedEntities,就在[weakSelf.resourceIds addObjectsFromArray:[array valueForKey:@"myID"]];之后 感谢@AaronA 的反馈。我在更改代码的过程中复制了代码,所以是的,之前是localContext。 90% 的时间我都会打电话给deleteNonImportedEntities(我在 if 语句下有它)。我不能在addObjectsFromArray 之后立即调用它,因为我可能会循环x 文件数,从而多次调用该方法。我必须等到这些都完成后才能调用它。由于内存问题,我无法将 MR 块放在文件周围。我想在导入时经常清除上下文。 使用单独的操作确实有帮助吗?这不仅改善了问题吗?在默认上下文合并之前调用并保存新操作仍然可能发生,对吧? 默认上下文应该在程序的下一个或两个周期合并,而操作应该需要更长的时间。也就是说,是的,它仍有可能发生,但可能性要小得多。【参考方案2】:

我也在使用MagicalRecord,最近遇到了类似的问题。

CoreData:警告:NSManagedObjectContext 委托覆盖错误 处理行为以静默删除具有 ID 的对象 '0xd0000000071c0006 ' 并将所有属性值替换为 nil/0 而不是抛出。

我的核心数据栈有点复杂,根是 NSPersistentStoreCoordinator。右侧是我的应用程序的主要嵌套堆栈。

MR_rootSavingContext,后台上下文,用于写入磁盘 MR_defaultContext,用于主线程 UI,例如NSFetchedResultsController MyWorkerContext,后台上下文,用于核心数据从 API 逻辑写入的入口点

BackgroundContext1 或 BackgroundContext2 是我实现一些海量数据操作的计划,这些操作应在后台线程上进行。

请注意红线显示 BackgroundContext1 监听 MyWorkerContext 并合并更改,这将导致错误。原因是当 BackgroundContext1 保存并发布有关更改的通知时,更改尚未传播到磁盘。 BackgroundContext1 没有数据,因此无法实现 Faulting。

我的建议是 NSManagedObjectContext 应该只合并来自同级别的其他 NSManagedObjectContext 的更改 或父母。例如。以下两种情况都可以:

BackgroundContext1 监听并合并来自 MR_rootSavingContext 的更改 BackgroundContext1 监听并合并来自 MyWorkerContext 的更改

【讨论】:

以上是关于使用 mergeChangesFromContextDidSaveNotification 时 CoreData 无法完成错误的主要内容,如果未能解决你的问题,请参考以下文章

在使用加载数据流步骤的猪中,使用(使用 PigStorage)和不使用它有啥区别?

今目标使用教程 今目标任务使用篇

Qt静态编译时使用OpenSSL有三种方式(不使用,动态使用,静态使用,默认是动态使用)

MySQL db 在按日期排序时使用“使用位置;使用临时;使用文件排序”

使用“使用严格”作为“使用强”的备份

Kettle java脚本组件的使用说明(简单使用升级使用)