如何同时迭代 NSManagedObject 项目的集合以提高性能?
How to iterate a collection of NSManagedObject items concurrently for boosting performance?
以下是我的用例:
我需要将大型核心数据存储导出为某种格式(例如,CSV
、JSON
),这需要获取主实体的所有对象,然后迭代每个对象并将其序列化到所需的格式。这是我的代码:
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
的调用在协调器块之外。将它们全部收集到一个大字符串中,并协调写入该字符串。小心批处理的字符串不要太大,因为它会在内存中。
请注意,这不会尝试以任何特定顺序放置导出文件。所有对象都被导出,但顺序是不可预测的。由于您没有在问题中使用排序描述符,所以我猜这无关紧要。如果是,异步处理意味着您将有更多工作要做。
以下是我的用例:
我需要将大型核心数据存储导出为某种格式(例如,CSV
、JSON
),这需要获取主实体的所有对象,然后迭代每个对象并将其序列化到所需的格式。这是我的代码:
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
的调用在协调器块之外。将它们全部收集到一个大字符串中,并协调写入该字符串。小心批处理的字符串不要太大,因为它会在内存中。
请注意,这不会尝试以任何特定顺序放置导出文件。所有对象都被导出,但顺序是不可预测的。由于您没有在问题中使用排序描述符,所以我猜这无关紧要。如果是,异步处理意味着您将有更多工作要做。