iOS CoreData:NSFetchedResultsController 性能

发布于 2024-09-25 02:40:23 字数 2273 浏览 2 评论 0原文

在模型中,我有两个实体:记录和类别。 Category与Record通过逆向关系是一对多的。持久存储是SQLITE类型,数据库也不是那么小,大约23MB(17k记录)。

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

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

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

天啊!

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

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

是的,那太好了!但事实并非如此!在列表视图控制器中,当用户点击记录时,我分配一个详细的视图控制器,并在 fetchedResultsController 中的索引路径处传递该记录:

- (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];
}

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

- (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 上冻结很长时间)。

发生什么事了?该问题显然与批量获取请求有关。更多的获取请求 = 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 需要再次执行到达该记录所需的所有 fetchRequest,即使我将该记录作为对象传递给详细视图控制器。

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

In the model, I have two entities: Record and Category. Category is one-to-many with Record through inverse relationship. The persistent store is of SQLITE type and the db is not so small, about 23MB (17k records).

I use a list-detail design to show the records table and the detailed record view.The list viewController uses NSFetchedResultsController.

Building on the device, if I don't use setFetchBatchSize:

CoreData: annotation: sql connection fetch time: 15.8800s
CoreData: annotation: total fetch execution time: 16.9198s for 17028 rows.

OMG!

If I use setFetchBatchSize:25, everything works great again:

CoreData: annotation: sql connection fetch time: 1.1736s
CoreData: annotation: total fetch execution time: 1.1900s for 17028 rows.

Yeah, that would be great! But it is not! In the list viewController, when user taps on a record I allocate a detailed viewController and I pass the record at the indexPath in the fetchedResultsController:

- (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];
}

NOW, in the detailed viewController, I have a button to set a record as favorite or not:

- (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###

}

OK, are u ready? If I tap on the first record in the list, then I add or remove it from the favorites, it happens in 0.0046 seconds, instantly! Console with SQL Debug mode shows only the UPDATE statement:

CoreData: sql: BEGIN EXCLUSIVE
CoreData: sql: UPDATE ZRECORD SET ZISFAV = ?, Z_OPT = ? WHERE Z_PK = ? AND Z_OPT = ?
CoreData: sql: COMMIT
CoreData: annotation: sql execution time: 0.0046s

If I scroll very fast the big list (and I obviously find the batch requests on the console), when I tap a record reached with many batch requests and I add\remove it from favorites, many many many many (too many! the more I scroll the more they are!) SELECT statements appears in the console before the UPDATE one. This means total execution time not acceptable (the uibutton freezes for a long time on the iphone).

What's happening? The problem is clearly related to the batched fetch requests. More fetch requests = more SELECT statements before the UPDATE statement. This is one of them:

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

If I remove the setFetchBatchSize, there's no problem (but startup requires 16 seconds). It seems that when I update the property ISFAV, CoreData needs to execute again all the fetchRequests that were needed to reach that record, even if I pass that record to the detail viewController as object.

Sorry for the long post, I tried to be as clearer as possible.
Thank you very much, I'm driving myself crazy...

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。

评论(2

痕至 2024-10-02 02:40:23

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

What's happening is the fetched results controller sees the change notification when you update the managed object, and it has to figure out what index that object was so it can tell its delegate that the object was updated. To do this, it's going through all the batch selects again until it can find the right one. I'm not sure why it can't just have this information cached, but obviously it isn't. Have you tried adding a section cache to the fetched results controller? That may possibly speed things up (depending on whether or not the fetched results controller uses that cache in this instance). You do so simply by specifying the cache name when you call -initWithFetchRequest:managedObjectContext:sectionNameKeyPath:cacheName:.

不寐倦长更 2024-10-02 02:40:23

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

开始检查您是否已在核心数据模型中正确索引用于设置与 NSFetchedResultsController 关联的 NSPredicate 的属性。考虑一下您的“工作集”记录的大小。这应该尽可能小,并且通常是数百条记录的数量级。

在您的情况下,考虑到您希望允许用户浏览 17K 记录,将 setFetchBatchSize 设置为 25 可能不合适。由于 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]];

仅更新单个属性的操作,您可能不会注意到差异,但如果您需要使用多个属性,则可能会开始节省大量成本。

First, I would suggest to avoid showing to the user a big table with 17K records; instead you should allow the user searching for records and then selecting one of the search results. Anyway, if you want to allow the user selecting a record directly from the big table, you need to think about the fetching process.

Start checking that you have properly indexed in your Core Data model the attributes you use to setup the NSPredicate associate to your NSFetchedResultsController. Think about the size of your "working set" of records. This should be as small as possible, and is usually in the order of hundreds of records.

In your case, setting setFetchBatchSize to 25 is probably not appropriate, given that you want to allow your user browsing 17K records. Since 17000:25 = 680, you will need that many fetches to reach the latest 25 records. But fetching involves actual I/O to the underlying database to make sure that everything is always in sync with other "possible" insert/delete/update operations done by other threads.

Even if your application does not use multiple threads with Core Data, the Core Data framework must check to verify if something changed. Now, since I/O is expensive, you need a tradeoff. Setting setFetchBatchSize to 1000 will require in the worst case 17 fetches to reach the latest 1000 records (improving by a factor of 40) even though each individual fetch may take "slightly" longer.

Using the cache as suggested may provide some benefit unless other threads modify the data. Indeed, cache hits are fast, very fast. However, cache misses are extremely expensive, requiring I/O to fetch the associated data from the database. The chance of cache misses increases of course when multiple threads work simultaneously on the same database (unless these threads only read records).

Another possible solution you may want to try, involves using multiple threads. Your main thread only fetches an initial number of records and presents them to the user while another thread using a different managed object context fetches another batch of records asynchronously, in background (using a proper offset). These records are then handed over to the main thread for visualization.

One more thing. You should not use KVC to update the value of your attributes; for performance reasons it is much better to do something like

record.ISFAV =  [NSNumber numberWithInt:1];

or

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

Updating just a single attribute you may not notice a difference, but if you need to work with several attributes, you may start experiencing huge savings.

~没有更多了~
我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
原文