【问题标题】:iOS CoreData: NSFetchedResultsController performancesiOS CoreData:NSFetchedResultsController 性能
【发布时间】:2011-04-17 07:43:36
【问题描述】:

在模型中,我有两个实体:Record 和 Category。类别通过反向关系与记录一对多。持久化存储是 SQLITE 类型,db 也不算小,大约 23MB(17k 条记录)。

我使用列表详细设计来显示记录表和详细记录视图。列表视图控制器使用 NSFetchedResultsController。

在设备上构建,如果我不使用 setFetchBatchSize:

CoreData:注解:sql连接获取时间:15.8800s CoreData:注释:总提取执行时间:16.9198 秒,17028 行。

天哪!

如果我使用 setFetchBatchSize:25,一切都会再次正常运行:

CoreData:注解:sql连接获取时间:1.1736s CoreData:注释:总提取执行时间:1.1900 秒,17028 行。

是的,那太好了!但事实并非如此!在列表 viewController 中,当用户点击记录时,我会分配一个详细的 viewController,并在 fetchedResultsController 中的 indexPath 处传递记录:

- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath {
Record *record = (Record *)[fetchedResultsController objectAtIndexPath:indexPath];
RecordViewController *recordViewController= [[RecordViewController alloc] init];
    recordViewController.record = record;
    [self.navigationController pushViewController:recordViewController animated:YES];
    [recordViewController release];
}

现在,在详细的 viewController 中,我有一个按钮可以将记录设置为收藏或不收藏:

- (IBAction) setFavorite {
if (![record.ISFAV intValue]) 
[record setValue:[NSNumber numberWithInt:1] forKey:@"ISFAV"];

else 
[record setValue:[NSNumber numberWithInt:0] forKey:@"ISFAV"];

###SAVE ON THE CONTEXT HERE###

}

好的,你准备好了吗?如果我点击列表中的第一条记录,然后从收藏夹中添加或删除它,它会在 0.0046 秒内立即发生!带有 SQL 调试模式的控制台仅显示 UPDATE 语句:

CoreData: sql: BEGIN EXCLUSIVE CoreData: sql: UPDATE ZRECORD SET ZISFAV = ?, Z_OPT = ?在哪里 Z_PK = ?和 Z_OPT = ? 核心数据:sql:提交 CoreData:注解:sql执行时间:0.0046s

如果我快速滚动大列表(我显然在控制台上找到了批处理请求),当我点击一个包含许多批处理请求的记录并从收藏夹中添加\删除它时,很多很多很多(太多!我滚动的越多,它们就越多!)SELECT 语句出现在控制台中,位于 UPDATE 语句之前。这意味着总执行时间不可接受(uibutton 在 iphone 上冻结了很长时间)。

发生了什么事?该问题显然与批量获取请求有关。更多 fetch 请求 = 在 UPDATE 语句之前有更多 SELECT 语句。这是其中之一:

CoreData: sql: SELECT 0, t0.Z_PK, t0.Z_OPT, t0.ZCONTENT, t0.ZCONTENT2, t0.ZISUSER, t0.ZISFAV, t0.ZTITLE, t0.ZTITLE2, t0.ZID, t0 .ZAUTHOR, t0.ZCATEGORY FROM ZRECORD t0 WHERE t0.Z_PK IN (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,? ,?,?,?,?,?,?,?) ORDER BY t0.ZTITLE LIMIT 26

如果我删除 setFetchBatchSize,没有问题(但启动需要 16 秒)。似乎当我更新属性 ISFAV 时,CoreData 需要再次执行到达该记录所需的所有 fetchRequests,即使我将该记录作为对象传递给详细视图控制器。

抱歉,这篇文章太长了,我尽量说得更清楚。 非常感谢,我快把自己逼疯了……

【问题讨论】:

  • 让用户滚动浏览 17,000 行的表格是糟糕的设计,以至于您的用户会想要私刑。想象一下,在繁忙的人行道上用拇指滚动时,试图从 17,000 行中找到一行。您需要将其分解为表格的层次结构,以便点击三到四次将用户带到最多只有一百行左右的列表。这也将使您的表现更易于管理。

标签: iphone performance core-data nsfetchedresultscontroller


【解决方案1】:

发生的情况是,当您更新托管对象时,获取结果控制器会看到更改通知,并且它必须确定该对象的索引,以便它可以告诉其委托该对象已更新。为此,它会再次检查所有批次选择,直到找到正确的选择。我不确定为什么它不能只缓存这些信息,但显然不是。您是否尝试将部分缓存添加到获取的结果控制器?这可能会加快速度(取决于获取的结果控制器在这种情况下是否使用该缓存)。您只需在调用-initWithFetchRequest:managedObjectContext:sectionNameKeyPath:cacheName: 时指定缓存名称即可。

【讨论】:

  • 嗨凯文!非常感谢!当然,我认为你是对的。但我已经在使用缓存: NSFetchedResultsController *aFetchedResultsController = [[NSFetchedResultsController alloc] initWithFetchRequest:fetchRequest managedObjectContext:managedObjectContext sectionNameKeyPath:nil cacheName:@"Search"];
  • 在这种情况下,我认为您将不得不将其归结为 NSFetchedResultsController 中的限制。您应该为此提交bug report
【解决方案2】:

首先,我建议避免向用户显示包含 17K 条记录的大表;相反,您应该允许用户搜索记录,然后选择其中一个搜索结果。无论如何,如果您想允许用户直接从大表中选择一条记录,您需要考虑获取过程。

开始检查您是否已在 Core Data 模型中正确索引了用于设置 NSPredicate 关联到您的 NSFetchedResultsController 的属性。想想你的“工作集”记录的大小。这应该尽可能小,通常是数百条记录。

在您的情况下,将 setFetchBatchSize 设置为 25 可能不合适,因为您希望允许您的用户浏览 17K 记录。由于 17000:25 = 680,您将需要如此多的提取才能达到最新的 25 条记录。但是获取涉及到底层数据库的实际 I/O,以确保所有内容始终与其他线程完成的其他“可能的”插入/删除/更新操作同步。

即使您的应用程序不使用 Core Data 的多线程,Core Data 框架也必须检查以验证是否有更改。现在,由于 I/O 很昂贵,您需要进行权衡。将setFetchBatchSize 设置为 1000 在最坏的情况下需要 17 次获取才能达到最新的 1000 条记录(提高 40 倍),即使每次获取可能需要“稍微”更长的时间。

除非其他线程修改数据,否则按照建议使用缓存可能会带来一些好处。事实上,缓存命中非常快。但是,缓存未命中非常昂贵,需要 I/O 从数据库中获取相关数据。当多个线程同时在同一个数据库上工作时,缓存未命中的机会当然会增加(除非这些线程只读取记录)。

您可能想尝试的另一种可能的解决方案是使用多个线程。您的主线程仅获取初始数量的记录并将它们呈现给用户,而另一个使用不同托管对象上下文的线程在后台异步获取另一批记录(使用适当的偏移量)。然后将这些记录交给主线程进行可视化。

还有一件事。你不应该使用 KVC 来更新你的属性值;出于性能原因,最好这样做

record.ISFAV =  [NSNumber numberWithInt:1];

[record setISFAV:[NSNumber numberWithInt:1]];

仅更新一个属性您可能不会注意到差异,但如果您需要使用多个属性,您可能会开始体验到巨大的节省。

【讨论】:

  • +1 第一句话特别到位。有时,“我该怎么做”这个问题的正确答案是“不要那样做”。
  • 你能解释一下为什么使用 KVC 来更新属性是不好的吗?您失去了编译器支持,但开销应该相当小。 CoreData 已经根据需要动态生成访问器,我倾向于相信 KVC 会利用这一点。您是否有基准表明它实际上是严重的性能损失?
  • 使用 KVC 并没有错,但是,除了失去编译器支持之外,由于 KVC 内部结构,您还会产生开销,因为 KVC - 除了内部簿记 - 需要取消引用实际的访问器方法。这已在 WWDC 2010 会议 118“掌握核心数据”中进行了解释。有关优化的更多信息,请参阅第 137 节“在 iPhone OS 上优化核心数据性能”。
猜你喜欢
  • 2012-11-30
  • 1970-01-01
  • 2012-05-02
  • 1970-01-01
  • 1970-01-01
  • 2012-07-25
  • 2011-06-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多