【问题标题】:Core Data memory usage while importing large dataset导入大型数据集时的核心数据内存使用情况
【发布时间】:2012-10-19 04:54:46
【问题描述】:

我现在被一个令人讨厌的核心数据问题困住了大约两个星期。我阅读了很多博文、文章和 SO 问题/答案,但我仍然无法解决我的问题。

我进行了大量测试,能够将较大的问题缩减为较小的问题。 这将是一个很大的解释,所以请继续关注我!

问题 - 数据模型

我必须得到以下数据模型

对象 A 与对象 B 具有一对多关系,而对象 B 与对象 C 具有另一个一对多关系。由于 Core Data 的建议,我必须创建反向关系,因此 B 的每个实例都指向其父 A 并且指向其父 B 的 C 也是如此。

A <->> B <->> C

问题 - MOC 设置

为了保持响应顺畅,我创建了一个三级 managedObjectContext 结构。

  1. 父 MOC - 使用 NSPrivateQueueConcurrencyType 在自己的私有线程上运行,与 persistentStoreCoordinator 紧密相连
  2. MainQueue MOC - 使用 NSMainQueueConcurrencyType 在 mainThread 上运行并拥有父 MOC 1
  3. 对于每个解析操作,我都会创建第三个 MOC,它也有它的私有队列和父 mainQueue MOC

我的主数据控制器作为观察者添加到 MOC 2 的 NSManagedObjectContextDidSave 通知中,因此每次 MOC 2 在 MOC1 上保存 performBlock: 时都会触发执行保存操作(由于 performBlock: 而异步)。

问题 - 解析

为了将大型 JSON 文件解析为我的核心数据结构,我编写了一个循环解析器。这个解析器首先创建一个新的 MOC (3)。然后它获取对象 A 的数据并解析其属性。然后解析器读取 B 的 JSON 关系并创建相应的填充数据的对象。这些新对象通过在 A 上调用 addBObject: 添加到 A。 因为解析器是循环的,解析 B 意味着解析 C,并且这里也创建了新对象并将其附加到 B。 这一切都发生在 MOC 3 上的 performBlock: 中。

  • 解析(创建“A”对象并开始解析 B)
    • 解析 A(创建“B”对象,将它们附加到 A 并开始解析 C)
      • 解析 B(创建“C”对象,将它们附加到 B)
        • 解析 C(仅将数据存储在 C 对象中)

在每次解析操作之后,我保存 MOC 3 并在 mainThread 上调度主 MOC (2) 的保存操作。由于NSManagedObjectContextDidSave 通知,MOC 1 将异步自动保存。

        if (parsed){
            NSError *error = nil;
            if (![managedObjectContext save:&error])
                NSLog(@"Error while saving parsed data: %@", error);
        }else{
            // something went wrong, discard changes
            [managedObjectContext reset];
        }

        dispatch_async(dispatch_get_main_queue(), ^{                
            // save mainQueueManagedObjectContext
            [[HWOverallDataController sharedOverallDataController] saveMainThreadManagedObjectContext];
        });

为了释放我的内存占用,因为我现在不需要解析数据,所以我正在执行:

[a.managedObjectContext refreshObject:a mergeChanges:NO];

对于我刚刚解析的每个 A。

因为我需要解析大约 10 个 A,它们都有大约 10 个 B,它们大约有 10 个 C,因此会生成很多 managedObject。

问题 - 仪器

一切正常。唯一的事情是:当我打开分配工具时,我看到未发布的 A、B 和 C。我没有从他们的 retainCounts 或任何其他信息中获得任何有用的信息。 而且由于我的实际问题涉及更复杂的数据模型,因此活动对象成为严重的内存问题。 有人能弄清楚我做错了什么吗?使用正确的 managedObject 在其他 managedObjectContexts 上调用 refreshObjects 也不起作用。只有一个硬的reset 似乎可以工作,但随后我失去了指向 UI 使用的活动对象的指针。

我尝试过的其他解决方案

  • 我尝试创建单向关系而不是双向关系。这会产生许多其他问题,导致 Core Data 不一致和奇怪的行为(例如悬空对象和 Core Data 生成 1-n 关系而不是 n-n 关系(因为反向关系未知)。

  • 当我在任何对象上检索 NSManagedObjectContextDidSave 通知时,我尝试刷新每个更改或插入的对象

这两个“解决方案”(顺便说一句,不起作用)似乎也有点老套。这不应该是要走的路。不过,应该有一种方法可以在不增加内存占用并保持 UI 流畅的情况下使其工作?

- 代码演示

http://cl.ly/133p073h2I0j

- 进一步调查

在 mainContext 中(在 mainSave 之后)刷新每个曾经使用过的对象(这是一项繁琐的工作)后,对象的大小会减少到 48 个字节。这表明对象全部出错,但内存中仍有一个指针。当我们有大约 40.000 个对象都出现故障时,内存中仍然有 1.920 MB 的内存在persistentManagedObjectContext 重置之前永远不会释放。这是我们不想做的事情,因为我们失去了对任何 managedObject 的所有引用。

【问题讨论】:

  • 这是一个很好的长描述,但如果没有代码,我怀疑你得到的不仅仅是猜测......
  • 在我看来,与哪些代码非常相关不是问题。它更具概念性。但如果有必要,我可以制作一个小型演示项目,您可以在其中遇到问题。
  • 我添加了一个说明问题的 XCode 项目
  • 将导入/解析处理包装在 @autoreleasepool 块中,以便为每个对象 A(及其 B 和 C)清除池。
  • @Rog 不,可能不是这样。将它们包装在自动释放块之间并不能解决问题。经过更多调试后,mainManagedObjectContext (2) 似乎保持对 A 对象的强引用。

标签: ios core-data memory


【解决方案1】:

对于您为特定目的保留的每个NSManagedObjectContext,您将积累NSManagedObject 的实例

NSManagedObjectContext 只是一张便条纸,如果您希望将更改保留在 NSPersistentStore 中,您可以随意实例化并保存它,然后再丢弃。

对于解析操作(第 3 层),请尝试为 op 创建一个 MOC,进行解析,保存 MOC,然后将其丢弃。

感觉你至少有一层 MOC 被强引用太多了。

基本上针对每个 MOC 提出问题。 “为什么要让这个对象及其关联的孩子保持活力”。

【讨论】:

  • 这正是我现在正在做的事情。对于每个解析操作,我创建一个新的 MOC (3),它是级别 2 (mainQueue MOC) 的 MOC 的子级。解析后 MOC (3) 被保存并且不再被强引用(因此应该由 ARC 释放)。
【解决方案2】:

我有一个非常相似的导入助手。

看看下面的代码,看看它是否对你有帮助

__block NSUInteger i = 0;
NSArray *jsonArray = ...
for (NSDictionary *dataStucture in jsonArray)
{
    [managedObjectContext performBlock:^{
        @autoreleasepool {
            i++;
            A *a = (A*)[self newManagedObjectOfType:@"A" inManagedObjectContext:managedObjectContext];
            [self parseData:[dataStucture objectForKey:@"a"]
                 intoObject:a
     inManagedObjectContext:managedObjectContext];

            [managedObjectContext refreshObject:a
                                   mergeChanges:YES];
            if (i > 20) // Arbitrary number here
            {
                NSError *error = nil;
                [managedObjectContext save:&error];
                [managedObjectContext reset];
            }

            [managedObjectContext refreshObject:a
                                   mergeChanges:YES];

        }
        dispatch_async(dispatch_get_main_queue(), ^{
            [self saveMainThreadManagedObjectContext];

            NSLog(@"DONE");
            // parsing is done, now you see that there are still
            // A's, B's and C's left in memory.
            // Every managedObjectContext is saved and no references are kept
            // to any A, B and C so they should be released. This is not true,
            // so a managedObjectContext is keeping a strong reference to these
            // objects.
        });
    }];
}

【讨论】:

  • 感谢您的建议。我试过了,但每个对象仍有 2 个实例仍然处于活动状态(在 mainManagedObjectContext (2) 和 persistentManagedObjectContext (3) 中)。
【解决方案3】:

罗宾,

我有一个类似的问题,但我的解决方法与你不同。在您的情况下,您有第三个,IMO,冗余 MOC,父 MOC。就我而言,我让两个 MOC 以老式的方式通过 DidSave 通知通过持久存储协调器进行通信。新的面向块的 API 使这更加简单和健壮。这让我可以重置子 MOC。虽然您从第三个 MOC 获得了性能优势,但与我利用的 SQLite 行缓存相比,它的优势并不大。您的路径会消耗更多内存。最后,通过跟踪 DidSave 通知,我可以在创建项目时对其进行修剪。

顺便说一句,您的MALLOC_TINYMALLOC_SMALL VM 区域的大小也可能大幅增加。我的尾随修剪算法让分配器更快地重用空间,从而延缓这些问题区域的增长。根据我的经验,这些区域由于它们的大量驻留内存占用,是我的应用程序 Retweever 被杀死的主要原因。我怀疑你的应用也会遭遇同样的命运。

当内存警告来临时,我调用下面的sn-p:

[self.backgroundMOC performBlock: ^{ [self.backgroundMOC reset]; }];

[self.moc save];

[self.moc.registeredObjects trimObjects];

-[NSArray(DDGArray) trimObjects] 只是遍历一个数组并刷新对象,从而修剪它们。

总之,Core Data 似乎为许多 MOC 中出现的项目实现了写入时复制算法。因此,您以意想不到的方式保留了一些东西。我专注于在导入后断开这些连接以最小化我的内存占用。由于 SQLite 行缓存,我的系统似乎表现得可以接受。

安德鲁

【讨论】:

  • 感谢您的建议。几个问题:你们的 MOC 结构是什么?一个带有两个 MOC 的 persistentStoreCoordinator?还是父子关系?其中哪一个是 backgroundMOC?
  • 主 MOC self.moc 和后台 MOC self.backgroundMOC 使用相同的商店协调器。他们的合并政策略有不同,有利于主要的 MOC。他们当然会使用DidSave 通知进行交流。
  • 确实,这正是我所期望的。我们现在让我们的后端准备好在这个设置中使用两个 MOC。我会让你知道结果
  • Robin,让我补充一下,我的应用程序中还有一些其他暂存器 MOC。他们通过缓存模型计算所需的内容来支持 UI。它们也很容易重置。由于内存问题,我不会将它们设为主 MOC 的子节点。基本上,我将父子 MOC 模式视为 MOC 通知模式的替代方案,而不是替代模式。能够按需修剪内存给了我在具有大型常驻 VM 区域的应用程序中的一些优势。用你的 40K 物品,你分担我的痛苦。你真的必须管理你居住地区的增长。安德鲁
  • 我终于测试了一切,这似乎工作得很好。非常感谢您的回复。这就是我一直在寻找的答案
猜你喜欢
  • 2015-10-04
  • 2011-08-04
  • 2014-01-04
  • 1970-01-01
  • 1970-01-01
  • 2021-03-03
  • 2013-09-03
  • 2016-12-17
  • 2019-12-14
相关资源
最近更新 更多