我已经仔细阅读了SO上的所有相关线程,但仍然对如何从多个线程更改核心数据对象感到困惑,而不必在每次更改后保存。
我正在开发一款不断与服务器对话的应用。该应用程序使用Core Data进行存储,并在少数视图控制器中使用NSFetchedResultsController
从持久性存储中获取数据。通常,当用户执行操作时,将触发网络请求。在发送网络请求之前,通常应对相关的Core Data对象进行一些更改,并且在服务器响应时,将对这些Core Data对象进行更多更改。
最初,所有Core Data操作都在同一NSManagedObjectContext
的主线程上完成。一切都很顺利,除非网络流量很高,应用程序可能会在几秒钟内无响应。显然这是不可接受的,所以我考虑将一些Core Data操作移到后台运行。
我尝试的第一种方法是创建一个NSOperation对象来处理每个网络响应。在NSOperation对象的主要方法中,我设置了一个专用的MOC,进行了一些更改,并在最后提交了更改。
- (void)main
{
@try {
NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];
// Create a dedicated MOC for this NSOperation
NSManagedObjectContext * context = [[NSManagedObjectContext alloc] init];
[context setPersistentStoreCoordinator:[APP_DELEGATE persistentStoreCoordinator]];
// Make change to Core Data objects
// ...
// Commit the changes
NSError *error = nil;
if ([context hasChanges] && ![context save:&error]) {
NSLog(@"Unresolved error %@, %@", error, [error userInfo]);
}
// Release the MOC
[context release];
// Drain the pool
[pool drain];
}
@catch (NSException *exception) {
// Important that we don't rethrow exception here
NSLog(@"Exception: %@", exception);
}
}
主线程上的MOC已注册NSManagedObjectContextDidSaveNotification
。
[[NSNotificationCenter defaultCenter] addObserver:self
selector:@selector(backgroundContextDidSave:)
name:NSManagedObjectContextDidSaveNotification
object:nil];
因此,当后台上下文提交更改时,将通知主MOC,然后将合并更改:
- (void)backgroundContextDidSave:(NSNotification *)notification
{
// Make sure we're on the main thread when updating the main context
if (![NSThread isMainThread]) {
[self performSelectorOnMainThread:@selector(backgroundContextDidSave:) withObject:notification waitUntilDone:NO];
return;
}
// Merge the changes into the main context
[[self managedObjectContext] mergeChangesFromContextDidSaveNotification:notification];
}
但是,正如我之前提到的,我还需要对主MOC中的Core Data对象进行更改。每个更改通常都非常小(例如,更新对象中的一个实例变量),但可能有很多变化。所以我真的不想在每次改变后都保存主要的MOC。但是,如果我不这样做,我在将更改从后台MOC合并到主MOC时会遇到问题。由于两个MOC都有未保存的更改,因此会发生合并冲突。设置合并策略也没有帮助,因为我想保留两个MOC的更改。
一种可能性是将背景MOC注册到NSManagedObjectContextDidSaveNotification
,但这种方法对我来说闻起来很糟糕。每次改变后我仍然需要保存主要的MOC。
我尝试的第二种方法是在永久后台线程上运行的专用后台上下文中进行所有Core Data更改。
- (NSThread *)backgroundThread
{
if (backgroundThread_ == nil) {
backgroundThread_ = [[NSThread alloc] initWithTarget:self selector:@selector(backgroundThreadMain) object:nil];
// Actually start the thread
[backgroundThread_ start];
}
return backgroundThread_;
}
// Entry point of the background thread
- (void)backgroundThreadMain
{
NSAutoreleasePool * pool = [[NSAutoreleasePool alloc] init];
// We can't run the runloop unless it has an associated input source or a timer, so we'll just create a timer that will never fire.
[NSTimer scheduledTimerWithTimeInterval:DBL_MAX target:self selector:@selector(ignore) userInfo:nil repeats:YES];
[[NSRunLoop currentRunLoop] run];
// Create a dedicated NSManagedObjectContext for this thread.
backgroundContext_ = [[NSManagedObjectContext alloc] init];
[backgroundContext_ setPersistentStoreCoordinator:[self persistentStoreCoordinator]];
[pool drain];
}
因此,每当我需要从主线程进行Core Data更改时,我必须从主线程获取objectID,并传递给后台线程以执行更改。保存后台上下文后,更改将合并回主MOC。
- (void)addProduct:(Product *)product toCatalog:(Catalog *)catalog;
将更改为:
- (void)addProduct:(NSManagedObjectID *)productObjectId toCatalog:(NSManagedObjectID *)catalogObjectId
{
NSArray * params = [NSArray dictionaryWithObjects:productObjectId, catalogObjectId, nil];
[self performSelector:(addProductToCatalogInBackground:) onThread:backgroundThread_ withObject:params waitUntilDone:NO];
}
但这似乎令人费解和丑陋。编写这样的代码似乎否定了首先使用Core Data的有用性。此外,我仍然需要在每次更改后保存MOC,因为我无法获得新对象的objectId而不将其首先保存到数据存储区。
我觉得我在这里遗漏了一些东西。我真的希望有人可以对此有所了解。感谢。
答案 0 :(得分:2)
NSManagedObjectContext
只是一个便笺簿。保存它的行为将一个便笺簿中的更改向下移动到NSPersistentStoreCoordinator
,并可能下移到磁盘。 MOC可以通过NSPersistentStoreCoordinator
了解来自另一个MOC的变化的唯一方法。因此需要保存。但是,在iOS的下一个版本中,节省的成本要低得多。
如果您必须符合iOS4或更低版本,则保存是唯一的选择。但是,根据应用程序的设计,您可以加载保存并减少它们的使用频率。如果要导入数据,则在导入完成时保存或在导入内部以逻辑单位保存。每次进入后都没有必要保存,这很浪费。
顺便说一下,我建议使用NSOperation
个实例,而不是直接使用NSThread
个实例。它们更易于使用并且性能更好。
此外,您不需要在try / catch块中包装Objective-C代码。很少有事情会引发异常;特别是在iOS上。
最后,我建议您查看有关在后台线程中导入的my post on CIMGF。