【问题标题】:How to iterate a collection of NSManagedObject items concurrently for boosting performance?如何同时迭代 NSManagedObject 项的集合以提高性能?
【发布时间】:2021-03-11 13:18:09
【问题描述】:

以下是我的用例:

我需要将大型核心数据存储导出为某种格式(例如,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]; 
} 

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

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

【问题讨论】:

    标签: ios performance core-data concurrency nsmanagedobject


    【解决方案1】:

    您必须分批处理它们,其中每个批次都由单独的后台上下文获取,并且导出发生在该上下文的队列中。对于名为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 的调用位于协调器块之外。将它们全部收集到批处理的一个大字符串中,并协调编写该字符串。不过要注意批处理的字符串不要太大,因为它会在内存中。

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

    【讨论】:

    • 汤姆,非常感谢您的详细回答。非常感谢。
    猜你喜欢
    • 2021-07-04
    • 2014-01-06
    • 1970-01-01
    • 2022-01-02
    • 2012-06-17
    • 1970-01-01
    • 2015-09-07
    • 2023-01-12
    • 1970-01-01
    相关资源
    最近更新 更多