以下是我的用例:
我需要一个大的核心数据存储导出到某些格式(例如,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循环代码是在单个线程中同步运行的,因此可能需要很长时间才能完成。当处理包含数千条记录的大型数据库时,尤其如此。
我想知道是否存在一种可以同时迭代数组的方法,从而可以充分利用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
,该方法返回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
在协调器块之外。将它们全部收集到一个大的字符串中进行批处理,并协调编写该字符串。请注意,批处理的字符串不会太大,因为它会存储在内存中。
请注意,这不会尝试以任何特定顺序放置导出文件。所有对象都被导出,但是顺序是不可预测的。由于你没有在问题中使用排序描述符,所以我猜这没有关系。如果是这样,则异步处理意味着你还有更多工作要做。
汤姆,非常感谢您提供详细的答案。非常感谢。