【问题标题】:Core Data Concurrency Import Performance核心数据并发导入性能
【发布时间】:2016-04-10 15:43:45
【问题描述】:

我正在开发一个连接到网络服务的应用程序,该服务在应用程序启动期间检索大量数据。我使用并发来避免 UI 阻塞。我选择了以下核心数据堆栈模式:background private moc —> main moc —> writer private moc —> coordinator —> file。

在导入操作时会出现问题。 CPU 已 100% 使用,应用程序在此过程中变慢。我处理了 300 个对象的批次,总共导入了大约 10,000 个对象。

对于每个批次,都会创建一个 NSOperation,并带有一个关联的临时 moc,即背景 moc 的子代。操作在 NSOperationQueue 中排队。 导入作业完成后,应用程序会变得更慢,具体取决于正在运行的作业数量。我还注意到,当应用程序被终止并重新启动时,它确实更加可用和快速。

导入时我的内存占用在 40Mo 和 60Mo 之间变化。是不是觉得太过分了?

您认为我的堆栈模式是否适合我的需求?我应该迁移到有 2 个协调器的堆栈吗?

此外,在获取要在 tableView 中显示的数据时,我应该在显示视图之前立即使用 performBlockAndWait 获取数据吗?

感谢您的帮助

【问题讨论】:

  • 你能贴一些代码吗?
  • 永远不要让作家通过主 moc 到达协调员。保存堆栈时,您将失去所有性能提升。
  • 感谢您的回答。所以我应该设置 writer context 的 background context child 并让 main context 直接链接到 coordinator ?但是根据@MattMorey 的说法,作者应该是主要speakerdeck.com/player/360691a030570131a0a76af09d9fc329# 的父母,错了吗?
  • @Avi,他的作者上下文没有通过主要上下文。根据他的描述,他的堆栈是正常的父子设置。
  • @MarcusS.Zarra,我的意思是执行导入的线程。它本质上是这个过程的作者。

标签: ios objective-c performance core-data concurrency


【解决方案1】:

您所描述的堆栈很好。

CPU 使用率可能会产生误导。您要确保您不在主线程上,因为这会导致您的应用程序运行缓慢和/或卡顿。

当您在 Instruments 中观看您的应用时,最耗时的是什么?在主队列上花费了多少时间?

一般来说,导入不应导致 CPU 处于 100% 的状态。如果您从后台线程执行此操作,则很可能会获得一些性能提升。

如果应该共享您的导入代码和/或 Instruments 跟踪,以便我可以看到发生了什么。

【讨论】:

  • 我会尽快为您提供更多关于仪器的详细信息。感谢您的帮助。
【解决方案2】:

我认为您的设置有问题。您声明后台托管对象上下文的子线程是主线程,并且您创建了要导入的此类子线程。这势必会导致 UI 故障。

另外,我认为依赖NSOperation 是不必要的过度设计。您应该改用 NSManagedObjectContext 块 API。

我推荐的设置是:

RootContext (background, writing to persistent store) -> parent of
MainContext (foreground, UI) -> parent of
WorkerContext(s) (background, created and discarded ad hoc)

您可以在 Web 调用的回调中创建工作人员上下文,以完成导入的繁重工作。确保您使用的是块 API 并将所有对象限制在本地上下文中。您save 将更改推送到主线程的上下文(在将数据保存到存储之前已经可以开始显示数据),并定期保存主上下文和编写器上下文,始终使用 bock API。

这样一个典型的saveContext函数,可以称为线程安全(这里self指的是数据管理器单例或应用委托):

func saveContext () {
    if self.managedObjectContext.hasChanges  {
       self.managedObjectContext.performBlocAndWait {
            do { try self.managedObjectContext.save() }
            catch let error as NSError {
                print("Unresolved error while saving main context \(error), \(error.userInfo)")
            }
        }
        self.rootContext.performBlockAndWait {
            do { try self.rootContext.save() }
            catch let error as NSError {
                print("Unresolved error while saving to persistent store \(error), \(error.userInfo)")
            }
        }
    }
}

【讨论】:

  • 我的堆栈与您所指的相同。唯一的区别是,我使用工人作为现有背景上下文的孩子(主要的孩子)。另一方面,我会尝试删除 NSOperation 以进行导入,因为我认为您对过度工程的看法是正确的。我也将块 API 用于与核心数据相关的所有工作,但我不太喜欢 performBlockAndWait 来保存方法,因为我认为阻止调用线程对于该工作来说不是必需的,不是吗?
【解决方案3】:

经过几天的测试和仪器跟踪,我可以给你更多的细节。以下 sn-p 显示了我如何从共享实例中保存我的上下文(基于父/子模式):

- (void)save {
    [self.backgroundManagedObjectContext performBlockAndWait:^{
        [self saveContext:self.backgroundManagedObjectContext];

        [self.mainManagedObjectContext performBlock:^{
            [self saveContext:self.mainManagedObjectContext];

            [self.writerManagedObjectContext performBlock:^{
                [self saveContext:self.writerManagedObjectContext];
            }];
        }];
     }];
}

- (void)saveContext:(NSManagedObjectContext*)context {
     NSError *error = nil;

     if ([context hasChanges] && ![context save:&error]) {
         NSLog(@"Unresolved error %@, %@", error, [error userInfo]);
     }
}

然后,由于同步操作,每个导入作业都在后台上下文中执行。以下方法在 main 操作中触发。

- (void)operationDidStart
{
    NSManagedObjectContext *moc = self.context;
    NSMutableArray *insertedOrUpdatedObjects = [NSMutableArray array];
    NSMutableArray *subJSONs = [NSMutableArray array];
    NSUInteger numberOfJobs = ceil((double)self.JSONToImport.count/self.batchSize);

    for (int i = 0; i < numberOfJobs; i++) {
        NSUInteger startIndex = i * self.batchSize;
        NSUInteger count = MIN(self.JSONToImport.count - startIndex, self.batchSize);
        NSArray *arrayRange = [self.JSONToImport subarrayWithRange:NSMakeRange(startIndex, count)];
        [subJSONs addObject:arrayRange];
    }

    __block NSUInteger  numberOfEndedJobs = 0;

    for (NSArray *subJSON in subJSONs) {
        [moc performBlock:^{
            [self startJobWithJSON:subJSON context:moc completion:^(NSArray *importedObjects, NSError *error) {

                numberOfEndedJobs++;

                if (!error && importedObjects && importedObjects.count > 0) {
                    [insertedOrUpdatedObjects addObjectsFromArray:importedObjects];
                }

                if (numberOfEndedJobs == numberOfJobs) {
                    [[CoreDataManager manager] save];

                    if (self.operationCompletion) {
                        self.operationCompletion(self, insertedOrUpdatedObjects, error);
                    }
                }
            }];
        }];
    }
}

如您所见,我将我的导入分段(500 个)。该操作在后台上下文队列上执行每个批处理,并在所有批处理结束时保存我的堆栈。

由于 Time Profiler,save 方法似乎占用了每个线程 23% 的 CPU 使用率。

希望尽可能清楚。

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2012-02-06
    • 2018-01-02
    • 2011-08-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多