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

How to iterate a collection of NSManagedObject items concurrently for boosting performance?

以下是我的用例:

我需要将大型核心数据存储导出为某种格式(例如,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-loop 代码是 运行 在单个线程中同步进行的,因此可能需要很长时间才能完成。在处理包含数千条记录的大型数据库时尤其如此。

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

我的思路是用下面的代码代替上面的for循环代码:

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

但是,由于违反核心数据并发规则,这显然会使应用程序崩溃...

不知道有没有针对这个用例的。

您必须分批处理它们,其中每个批次都由单独的后台上下文获取,并且导出发生在该上下文的队列中。对于名为 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 的方法,该方法 returns 一个 NSURL 您要导出数据的位置。它还假定您的托管对象有一个名为 exportString 的方法,该方法 returns 您想要为对象导出的任何字符串。 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 的调用在协调器块之外。将它们全部收集到一个大字符串中,并协调写入该字符串。小心批处理的字符串不要太大,因为它会在内存中。

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