Warm tip: This article is reproduced from serverfault.com, please click

ios-如何同时迭代NSManagedObject项的集合以提高性能?

(ios - How to iterate a collection of NSManagedObject items concurrently for boosting performance?)

发布于 2020-11-29 11:37:12

以下是我的用例:

我需要一个大的核心数据存储导出到某些格式(例如,CSVJSON),它要求提取所述主实体的所有对象,然后迭代每个对象,并将其序列化到所需的格式。这是我的代码:

NSError *error = nil;
NSFetchRequest *request = [NSFetchRequest fetchRequestWithEntityName:@"MyEntity"];
NSArray<NSManagedObject *> *allItems = [managedObjectContext executeFetchRequest:request error:&error];
for (NSManagedObject *item in allItems) {
    [self exportItem:item];
}

由于for循环代码是在单个线程中同步运行的,因此可能需要很长时间才能完成。当处理包含数千条记录的大型数据库时,尤其如此。

我想知道是否存在一种可以同时迭代数组的方法,从而可以充分利用iOS设备上可用的多个内核。这可能会大大提高性能。

我正在考虑使用以下代码替换上面的for循环代码:

[allItems enumerateObjectsWithOptions:NSEnumerationConcurrent usingBlock:^(NSManagedObject* item) { 
    [self exportItem:item]; 
} 

但是,这显然会因为违反核心数据并发规则而使应用程序崩溃。

我想知道是否有针对此用例的对象。

Questioner
Joshua
Viewed
0
Tom Harrington 2020-12-05 07:58:24

你必须分批处理它们,每个批处理都由单独的后台上下文获取,并且导出在该上下文的队列中进行。对于名为的实体,这是你可以执行此操作的一种方法Event通用方法是获取要导出的所有对象的对象ID,然后将它们分成可分别由单独的后台上下文处理的组。

由于受管对象无法跨队列工作,因此首先要获取对象ID并将其分成多个批次。首先获取所有对象ID。

NSManagedObjectContext *context = [self.fetchedResultsController managedObjectContext];
NSFetchRequest<Event *> *fetchRequest = Event.fetchRequest;
fetchRequest.resultType = NSManagedObjectIDResultType;
NSError *fetchError = NULL;

NSArray<NSManagedObjectID *> *allObjectIDs = [context executeFetchRequest:fetchRequest error:&fetchError];

然后使用子范围遍历该数组。对于每个批次,创建一个新的背景上下文。使用该上下文来获取该批对象ID的托管对象。然后处理导出托管对象。

NSInteger batchSize = 100;
NSRange currentRange = NSMakeRange(0, batchSize);
AppDelegate *appDelegate = (AppDelegate *) [[UIApplication sharedApplication] delegate];
NSPersistentContainer *persistentContainer = appDelegate.persistentContainer;

while (currentRange.location < allObjectIDs.count) {
    NSArray<NSManagedObjectID *> *batchObjectIDs = [allObjectIDs subarrayWithRange:currentRange];

    NSManagedObjectContext *batchContext = persistentContainer.newBackgroundContext;
    [batchContext performBlock:^{
        NSFetchRequest<Event *> *fetchRequest = Event.fetchRequest;
        fetchRequest.predicate = [NSPredicate predicateWithFormat:@"self in %@", batchObjectIDs];
        NSError *fetchError = NULL;
        NSArray <Event *> *batchEvents = [batchContext executeFetchRequest:fetchRequest error:&fetchError];


        // Put your export code here, for the objects that were just fetched.


    }];
    
    
    currentRange.location += batchSize;
}

你应该尝试批量大小,看看哪种最适合你。

但是,这变得比较棘手,因为你的导出代码可能同时在多个队列上运行,并且你需要确保导出文件不会以损坏的文件结尾。解决该问题的一种方法是NSFileCoordinator确保每次只允许写入一个队列。在上面的循环之前创建协调器:

NSFileCoordinator *coordinator = [[NSFileCoordinator alloc] init];

然后在上面的代码说要放置导出代码的地方,执行以下操作:

        [coordinator coordinateWritingItemAtURL:[self exportFileURL] options:0 error:&coordinatorError byAccessor:^(NSURL * _Nonnull newURL) {
            NSFileHandle *exportHandle = [self createExportFileHandle];
            for (Event *event in batchEvents) {
                NSData *exportData = [[event exportString] dataUsingEncoding:NSUTF8StringEncoding];
                NSError *writeError = NULL;
                [exportHandle writeData:exportData error:&writeError];
                if (writeError != NULL) {
                    NSLog(@"Write error: %@", writeError);
                }
            }
        }];

该代码假定你有一个名为的方法exportFileURL该方法返回NSURL要导出数据的位置的。它还假定你的托管对象有一个名为的方法exportString该方法返回要为对象导出的任何字符串。createExportFileHandle方法使用并且(exportFileURL这很重要)在写入之前先查找文件的末尾。就像是

- (NSFileHandle *)createExportFileHandle {
    NSError *error = NULL;
    if (![[NSFileManager defaultManager] fileExistsAtPath:[[self exportFileURL] path]]) {
        [[NSFileManager defaultManager] createFileAtPath:[[self exportFileURL] path] contents:nil attributes:nil];
    }
    NSFileHandle *handle = [NSFileHandle fileHandleForWritingToURL:self.exportFileURL error:&error];
    [handle seekToEndOfFile];
    return handle;
}

你需要在文件协调器块内创建句柄,因为文件位置的末尾不断变化,并且你希望在开始写入数据之前获取当前的句柄。

需要协调文件访问可能会限制你从中获得多少加速。这可能会得到改善。例如,对代码进行重做,以使对的调用exportString在协调器块之外。将它们全部收集到一个大的字符串中进行批处理,并协调编写该字符串。请注意,批处理的字符串不会太大,因为它会存储在内存中。

请注意,这不会尝试以任何特定顺序放置导出文件。所有对象都被导出,但是顺序是不可预测的。由于你没有在问题中使用排序描述符,所以我猜这没有关系。如果是这样,则异步处理意味着你还有更多工作要做。