核心数据sectionNameKeyPath与关系属性性能问题
我有一个包含三个实体的核心数据模型:
Person
, Group
, Photo
与他们之间的关系如下:
- 人<< ———–>小组(一对多关系)
- 人<————->照片(一对一)
当我使用UITableView
的NSFetchedResultsController
执行提取操作时,我想要使用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的各个级别。
NSFetchedResultsController
caching主要用于区段信息。 这样可以防止在每次更改(最好的情况下)都必须对部分进行完全重新计算,但实际上可以使初始获取部分的部分花费更长的时间。 你将不得不尝试一下,看看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中略有改变):
-
关于不一致的提取时间:我正在使用一个caching,当时我正在打开,closures和重置模拟器。
-
关于事实上,一切都是小批量提前没有滚动(在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
- UIImageView + AFNetworking setImageWithURLRequest使用RestKit?
- 为什么iOS在通过UIManagedDocument保存核心数据时延迟?
- 核心数据与iClouddevise
- 在使用UIManagedDocument的Xcode 4.5的iPhone模拟器5.1中的错误
- 如何在NSDate中使用NSPredicate进行分组 – 需要创build多个UITableView部分
- ios – 手动调用viewWillAppear()时出错(Objective-C)
- 通过使用核心数据的关系获取属性的值 – 当前传递NULL
- 何时使用核心数据的NSMainQueueConcurrencyType?
- 部分保存managedObjectContext