使用Core Data高效地展示10万个项目

5

我正在使用NSFetchResultsController在UITableView中显示超过100,000条记录。虽然可以正常工作,但速度非常慢,尤其是在iPad 1上。加载需要7秒钟,这对我的用户来说是一种折磨。

我还希望能够使用分区,但这会至少再增加3秒的加载时间。

以下是我的NSFetchResultsController:

- (NSFetchedResultsController *)fetchedResultsController {

    if (self.clientsController != nil) {
        return self.clientsController;
    }

    NSFetchRequest *request = [[NSFetchRequest alloc] init];
    NSEntityDescription *entity = [NSEntityDescription entityForName:@"Client" inManagedObjectContext:self.managedObjectContext];
    [request setEntity:entity];
    [request setPredicate:[NSPredicate predicateWithFormat:@"ManufacturerID==%@", self.manufacturerID]];
    [request setFetchBatchSize:25];

    NSSortDescriptor *sort = [[NSSortDescriptor alloc]  initWithKey:@"UDF1" ascending:YES];
    NSSortDescriptor  *sort2= [[NSSortDescriptor alloc] initWithKey:@"Name" ascending:YES];
    [request setSortDescriptors:[NSArray arrayWithObjects:sort, sort2,nil]];

    NSArray *propertiesToFetch = [[NSArray alloc] initWithObjects:@"Name", @"ManufacturerID",@"CustomerNumber",@"City", @"StateProvince",@"PostalCode",@"UDF1",@"UDF2", nil];
    [request setPropertiesToFetch:propertiesToFetch];

    self.clientsController =
    [[NSFetchedResultsController alloc] initWithFetchRequest:request
                                        managedObjectContext:self.managedObjectContext sectionNameKeyPath:nil
                                                   cacheName:nil];

    return self.clientsController;

}

我在ManufacturerID上创建了一个索引,并用它来进行NSPredicate操作。这似乎是一个非常基本的NSFetchRequest - 有什么方法可以加速它吗?或者我只是达到了限制?我一定是漏了些什么。


你有一面墙那么大的屏幕? - vonbrand
@vonbrand 不确定这是什么意思? - Slee
显示 100,000 条数据需要 巨大 的空间。 - vonbrand
它是一个在UITableView中滚动的列表。 - Slee
然后,您想要显示一系列不断变化的行,这与所要求的非常不同... - vonbrand
2个回答

7
首先,您可以使用NSFetchedResultsController的缓存来加速第一次获取后的显示。这样应该很快降到不到一秒的水平。
其次,您可以尝试仅在屏幕上显示第一页内容,然后在后台抓取其余内容。我是通过以下方式实现的:
  • 当视图出现时,请检查是否有第一页缓存。
  • 如果没有,我会获取第一页。您可以通过设置获取请求的fetchLimit来实现此目的。
    • 如果您正在使用部分,则进行两次快速抓取以确定第一个部分标头和记录。
  • 在后台线程中使用长时间获取来填充第二个获取结果控制器。
    • 您可以创建一个子上下文并使用performBlock:
    • 使用dispatch_async()
  • 将第二个FRC分配给表视图并调用reloadData
这在我最近的一个项目中非常好地工作,记录数量超过200K。

1
这是我之前在脑海中想到的,但还没有尝试实现,我觉得他们无法比我更快地滚动列表,因为我可以在后台加载更多。你使用了两个单独的FRC吗?如果是的话,我假设在第二个FRC加载后设置了某个标志来切换表格所提供的FRC - 这听起来正确吗? - Slee
嗨Mundi,我想问一下,你如何使用NSFetchedResultsController在后台执行获取操作。因为我尝试过,但没有成功 - https://dev59.com/rcDqa4cB1Zd3GeqPUQff - Cheok Yan Cheng

1

我知道@Mundi提供的答案已经被接受了,但是我试着实现它时遇到了问题。具体来说,第二个FRC创建的对象将基于其他线程的ManagedObjectContext。由于这些对象不是线程安全的,并属于其他线程上自己的MOC,我找到的解决方案是在加载时将对象置为“fault”。因此,在cellForRowAtIndexPath中,我添加了这行代码:

NSManagedObject *object = [self.fetchedResultsController objectAtIndexPath:indexPath];
object = (TapCellar *)[self.managedObjectContext existingObjectWithID:[object objectID] error:nil];

您需要一个正确线程的对象。另外一个注意事项是,您对对象所做的更改不会反映在后台MOC中,因此您需要进行协调。我的做法是将后台MOC设置为私有队列MOC,前台MOC是其子类,如下所示:

NSPersistentStoreCoordinator *coordinator = [self persistentStoreCoordinator];
if (coordinator != nil) {
  _privateManagedObjectContext = [[NSManagedObjectContext alloc] initWithConcurrencyType:NSPrivateQueueConcurrencyType];
  [_privateManagedObjectContext setPersistentStoreCoordinator:coordinator];

  _managedObjectContext = [[NSManagedObjectContext alloc] initWithConcurrencyType:NSMainQueueConcurrencyType];
  [_managedObjectContext setParentContext:_privateManagedObjectContext];

}

现在当我在主线程进行更改时,我可以通过这样轻松地进行调和:
if ([self.managedObjectContext hasChanges]) {
   [self.managedObjectContext performBlockAndWait:^{
      NSError *error = nil;
      ZAssert([self.managedObjectContext save:&error], @"Error saving MOC: %@\n%@",
             [error localizedDescription], [error userInfo]);
   }];
}

在这一点上,我会等待它的返回,因为我要重新加载表格数据,但如果您愿意,可以选择不等待。尽管通常只更改一个或两个记录,但即使有30K+记录,该过程也非常快速。

希望这能帮助那些陷入困境的人!


1
很不幸,这仍然不是线程安全的。如果在 cellForRowAtIndexPath 中调用 [self.fetchedResultsController objectAtIndexPath:indexPath],那么你就会跨越线程边界。不能保证你请求的 indexPath 仍然指向同一个对象,因为在后台线程中,获取结果控制器正在监听托管对象上下文的更改并相应地更新自己。 - stevesw

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接