核心数据 - 打破父上下文的保留循环

14
假设我们在Core Data模型中有两个实体:部门和员工。
部门与员工之间存在一对多的关系。
我有以下Managed Object Contexts:
- Root: 连接到持久性存储协调器
- Main: 具有父级Root的上下文
当我想创建一个员工时,我会执行以下操作:
- 我有一个Main上下文中的部门
- 我在Main上下文中创建一个员工
- 我将部门分配给员工的部门属性
- 我保存Main上下文
- 我保存Root上下文
这会在Main上下文和Root上下文中都创建一个保留循环。
如果我没有使用子上下文(全部在Root上下文中完成),那么我可以通过在员工上调用refreshObject:mergeChanges来打破保留循环。在我的情况下,使用这种方法可以在Main上下文中打破循环,但是我该如何在Root上下文中打破循环呢?
注意:这只是一个简单的例子来描述我的问题。在Instruments中,我可以清楚地看到分配数量增加。在我的应用程序中,我有比一个级别更深的上下文,导致更大的问题,因为我每个保存的上下文都会得到一个新的带有保留周期的实体分配。
更新15/04:NSPrivateQueueConcurrencyType vs NSMainQueueConcurrencyType 在保存了两个上下文之后,我可以对Department对象的主上下文执行refreshObject:mergeChanges。这将如预期一样重新加载Department对象,打破保留周期并在该上下文中释放Department和Employee实体。
下一步是打破存在于根上下文中的保留周期(保存主上下文已将实体传播到根上下文)。我可以在这里使用相同的技巧,并使用refreshObject:mergeChanges在根上下文中与Department对象一起使用。
奇怪的是:当我的根上下文创建为NSMainQueueConcurrencyType时(所有分配都被重新加载并释放),这很好用,但是当我的根上下文创建为NSPrivateQueueConcurrencyType时(所有分配都被重新加载,但不是释放)。
Note: 所有Root上下文的操作都在performBlock(AndWait)调用中完成。
更新15/04:第二部分 当我使用NSPrivateQueueConcurrencyType在Root上下文上进行另一个(无用的,因为没有更改)保存或回滚时,对象似乎已被释放。我不明白为什么这与NSMainQueueConcurrencyType的行为不同。
更新16/04:演示项目 我创建了一个演示项目:http://codegazer.com/code/CoreDataTest.zip 更新21/04:接近成功 感谢Jody Hagings的帮助! 我试图将refreshObject:mergeChanges移出我的ManagedObject didSave方法。 您能否向我解释以下两者之间的区别:
[rootContext performBlock:^{
    [rootContext save:nil];
    for (NSManagedObject *mo in rootContext.registeredObjects)
        [rootContext refreshObject:mo mergeChanges:NO];
}];

并且

[rootContext performBlock:^{
    [rootContext save:nil];
    [rootContext performBlock:^{
        for (NSManagedObject *mo in rootContext.registeredObjects)
            [rootContext refreshObject:mo mergeChanges:NO];
    }];
}];

顶部的代码不会释放对象,而底部的代码会释放对象。

有趣的问题。当您将 Department 实体分配给 Employee 的部门属性时,它处于什么上下文中? - sean woodward
该部门位于主上下文中。 - Yvo
你有一个小的代码测试案例来演示这个吗?你是如何保存根上下文的?此外,当你转储registeredObjects时,你看到了什么?记住,performBlock包装了完整的“用户事件”,但performBlockAndWait则不是。 - Jody Hagins
@JodyHagins 我已经添加了一个演示项目来说明一些问题。 - Yvo
3个回答

10

我看了你的样例项目,让人赞叹你的分享。

首先,你看到的行为不是一个错误......至少在Core Data中不是。你知道,关系会导致保留循环,必须手动打破(在此文档中记录:https://developer.apple.com/library/mac/#documentation/cocoa/Conceptual/CoreData/Articles/cdMemory.html)。

你的代码在didSave:中实现了这个功能。可能有更好的地方来打破这个循环,但那是另一回事。

请注意,您可以通过查看registeredObjects属性轻松查看MOC中注册的对象。

但是,您的示例永远不会释放根上下文中的引用,因为该MOC上从未调用processPendingEvents。因此,MOC中注册的对象将永远不会被释放。

Core Data有一个名为“用户事件”的概念。默认情况下,“用户事件”正确包装在主运行循环中。

但是,对于不在主线程上的MOC,您需要确保用户事件得到正确处理。请参阅此文档:http://developer.apple.com/library/ios/#documentation/cocoa/conceptual/CoreData/Articles/cdConcurrency.html,特别是标题为Track Changes in Other Threads Using Notifications的部分的最后一段。

当您调用performBlock时,您给出的块被包装在完整的用户事件中。但是,对于performBlockAndWait,情况并非如此。因此,私有上下文MOC将保留其registeredObjects集合中的这些对象,直到调用processPendingChanges

在您的示例中,如果您在performBlockAndWait内部调用processPendingChanges或将其更改为performBlock,则可以看到释放的对象。任何一种方法都将确保MOC完成当前用户事件并从registeredObjects集合中删除对象。

编辑

针对您的编辑...问题不在于第一个示例没有dealloc对象。而是MOC仍将对象注册为faults。这发生在保存后,在同一事件中发生。如果你只是执行一个没有操作的block [context performBlock:^{}],你会看到对象从MOC中移除。

因此,你不必担心它,因为在该MOC的下一个操作中,对象将被清除。您不应该有一个无所作为的长时间运行的后台MOC,因此这对您来说并不是一个大问题。

通常,您不希望刷新所有对象。但是,如果您要在保存后删除所有对象,则在didSave:中执行它的原始概念是合理的,因为这发生在保存过程中。但是,这将使所有上下文中的对象都变为faults(您可能不想要)。您可能只想对后台MOC使用这种严厉的方法。您可以在didSave:中检查object.managedObjectContext,但这不是一个好主意。更好的做法是安装DidSave通知的处理程序...

id observer = [[NSNotificationCenter defaultCenter]
    addObserverForName:NSManagedObjectContextDidSaveNotification
                object:rootContext
                 queue:nil
            usingBlock:^(NSNotification *note) {
    for (NSManagedObject *mo in rootContext.registeredObjects) {
        [rootContext refreshObject:mo mergeChanges:NO];
    }
}];

您会发现这可能会给您想要的东西……不过只有您可以确定您真正想要实现什么。


非常感谢您的解释!我知道我必须使用refreshObject:mergeChanges来打破保留循环,但我不知道用户事件。我在我的帖子中添加了一个小问题,关于如何打破保留循环,您能解释一下它与用户事件之间的区别吗?这是否也与用户事件有关? - Yvo
再次感谢!我会在我的应用程序中探索通知方法。如果您决定写一本关于Core Data的书,请告诉我 :) - Yvo
Jody,我遇到了一个可能的Core Data错误。我认为你是Core Data大师:),你能否看一下:https://dev59.com/z43da4cB1Zd3GeqP37gC - Yvo

3

您上面描述的步骤是在Core Data中执行的常见任务。苹果在Core Data编程指南:对象生命周期管理中清楚地记录了这些操作的副作用。

当您在托管对象之间建立关系时,每个对象都会保持对其相关对象的强引用。这可能导致强引用循环。为确保打破引用循环,当您完成一个对象时,可以使用托管对象上下文方法refreshObject:mergeChanges:将其转换为虚拟状态。

只有在它们不是虚拟状态、而是实例化的NSManagedObject时,对象才会相互保持强引用。使用嵌套上下文,在主上下文中保存对象时,这些更改应该传播到根上下文。但是,除非您在根上下文中提取它们,否则不应该创建保留循环。在保存主要上下文后,刷新那些对象应该就足够了。

关于内存占用方面的一般性问题:

如果您觉得分配的资源过多,可以尝试结构化代码,使导致大量对象故障的任务在单独的上下文中执行,并在完成任务后丢弃该上下文。此外,如果您正在使用撤销管理器,则与上下文相关联的撤销管理器会对任何更改的托管对象保持强引用。默认情况下,在OS X中,上下文的撤销管理器保留无限的撤销/重做堆栈。为了限制应用程序的内存占用,您应该确保在适当的时候清除(使用removeAllActions)上下文的撤销堆栈。除非您保留上下文的撤销管理器的强引用,否则它将随其上下文一起被释放。更新#1:通过实验分配仪器和专门编写的测试代码,我可以确认根上下文不会释放内存。这是一个框架错误还是设计意图尚不清楚。我在here找到了描述相同问题的帖子。
在执行[context save:]之后调用[context reset]确实会释放内存。我注意到,在保存之前,根上下文中有所有我通过子上下文插入的对象,它们都在[context insertedObjects]集合中。迭代这些对象并执行[context refreshObject:mergeChanges:NO]会重新加载这些对象。因此,似乎有一些解决方法,但我不知道这是否是一个错误,是否会在即将发布的版本中修复,或者它是否按设计保留。

在传播到根上下文之后,保留循环也将存在于根上下文中。我已经使用工具测试过了。我已经更新了问题并提供了更多信息。我没有使用撤销管理器(iOS默认)。 - Yvo
好的,我会在以后有更多时间时进行自己的实验。我会及时告诉你我的发现。 - svena
@Zyphrax发布了我的发现的更新。我没有好消息,除非有人有更好的解释,否则似乎你只能使用解决方法。 - svena
谢谢。我可能会向苹果提交错误报告,但我也不确定这是否是框架的预期行为。现在我正在使用 refreshObject:mergeChanges 在 MO 的 didSave 中触发所有 MOC 中的重新失效(最终处理)。虽然不太美观,但可以满足需求。 - Yvo
+1。对这个问题的研究很棒。如果您已经向苹果提交了错误报告,请在此处发布错误编号,以便我们在提交类似错误时进行参考。 - memmons

1
当保存到根上下文时,唯一持有对象的强引用的是根上下文本身,所以,如果只重置它,对象将在根上下文中被释放。
您的保存流程应该是:
- 保存主要内容
- 保存根
- 重置根
我没有重置或刷新主上下文中的对象,即使如此,也没有发现泄漏或僵尸。在父上下文保存和重置后,内存似乎被分配和释放。

使用-reset是一种有些粗暴的方法,我不能使用它。它将使我根上下文中的所有实体无效(不仅是重新故障,而是完全无法使用)。这将使整个上下文-父上下文系统变得无用。 - Yvo
如果您的对象存在于父上下文中,则对它们所做的所有更改都将与从子上下文引入的更改一起保存。在这种情况下,您将不得不保存父上下文,并保留其中的对象(您可能需要手动刷新它们)。无论如何,一旦上下文被重置或释放,它就会放弃所有已注册的对象并将它们转换为故障==>打破保留循环。您可能希望考虑在单独的上下文中进行更改,然后使用通知合并它们。 - Dan Shelly
我的根上下文用于在屏幕上显示项目,并通过KVO进行观察,有许多活动ManagedObjects。当我 - 重置根上下文时,所有这些对象都变得无法使用。我必须重新初始化整个应用程序的某些部分才能将它们恢复。我简直无法想象父MOC-子MOC应该如何工作。我不明白为什么没有更多的人遇到这个问题。基本上关系+父/子MOC=内存问题。 - Yvo
如果在保存到父上下文时出现内存问题,您必须自己处理对象图。当一个对象被故障到一个上下文并被添加到注册的对象中后,它将至少保留在那里直到下一次保存/重置,刷新对象只会将其转换为故障,并不会释放其存根内存。 - Dan Shelly
NSManagedObjectContext在对象有更改时具有强引用,但如果它们没有更改(保存后),则引用是弱的。因此,刷新对象应该打破保留循环,将引用计数降至0并释放对象。 - Yvo

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接