【问题标题】:Core Data - break retain cycle of the parent context核心数据 - 打破父上下文的保留周期
【发布时间】:2013-04-06 16:01:01
【问题描述】:

假设我们在核心数据模型中有两个实体:部门和员工。
部门与员工是一对多的关系。

我有以下 ManagedObjectContexts:
- 根:连接到持久存储协调器
- 主要:与父根的上下文

当我想创建员工时,我会执行以下操作:
- 我在主要上下文中有一个部门
- 我在主上下文中创建了一个员工
- 我将部门分配给员工的部门属性
- 我保存主上下文
- 我保存了根上下文

这会在 Main 上下文和 Root 上下文中创建一个保留循环。

如果我在没有子上下文的情况下执行此操作(全部在 Root 上下文中),那么我可以通过在 Employee 上调用 refreshObject:mergeChanges 来打破保留循环。在我使用这两个上下文的情况下,我仍然可以使用该方法来打破 Main 上下文的循环,但我将如何打破 Root 上下文的循环?

旁注:这是一个描述我的问题的简单示例。在 Instruments 中,我可以清楚地看到分配的数量在增长。在我的应用程序中,我的上下文比一层更深,导致了更大的问题,因为我得到了一个新的实体分配,每个上下文都有保留周期。

15/04 更新:NSPrivateQueueConcurrencyType 与 NSMainQueueConcurrencyType
保存两个上下文后,我可以使用 Department 对象在 Main 上下文中执行 refreshObject:mergeChanges。正如预期的那样,这将重新故障部门对象,打破保留周期并在该上下文中取消分配部门和员工实体。

下一步是打破根上下文中存在的保留循环(保存主上下文已将实体传播到根上下文)。我可以在这里执行相同的技巧,并在带有 Department 对象的 Root 上下文中使用 refreshObject:mergeChanges

奇怪的是:当我的 Root 上下文是使用 NSMainQueueConcurrencyType 创建的(所有分配都重新出错并解除分配)时,这可以正常工作,但是当我的 Root 上下文是使用 NSPrivateQueueConcurrencyType 创建时不起作用(所有分配都重新出错,但没有释放)。

旁注:Root 上下文的所有操作都在 performBlock(AndWait) 调用中完成

15/04 更新:第 2 部分
当我使用 NSPrivateQueueConcurrencyType 在根上下文上执行另一个(无用,因为没有更改)保存或回滚时,对象似乎已被释放。我不明白为什么这与 NSMainQueueConcurrencyType 的行为不同。

16/04 更新:演示项目
我创建了一个演示项目:http://codegazer.com/code/CoreDataTest.zip

21/04 更新:到达那里
感谢乔迪·哈金斯的帮助!
我正在尝试将 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 的部门属性时,它在哪个上下文中?
  • 部门在主要上下文中
  • 你有一个小的代码测试用例来证明这一点吗?你如何保存根上下文?另外,当你转储注册对象时你会看到什么?请记住,performBlock 包含一个完整的“用户事件”,但 performBlockAndWait 没有。
  • @JodyHagins 我添加了一个演示项目来说明一些问题。

标签: ios5 core-data fault retain-cycle


【解决方案1】:

我查看了您的示例项目。感谢发帖。

首先,您看到的行为不是错误……至少在 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 集合中删除对象。

编辑

响应您的编辑...并不是第一个不释放对象。就是 MOC 仍然将对象注册为故障。这发生在保存之后,在同一事件中。如果您只是发出一个无操作块[context performBlock:^{}],您将看到从 MOC 中删除的对象。

因此,您无需担心,因为在该 MOC 的下一次操作中,对象将被清除。你不应该有一个长期运行的背景 MOC 无论如何什么都不做,所以这对你来说真的没什么大不了的。

通常,您不想只刷新所有对象。但是,如果您想在保存后删除所有对象,那么您在didSave: 中执行此操作的原始概念是合理的,因为在保存过程中会发生这种情况。但是,这会使所有上下文中的对象出错(您可能不想要)。对于后台 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 打破保留周期,但我不知道用户事件。我在开篇文章中添加了一个关于打破保留周期的小问题,你能向我解释一下其中的区别吗?这是否也是由用户事件引起的?
  • 再次感谢!我将在我的应用程序中探索通知方法。如果你决定写一本关于核心数据的书,请告诉我:)
  • Jody,我遇到了一个可能的 Core Data 错误。我认为你是核心数据大师 :),你可以看看:stackoverflow.com/questions/31805678/…
【解决方案2】:

您在上面描述的步骤是您在 Core Data 中执行的常见任务。 Apple 在Core Data Programming Guide: Object Lifetime Management 中清楚地记录了副作用。

当您在托管对象之间建立关系时,每个对象 保持对它所在的一个或多个对象的强引用 有关的。这可能会导致强参考周期。以确保 参考周期被打破,当你完成一个对象时,你 可以使用托管对象上下文方法 refreshObject:mergeChanges: 把它变成一个错误。

这些对象仅在它们不是故障而是实时实例NSManagedObject 时才保持对彼此的强引用。使用嵌套上下文,将对象保存在您的主上下文中,这些更改应该传播到您的根上下文。但是,除非您在根上下文中获取它们,否则不应创建保留循环。保存完主要上下文后,只需刷新这些对象即可。

关于一般内存占用:

如果您觉得分配变得失控,您可以尝试构建您的代码,以便您在完成后丢弃的单独上下文中执行任务,这会导致大量对象触发错误任务。

另外,如果您使用的是撤消管理器,

与上下文关联的撤消管理器保留对 任何更改的托管对象。默认情况下,在 OS X 中,上下文的撤消 manager 保持无限的撤消/重做堆栈。为了限制你 应用程序的内存占用,你应该确保你擦洗 (使用 removeAllActions)上下文的撤消堆栈以及何时 合适的。除非您强烈引用上下文的撤消 管理器,它与它的上下文一起被释放。

更新 #1:

在尝试了分配工具和专门为此编写的一段代码之后,我可以确认根上下文不会释放内存。这要么是框架错误,要么是设计使然。我发现了一个帖子 here 描述了同样的问题。

[context save:] 之后调用[context reset] 确实释放了内存。我还注意到,在保存之前,根上下文在其[context insertedObjects] 集合中包含我通过子上下文插入的所有对象。对它们进行迭代并执行[context refreshObject:mergeChanges:NO] 确实使对象重新出错。

所以似乎没有什么变通方法,但我不知道这是一个错误并会在即将发布的版本中修复,还是会保持设计不变。

【讨论】:

  • 传播到根上下文后,保留周期也将存在于根上下文中。我已经用仪器测试过了。我已经用更多信息更新了这个问题。我没有使用撤消管理器(iOS 默认)。
  • 还好,以后有时间我会自己做实验。我会及时通知你我的发现。
  • 谢谢,那太好了!
  • @Zyphrax 发布了我的发现的更新。我没有好消息,除非有人有更好的解释,否则您似乎必须解决变通办法。
  • 谢谢。我可能会向 Apple 提交错误报告,我也不太确定这是否是框架的预期行为。现在,我在 MO 中的 didSave 中使用 refreshObject:mergeChanges 来触发所有 MOC 中的重新故障(并最终解除分配)。不漂亮,但可能就足够了。
【解决方案3】:

当保存到根上下文时,唯一持有对对象的强引用的是根上下文本身,因此,如果您只是重置它,对象将在根上下文中被释放。
您保存的流程应该是:
-保存主要
-保存根目录
-重置根目录

我没有重置或刷新主上下文中的对象,即使没有发现泄漏或僵尸。内存似乎在父上下文的保存和重置之后被分配和释放。

【讨论】:

  • 使用 -reset 有点像我无法使用的大锤方法。它将使生活在我的根上下文中的所有实体无效(不仅重新故障它们,而且使它们完全无法使用)。这将使整个上下文-父上下文系统无用。
  • 如果您的父上下文中有对象,对它们的所有更改都将与您从子上下文中引入的更改一起保存。在这种情况下,您必须保存父上下文,并将对象保留在那里(您可能需要手动刷新它们)。在任何情况下,一旦上下文被重置或释放,它就会放弃所有已注册的对象并将它们变成错误 ==> 中断保留周期。您可能希望在单独的上下文中考虑更改,并在以后使用通知合并它们。
  • My Root 上下文用于在屏幕上显示项目,通过 KVO 观察,有相当多的活动 ManagedObjects。当我重置根上下文时,所有这些对象都变得不可用。我必须重新初始化我的应用程序的整个部分才能让它们恢复。我简直无法想象这就是父 MOC - 子 MOC 应该如何工作的。我不明白为什么没有更多的人遇到这个问题。基本上关系 + 父/子 MOC = 记忆问题。
  • 如果您在保存到父上下文时遇到内存问题,您必须自己处理对象图。在一个对象发生故障并添加到已注册对象后,他将至少保留在那里直到下一次保存/重置,刷新对象只会将他变成一个故障并且不会释放他的存根内存
  • NSManagedObjectContext 在对象发生更改时对对象具有强引用,但如果对象没有更改(保存后),则引用是弱引用。因此,刷新对象应该会中断保留周期,将引用计数降至 0 并释放对象。
猜你喜欢
  • 2011-03-08
  • 1970-01-01
  • 1970-01-01
  • 2011-04-28
  • 1970-01-01
  • 2015-09-25
  • 1970-01-01
  • 2019-06-22
  • 2013-02-08
相关资源
最近更新 更多