NSFetchedResultsController 在同一持久存储的后台更新时提供表视图导致死锁

Posted

技术标签:

【中文标题】NSFetchedResultsController 在同一持久存储的后台更新时提供表视图导致死锁【英文标题】:NSFetchedResultsController feeding table view while background update of same persistent store causes deadlock 【发布时间】:2014-07-30 21:10:33 【问题描述】:

仍在努力将应用从每次使用或显示时下载信息转换为使用 CoreData 将其缓存到手机上(由 MagicalRecord 提供)。这是在 ios 7 上

因为我们没有设置数据推送系统来在后端发生某些数据更改时自动更新手机的缓存数据,所以过去几个月我一直在考虑(因为我们在处理应用程序)如何管理在手机上保存数据的本地副本并能够在缓存中拥有最新的数据。

我意识到,只要我仍然每次都获取数据:-(我可以使用手机的CoreData支持的数据缓存来显示和使用,并且只需使用数据的获取来更新手机数据库。

所以我一直在将主要数据对象从构成完整对象的下载数据转换为这些主要数据对象作为 CoreData 对象的轻型替代对象。

基本上,应用程序中的每个普通数据对象,而不是在内部包含对象的所有属性,而是仅包含底层CoreData对象的objectID,并且可能在内部包含应用程序特定的ID,并且所有其他属性都是动态的并且是获取的来自 CoreData 对象并传递(大多数属性是只读的,更新是通过从传入的 JSON 中批量重写核心数据来完成的)

像这样:

- (NSString *)amount

    __block NSString *result = nil;

    NSManagedObjectContext *localContext = [NSManagedObjectContext MR_newContext];

    [localContext performBlockAndWait:^
        FinTransaction  *transaction = (FinTransaction *)[localContext existingObjectWithID:[self objectID] error:nil];

        if (nil != transaction)
        
            result = [transaction.amount stringValue];
        
    ];

    return result;

有时需要设置一个,看起来像这样:

- (void)setStatus:(MyTransactionStatus)status

    [MagicalRecord saveWithBlock:^(NSManagedObjectContext *localContext) 
        FinTransaction *transaction = (FinTransaction *)[localContext existingObjectWithID:[self objectID] error:nil];

        if (nil != transaction)
        
            transaction.statusValue = status;
        

     completion:^(BOOL success, NSError *error)];

现在,我的问题是我有一个视图控制器,它基本上使用 NSFetchedResultsController 在表格视图中显示本地手机的 CoreData 数据库中存储的数据。在发生这种情况的同时,用户可能开始滚动浏览数据,手机会启动一个线程来下载数据的更新,然后开始使用更新的数据更新 CoreData 数据存储,此时它然后在主线程上运行异步 GCD 回调,让 fetched results controller 重新获取其数据,并告诉 table view 重新加载。

问题在于,如果用户滚动浏览初始获取结果控制器获取数据和表视图加载,并且后台线程在后台更新相同的 Core Data 对象,则会发生死锁。获取和重写的不是完全相同的实体(发生死锁时),即,不是对象 ID 1 被读取和写入,而是使用相同的持久数据存储。

每次访问,读取或写入,都发生在 MR_saveWithBlockMR_saveWithBlockAndWait(数据的写入/更新)中(视情况而定),以及 [localContext performBlock:] 或 [localContext performBlockAndWait:] .每个单独的读取或写入都有自己的NSManagedObjectContext。我没有看到任何地方有流浪的未决更改,它阻塞和死锁的实际位置并不总是相同的,但总是与从与后台线程相同的持久存储读取的主线程有关更新数据。

获取的结果控制器是这样创建的:

_frController = [[NSFetchedResultsController alloc] initWithFetchRequest:fetchRequest
                                                    managedObjectContext:[NSManagedObjectContext MR_rootSavingContext]
                                                      sectionNameKeyPath:sectionKeyPath
                                                               cacheName:nil];

然后performFetch 就完成了。

在需要在表格视图中显示范围数据并在后台使用新数据更新数据存储的情况下,如何最好地构建此类操作?

虽然我大部分时间都在使用 MagicalRecord,但我愿意接受使用 MagicalRecord 使用或不使用(直接 CD)的 cmets、答案等。

【问题讨论】:

代码编辑实际上使我更难阅读恕我直言,但这没关系。我的代码格式化风格是基于我在 80 年代后期在期刊上阅读的研究而开发的,自那时以来我通过使用对其进行了修改和扩展,并利用了我们人类阅读和处理信息的方式与狭窄和难以理解的方式阅读为处理历代流传下来的 80x24 或 80x25 字符终端而开发的样式……但我离题了。 :) 【参考方案1】:

所以我处理这个问题的方法是查看两个托管对象上下文,每个上下文都有自己的持久存储协调器。两个持久存储协调器都与磁盘上的同一个持久存储通信。

在 WWDC 2013 的第 211 节“核心数据性能优化和调试”中详细介绍了这种方法,您可以在 Apple's Developer Site for WWDC 2013 上获得该方法。

为了在 MagicalRecord 中使用这种方法,您需要考虑使用即将发布的 MagicalRecord 3.0 和 ClassicWithBackgroundCoordinatorSQLiteMagicalRecordStack(是的,这个名称需要工作!)。它实现了 WWDC 会议中概述的方法,但您需要注意您的项目需要进行更改以支持 MagicalRecord 3,而且它还没有完全发布。

基本上你最终得到的是:

1 x 主线程上下文:您使用它来填充您的 UI 和获取的结果控制器等。永远不要在此上下文中进行更改。 1 x 私有队列上下文:使用基于块的保存方法进行所有更改 - 它们会自动通过此上下文汇集并保存到磁盘。

我希望这是有道理的——一定要观看 WWDC 会议——他们使用一些很棒的动画图表来解释为什么这种方法更快(并且不应该像你现在使用的方法那样阻塞主线程)。

如果您需要,我很乐意提供更多详细信息。

【讨论】:

观看了视频。下载了当前工作的 MR 3。正在处理这个问题。谢谢! 使用ClassicWithBackgroundCoordinatorSQLiteMagicalRecordStack 我假设设置是setupClassicStackWithSQLiteStoreNamed:?是否有与 ClassicWithBackgroundCoordinatorSQLiteMagicalRecordStack 不同的 ClassicStack? ClassicWithBackgroundCoordinatorSQLiteMagicalRecordStack(天哪,这东西的名字!)是ClassicSQLiteMagicalRecordStack 的子类,所以是的,[ClassicWithBackgroundCoordinatorSQLiteMagicalRecordStack stackWithStoreNamed:@"Blah"]; 之类的东西将为您设置堆栈。请记住,在 MR3 中保持该堆栈是您的责任。还有其他接受路径、URL 和托管对象模型的堆栈工厂方法,因此您应该能够找到适合您需求的方法。 谢谢。昨天下午我开车回家的路上,为了更好地探索头文件和 .m 文件来解决这个问题,这让我大吃一惊。今天会有更多的时间想清楚。谢谢! 你有一些关于如何访问主线程上下文和私有队列上下文的示例代码吗? [NSManagedObjectContext MR_mainQueueContext][NSManagedObjectContext MR_privateQueueContext] 返回的新上下文没有连接到任何东西,据我通过查看代码可知。他们不知道我的实体(+entityForName: could not locate an NSManagedObjectModel for entity name 错误)。当你说我最终得到 1 x Main 和 1 x Private 时,这是概念性的吗?我真的根据需要得到一个给定类型的新的吗?还是真的有 2 个 MOC 被创建并用作父母?

以上是关于NSFetchedResultsController 在同一持久存储的后台更新时提供表视图导致死锁的主要内容,如果未能解决你的问题,请参考以下文章

在 Core Data 应用程序中调用 performFetch 后,是不是需要手动更新表视图?