核心数据sectionNameKeyPath与关系属性性能问题

我有一个包含三个实体的核心数据模型:
PersonGroupPhoto与他们之间的关系如下:

  • 人<< ———–>小组(一对多关系)
  • 人<————->照片(一对一)

当我使用UITableViewNSFetchedResultsController执行提取操作时,我想要使用Group的实体name属性在部分中对Person对象进行Group

为此,我使用sectionNameKeyPath:@"group.name"

问题是,当我使用Group关系中的属性时, setFetchBatchSize: 20以小批量20(我有setFetchBatchSize: 20 )取得一切,而不是在滚动tableView取批。

如果我使用Person实体的属性(如sectionNameKeyPath:@"name" )来创build节,一切正常: NSFetchResultsController在我滚动时加载小批量的20个对象。

我用来实例化NSFetchedResultsController的代码:

 - (NSFetchedResultsController *)fetchedResultsController { if (_fetchedResultsController) { return _fetchedResultsController; } NSFetchRequest *fetchRequest = [[NSFetchRequest alloc] init]; NSEntityDescription *entity = [NSEntityDescription entityForName:[Person description] inManagedObjectContext:self.managedObjectContext]; [fetchRequest setEntity:entity]; // Specify how the fetched objects should be sorted NSSortDescriptor *groupSortDescriptor = [[NSSortDescriptor alloc] initWithKey:@"group.name" ascending:YES]; NSSortDescriptor *personSortDescriptor = [[NSSortDescriptor alloc] initWithKey:@"birthName" ascending:YES selector:@selector(localizedStandardCompare:)]; [fetchRequest setSortDescriptors:[NSArray arrayWithObjects:groupSortDescriptor, personSortDescriptor, nil]]; [fetchRequest setRelationshipKeyPathsForPrefetching:@[@"group", @"photo"]]; [fetchRequest setFetchBatchSize:20]; NSError *error = nil; NSArray *fetchedObjects = [self.managedObjectContext executeFetchRequest:fetchRequest error:&error]; if (fetchedObjects == nil) { NSLog(@"Error Fetching: %@", error); } _fetchedResultsController = [[NSFetchedResultsController alloc] initWithFetchRequest:fetchRequest managedObjectContext:self.managedObjectContext sectionNameKeyPath:@"group.name" cacheName:@"masterCache"]; _fetchedResultsController.delegate = self; return _fetchedResultsController; } 

这是我在仪器中得到的,如果我创build基于"group.name"而不与应用程序的用户界面进行任何交互: 核心数据与关系部分提取

这就是我得到的(有一点在UITableView滚动)如果sectionNameKeyPath是零: 核心数据提取没有任何部分

请问有谁能帮我解决这个问题?

编辑1:

看起来我从模拟器和乐器中得到了不一致的结果:当我提出这个问题时,应用程序在大约10秒钟(通过时间分析器)使用上述代码在模拟器中启动。

但是今天,使用与上面相同的代码,应用程序在模拟器中以900ms启动,即使它对所有对象进行临时前期提取,并且不会阻塞用户界面。

我附上了一些新的截图: 时间分析器与模拟器预先在模拟器中抓取而不滚动预先在模拟器中提取滚动和小批量提取

编辑2:我重置模拟器,结果是有趣的:执行导入操作后,退出应用程序第一次运行看起来像这样: 首先在模拟器重置和新导入之后运行 滚动一下后: 首先运行模拟器复位后,新的导入和一些滚动 现在这是第二次运行的情况: 模拟器复位和新导入后第二次运行 第五次运行后: 第五次运行

编辑3:第七次和八次运行应用程序,我得到这个: 第七次运行第八次运行

这是你所说的目标:“我需要Person对象在关系实体Group,name属性和NSFetchResultsController中进行分组,以在我滚动时执行小批量提取,而不是像现在这样进行提前操作。

答案有点复杂,主要是因为NSFetchedResultsController如何构build节,以及如何影响获取行为。

TL; DR; 要改变这种行为,你需要改变NSFetchedResultsController如何构build部分。

发生什么事?

当一个NSFetchedResultsController被赋予一个带有分页的获取请求(fetchLimit和/或fetchBatchSize)时,会发生一些事情。

如果没有指定sectionNameKeyPath ,它确实如你所期望的那样。 fetch返回结果的代理数组,第一个fetchBathSize项的数量为“真实”对象。 因此,例如,如果将setFetchBatchSize为2,并且谓词与商店中的10个项目匹配,则结果将包含前两个对象。 其他对象将在访问时分开提取。 这提供了平滑的分页响应体验。

但是,当指定sectionNameKeyPath时,获取的结果控制器必须做更多。 要计算结果中需要访问关键path的所有对象的部分。 它枚举了我们例子中的10个结果。 前两个已经被提取。 在枚举过程中将获取其他8个值,以获取构build节信息所需的关键path值。 如果您的提取请求有很多结果,这可能是非常低效的。 有一些关于这个function的公共bug:

NSFetchedResultsController最初花费太长时间来设置部分

NSFetchedResultsController忽略fetchLimit属性

NSFetchedResultsController,表索引和批处理提取性能问题

还有其他几个 当你考虑一下,这是有道理的。 要构buildNSFetchedResultsSectionInfo对象,需要获取的结果控制器查看sectionNameKeyPath结果中的每个值,将它们聚合为值的唯一联合,并使用该信息创build正确数量的NSFetchedResultsSectionInfo对象,设置名称和索引标题,知道一个部分包含的结果中有多less个对象等等。为了处理一般用例,没有办法解决这个问题。 考虑到这一点,你的乐器痕迹可能会更有意义。

你怎么能改变这个?

您可以尝试构build您自己的NSFetchedResultsController ,它提供了构buildNSFetchedResultsSectionInf o对象的替代策略,但是您可能遇到一些相同的问题。 例如,如果您正在使用现有的fetchedObjectsfunction来访问提取结果的成员,则在访问出现故障的对象时将会遇到相同的行为。 您的实施需要一个处理这个(这是可行的,但非常依赖于您的需求和要求)的战略。

哦,上帝不。 怎么样的临时黑客,只是使其performance更好一点,但不解决问题?

改变你的数据模型不会改变上述行为,但可以稍微改变性能影响。 批量更新不会对此行为产生任何显着影响,实际上,对于获取的结果控制器来说,这并不会起到很好的作用。 但是,您可能更有用,而是将relationshipKeyPathsForPrefetching设置为包含您的“组”关系,这可能显着提高抓取和断层行为。 另一种策略可能是在尝试使用抓取的结果控制器之前,执行另一次获取以对这些对象进行批处理,从而以更高效的方式填充核心数据内存caching的各个级别。

NSFetchedResultsControllercaching主要用于区段信息。 这样可以防止在每次更改(最好的情况下)都必须对部分进行完全重新计算,但实际上可以使初始获取部分的部分花费更长的时间。 你将不得不尝试一下,看看caching是否值得你的用例。

如果您的主要担心是这些核心数据操作阻止了用户交互 ,则可以将它们从主线程卸载。 NSFetchedResultsController可用于私有队列(后台)上下文 ,这将阻止核心数据操作阻止用户界面。

根据我的经验,实现您的目标的一个方法是使您的模型非规范化 。 特别是,您可以在Person实体中添加一个group属性,并将该属性用作sectionNameKeyPath 。 所以,当你创build一个Person你也应该通过它所属的组。

这个非规范化过程是正确的,因为它允许你避免获取相关的Group对象,因为没有必要。 一个缺点是,如果你改变了一个组的名字,所有与这个名字相关联的人必须改变,相反你可能会有不正确的值。

这里的关键是以下几点。 您需要记住Core Data不是关系数据库。 该模型不应该被devise为数据库模式,在那里可以进行标准化,但是应该从数据在用户界面中如何呈现和使用的angular度来devise模型。

编辑1

我不明白你的意见,你能解释一下吗?

我发现非常有趣的是,即使应用程序正在模拟器中执行完整的预先获取,应用程序也会在设备上加载900ms(包含5000个对象),尽pipe模拟器的加载速度要慢很多。

无论如何,我会有兴趣知道你的Photo实体的细节。 如果您预先获取照片,整体执行可能会受到影响。

你需要在你的表格视图中预取一张Photo吗? 他们是拇指(小照片)? 还是正常的图像? 你是否利用外部存储标志 ?

Person实体添加一个额外的属性(比如group )不会是一个问题。 如果在背景中执行Group对象的name更改,那么更新该属性的值不是问题。 另外,从iOS 8开始,您可以使用核心数据批量更新中描述的批量更新 。

在发布这个问题近一年后,我终于find了导致这种行为的罪魁祸首(在Xcode 6中略有改变):

  1. 关于不一致的提取时间:我正在使用一个caching,当时我正在打开,closures和重置模拟器。

  2. 关于事实上,一切都是小批量提前没有滚动(在Xcode 6的核心数据工具,现在不是这种情况了 – 现在是一个,大的获取,需要整整一秒):

看来, setFetchBatchSize不能正确的parent/child contexts 。 这个问题在2012年被报道,似乎它仍然存在http://openradar.appspot.com/11235622

为了解决这个问题,我使用NSMainQueueConcurrencyType创build了另一个independent context ,并将其persistence coordinator器设置为与其他contexts相同。

更多关于问题#2在这里: https : //stackoverflow.com/a/11470560/1641848