具有关系属性性能问题的核心数据sectionNameKeyPath

时间:2014-08-27 15:40:07

标签: ios objective-c uitableview core-data nsfetchedresultscontroller

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

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

当我使用NSFetchedResultsController中的UITableView执行抓取时,我想使用Person的实体Group个对象分组{ {1}}属性。

为此,我使用name

问题在于,当我使用sectionNameKeyPath:@"group.name"关系中的属性时,Group会在20个小批量(我有NSFetchedResultsController)中提前取出所有内容而不是在我滚动setFetchBatchSize: 20

的同时获取批次

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

我用来实例化NSFetchedResultsController的代码:

NSFetchResultsController

如果我根据- (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; } 创建部分而不与App的UI进行任何交互,那么这就是我在Instruments中获得的内容: Core Data Fetch with Sections by Relationship

如果sectionNameKeyPath为nil,这就是我得到的(在UITableView上滚动一点): Core Data Fetch without any Sections

拜托,有谁可以帮我解决这个问题?

编辑1:

我似乎从模拟器和仪器得到了不一致的结果:当我问这个问题时,应用程序在大约10秒内(由Time Profiler)使用上面的代码在模拟器中启动。

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

我附上了一些新截图: Time Profiler with Simulator Upfront Fetch in Simulator without scrolling Upfront Fetch in Simulator with scrolling and small batch fetches

编辑2: 我重置模拟器,结果很有趣:执行导入操作并退出应用程序后,第一次运行如下所示: First run after simulator reset and new import 滚动一下后: First run after simulator reset, new import and some scrolling 现在这是第二次运行时发生的事情: Second run after simulator reset and new import 第五次运行后: Fifth run

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

3 个答案:

答案 0 :(得分:1)

这是你声明的目标:"我需要通过关系实体Group,name属性和NSFetchResultsController按部分对Person对象进行分组,以便在我滚动时执行小批量提取,而不是在前面进行现在"

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

TL; DR;要更改此行为,您需要更改NSFetchedResultsController构建节的方式。

发生了什么事?

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

如果未指定sectionNameKeyPath,则它会完全符合您的预期。 fetch返回一个结果的代理数组,其中包含" real"第一个fetchBathSize项目的对象。因此,例如,如果您有setFetchBatchSize到2,并且您的谓词与商店中的10个项匹配,则结果包含前两个对象。其他对象将在访问时单独获取。这提供了平滑的分页响应体验。

但是,当指定sectionNameKeyPath时,获取的结果控制器必须执行更多操作。要计算部分,需要访问结果中所有对象的关键路径。它列举了我们示例中结果中的10个项目。前两个已经取得了进展。在枚举期间将获取另外8个以获取构建节信息所需的关键路径值。如果您的获取请求有很多结果,则效率非常低。有关此功能的公共错误有很多:

NSFetchedResultsController initially takes too long to set up sections

NSFetchedResultsController ignores fetchLimit property

NSFetchedResultsController, Table Index, and Batched Fetch Performance Issue

......还有其他几个人。当你考虑它时,这是有道理的。要构建NSFetchedResultsSectionInfo对象,需要获取的结果控制器查看sectionNameKeyPath结果中的每个值,将它们聚合到唯一的值组合,并使用该信息创建正确的{{1}个值。 1}}对象,设置名称和索引标题,知道结果包含的结果中有多少个对象等。要处理一般用例,没有办法解决这个问题。考虑到这一点,您的仪器痕迹可能更有意义。

你怎么能改变这个?

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

哦,上帝不。某种临时黑客怎么能让它表现得更好但却无法解决问题?

更改数据模型不会改变上述行为,但可以稍微改变性能影响。批量更新不会对此行为产生任何重大影响,实际上不会很好地使用获取的结果控制器。但是,对您来说可能更有用的是设置NSFetchedResultsSectionInf以包含您的"组"关系,可以显着改善取出和断裂行为。另一种策略可能是在尝试使用获取的结果控制器之前执行另一次提取以批处理这些对象,这将以更有效的方式填充各种级别的Core Data内存缓存。

relationshipKeyPathsForPrefetching缓存主要用于部分信息。这可以防止必须在每次更改时完全重新计算这些部分(在最好的情况下),但实际上可以使构建部分的初始提取需要更长的时间。您将不得不尝试查看缓存是否值得用于您的用例。

如果主要关注的是这些核心数据操作阻止用户交互,您可以从主线程卸载它们。 NSFetchedResultsController can be used on a private queue (background) context,它将阻止Core Data操作阻止UI。

答案 1 :(得分:0)

根据我的经验,实现目标的方法是非规范化您的模型。特别是,您可以在group实体中添加Person属性,并将该属性用作sectionNameKeyPath。因此,当您创建Person时,您还应该传递它所属的组。

此非规范化过程是正确的,因为它允许您避免获取相关的Group对象,因为没有必要。可能的缺点是,如果您更改组的名称,则与该名称关联的所有人员都必须更改,相反,您可能会有不正确的值。

这里的关键方面如下。您需要记住核心数据不是 关系型数据库。该模型不应该设计为可以进行规范化的数据库模式,但应该从数据如何在用户界面中呈现和使用的角度进行设计。

修改1

我无法理解你的评论,你能解释一下吗?

  

我发现非常有趣的是,即使应用程序是   在模拟器中执行完整的前期提取,应用程序加载   尽管模拟器在哪里,设备上900毫秒(有5000个物体)   载得慢得多。

无论如何,我有兴趣了解您的Photo实体的详细信息。如果您预先获取照片,则可能会影响整体执行。

您是否需要在表格视图中预取Photo?他们是大拇指(小照片)?还是正常的图像?你利用External Storage Flag吗?

group实体添加其他属性(比如Person)可能不是问题。当name对象的Group更改时,如果您在后台执行该操作,则更新该属性的值不是问题。此外,从iOS 8开始,您可以按Core Data Batch Updates

中所述进行批量更新

答案 2 :(得分:0)

自从我发布这个问题差不多一年后,我终于找到了支持这种行为的罪魁祸首(在Xcode 6中略有改变):

  1. 关于不一致的提取时间:我正在使用缓存,当时我来回打开,关闭和重置模拟器。

  2. 关于这一事实是所有内容都是在没有滚动的情况下小批量提取的(在Xcode 6的核心数据工具中已经不是这样了 - 现在它是一个,大取一个需要整整几秒钟):

  3. 似乎setFetchBatchSizeparent/child contexts无法正常工作。该问题已于2012年报告,似乎仍然存在http://openradar.appspot.com/11235622

    要解决此问题,我使用independent context创建了另一个NSMainQueueConcurrencyType,并将其persistence coordinator设置为与我的其他contexts正在使用的相同。

    有关问题#2的更多信息,请访问:https://stackoverflow.com/a/11470560/1641848