核心数据 - 中断父上下​​文的保留周期

时间:2013-04-14 14:04:44

标签: ios5 core-data fault retain-cycle

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

我有以下ManagedObjectContexts:
- Root:连接到Persistent Store Coordinator
- Main:具有父Root的上下文

当我想创建员工时,我会执行以下操作:
- 我在主要背景下有一个部门 - 我在Main主页中创建了一个Employee - 我将部门分配给员工的部门财产
- 我保存主要上下文
- 我保存了Root上下文

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

如果我没有子上下文(所有在Root上下文中)这样做,那么我可以通过调用Employee上的refreshObject:mergeChanges来中断保留周期。在我使用这两个上下文的情况下,我仍然可以使用该方法来打破Main上下文的循环,但是我如何打破Root上下文的循环呢?

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

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

下一步是打破Root上下文中存在的保留周期(保存Main上下文已将实体传播到Root上下文)。我可以在这里做同样的技巧,并使用Department对象在Root上下文中使用refreshObject:mergeChanges

奇怪的是:当使用NSMainQueueConcurrencyType创建我的Root上下文(所有分配都被重新出现并解除分配)时,这很好用,但是当使用NSPrivateQueueConcurrencyType创建我的Root上下文时(所有分配都重新出错,但解除分配。)

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

更新15/04:第2部分
当我使用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];
    }];
}];

顶部的一个不会释放对象,底部的就是。

3 个答案:

答案 0 :(得分:10)

我看了你的示例项目。感谢张贴。

首先,您看到的行为不是错误......至少在核心数据中没有。如您所知,关系会导致保留周期,必须手动打破(在此处记录: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被调用。

在您的示例中,如果您在processPendingChanges内拨打performBlockAndWait或将其更改为performBlock,则可以看到已发布的对象。其中任何一个都将确保MOC完成当前用户事件并从registeredObjects集合中删除对象。

修改

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

因此,您不必担心它,因为在该MOC的下一个操作中,将清除对象。你不应该有一个长期运行的背景MOC,无论如何都不做任何事情,所以这对你来说真的不是什么大问题。

通常,您不想只刷新所有对象。但是,如果您想要在保存后删除所有对象,那么在didSave:中执行此操作的原始概念是合理的,因为在保存过程中会发生这种情况。但是,这将导致所有上下文中的对象出错(您可能不想要)。您可能只希望这种严苛的方法用于后台MOC。您可以在object.managedObjectContext中查看didSave:,但这不是一个好主意。更好的是为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];
    }
}];

你会发现这可能会给你你想要的东西......虽然只有你能确定你真正想要完成的事情。

答案 1 :(得分:3)

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

  

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

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

关于内存占用情况:

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

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

  

与上下文关联的撤消管理器保留对其的强引用   任何更改的托管对象。默认情况下,在OS X中上下文的撤消   manager保持无限制的undo / redo堆栈。限制你的   应用程序的内存占用,你应该确保你擦洗   (使用removeAllActions)上下文的撤销堆栈时间和时间   适当。除非你强烈引用上下文的撤消   经理,它的背景已被解除分配。

更新#1:

在尝试使用分配工具和专门编写的代码片段进行测试之后,我可以确认根上下文不会释放内存。这可能是一个框架错误,也可能是设计意图。我找到了一篇描述同一问题的帖子here

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

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

答案 2 :(得分:1)

保存到根上下文时,唯一拥有对象强引用的是根上下文本身,因此,如果只是重置它,则会在根上下文中释放对象。 你节省的流量应该是:
-save主要 - 保存根
- 重置根

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