ios - 多个 NSManagedObjectContexts 问题

标签 ios objective-c multithreading core-data

我在实现两个 NSManagedObjectContext 之间的父/子关系时遇到问题。我的应用程序从 Web 服务导入大量数据,这些数据在保存上下文时导致 UI 滞后。因此,在我的 AppDelegate 中,我使用 NSPrivateQueueConcurrencyType 创建了一个父上下文(master):

- (NSManagedObjectContext *)masterMOC
{
  if (_masterMOC != nil) {
      return _masterMOC;
  }

  NSPersistentStoreCoordinator *coordinator = [self persistentStoreCoordinator];
  if (coordinator != nil) {
    _masterMOC = [[NSManagedObjectContext alloc] initWithConcurrencyType:NSPrivateQueueConcurrencyType];
    [_masterMOC setPersistentStoreCoordinator:coordinator];
  }
  return _masterMOC;
}

...和一个带有 NSMainQueueConcurrencyType 的子上下文(主)。我将主 MOC 的 parentContext 设置为 masterMOC:

- (NSManagedObjectContext *)mainMOC
{
  if (_mainMOC != nil) {
    return _mainMOC;
  }

  _mainMOC = [[NSManagedObjectContext alloc] initWithConcurrencyType:NSMainQueueConcurrencyType];
  [_mainMOC setUndoManager:nil];
  [_mainMOC setParentContext:[self masterMOC]];

  return _mainMOC;
}

在我的 applicationDidFinishLaunching 中,我启动了一个导入操作,该操作查询 Web 服务并将结果保存在主 (PrivateQueue) 上下文中。我还注册了 NSManagedObjectContextDidSaveNotification 并尝试将这些更改合并到子 mainMOC 中。

- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions
{
  NSNotificationCenter *notificationCenter = [NSNotificationCenter defaultCenter];
  [notificationCenter addObserver:self selector:@selector(contextChanged:) name:NSManagedObjectContextDidSaveNotification object:self.masterMOC];

  RequestHandler *handler = [[RequestHandler alloc] initWithManagedObjectContext:self.masterMOC];
  [handler importAllViews];

  ...

  return YES;
}

- (void) contextChanged: (NSNotification *) notification
{
  // Only interested in merging from master into main.
  if ([notification object] != self.masterMOC) return;

  [self.mainMOC performBlock:^{
    [self.mainMOC mergeChangesFromContextDidSaveNotification:notification];
  }];
}

RequestHandler 类有一个 saveContext 方法,可以在正确的线程上保存主上下文:

@implementation KDBRequestHandler

...

- (void) saveContext
{    
  [self.managedObjectContext performBlock:^{
    NSError *error;
    if (![self.managedObjectContext save:&error]) {
        [NSException raise:@"Unable to save build details." format:@"Error saving context: %@", error];
    }
  }];
}

... 

@end

我已经验证了导入正确地将其对象保存在 Instruments 的后台线程上。 我的问题在于具有 NSMainQueueConcurrencyType 的子托管对象上下文。导入开始后,应用程序 didFinishLaunching 按标准初始化 UI。 View Controller 分配有 mainMOC。

- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions
{
  // Override point for customization after application launch.
  if ([[UIDevice currentDevice] userInterfaceIdiom] == UIUserInterfaceIdiomPad) {
    UISplitViewController *splitViewController = (UISplitViewController *)self.window.rootViewController;
    UINavigationController *navigationController = [splitViewController.viewControllers lastObject];
    splitViewController.delegate = (id)navigationController.topViewController;

    UINavigationController *masterNavigationController = splitViewController.viewControllers[0];
    KDBMasterViewController *controller = (KDBMasterViewController *)masterNavigationController.topViewController;
    controller.managedObjectContext = self.mainMOC;
  } else {
    UINavigationController *navigationController = (UINavigationController *)self.window.rootViewController;
    KDBMasterViewController *controller = (KDBMasterViewController *)navigationController.topViewController;
    controller.managedObjectContext = self.mainMOC;
  }

  return YES;
}

MasterViewController 本质上是在创建核心数据项目时为您创建的样板 Controller 。它的 fetchedResultsController 最终在 mainMOC 上执行其工作,因此是主线程。

- (NSFetchedResultsController *)fetchedResultsController
{
  if (_fetchedResultsController != nil) {
    return _fetchedResultsController;
  }

  NSFetchRequest *fetchRequest = [[NSFetchRequest alloc] init];
  // Edit the entity name as appropriate.
  NSEntityDescription *entity = [NSEntityDescription entityForName:@"Job" inManagedObjectContext:self.managedObjectContext];
  [fetchRequest setEntity:entity];

  // Set the batch size to a suitable number.
  [fetchRequest setFetchBatchSize:20];

  // Edit the sort key as appropriate.
  NSSortDescriptor *sortDescriptor = [[NSSortDescriptor alloc] initWithKey:@"name" ascending:NO];
  NSArray *sortDescriptors = @[sortDescriptor];

  [fetchRequest setSortDescriptors:sortDescriptors];

  // Edit the section name key path and cache name if appropriate.
  // nil for section name key path means "no sections".
  NSFetchedResultsController *aFetchedResultsController = [[NSFetchedResultsController alloc] initWithFetchRequest:fetchRequest managedObjectContext:self.managedObjectContext sectionNameKeyPath:nil cacheName:@"Master"];
  aFetchedResultsController.delegate = self;
  self.fetchedResultsController = aFetchedResultsController;

  NSError *error = nil;
  if (![self.fetchedResultsController performFetch:&error]) {
    // Replace this implementation with code to handle the error appropriately.
    // abort() causes the application to generate a crash log and terminate. You should not use this function in a shipping application, although it may be useful during development.
    NSLog(@"Unresolved error %@, %@", error, [error userInfo]);
    abort();
  }

  return _fetchedResultsController;
}

在初始加载时,当数据库是新鲜且空的时,一切都会按预期进行。数据在后台下载并按预期填充到 MasterViewController 的 UITableView 中。但是在后续运行中,应用程序经常在 MasterViewController 的 fetchedResultsController 中崩溃。提取失败并出现此错误:

*** Terminating app due to uncaught exception 'NSInternalInconsistencyException', reason: '+entityForName: could not locate an NSManagedObjectModel for entity name 'Job''
*** First throw call stack:
(
0   CoreFoundation                      0x01c6a1e4 __exceptionPreprocess + 180
1   libobjc.A.dylib                     0x019678e5 objc_exception_throw + 44
2   CoreData                            0x002c6a1b +[NSEntityDescription entityForName:inManagedObjectContext:] + 251
3   JMobile                       0x00032ad4 -[KDBMasterViewController fetchedResultsController] + 340
4   JMobile                       0x00031d6e -[KDBMasterViewController numberOfSectionsInTableView:] + 78
5   UIKit                               0x0088e712 -[UITableViewRowData(UITableViewRowDataPrivate) _updateNumSections] + 102
6   UIKit                               0x0088f513 -[UITableViewRowData invalidateAllSections] + 69
7   UIKit                               0x006fa6ea -[UITableView _updateRowData] + 197
....

由于崩溃不会 100% 发生,所以我怀疑是并发问题。我通过注意到在 fetchedResultsController 查询时 mainMOC 的父上下文为零来确认。在尝试查询子上下文 (mainMOC) 之前,如何确保子上下文 (mainMOC) 与父上下文正确设置?

最佳答案

访问您的 mainMOC 时存在竞争条件:
在将观察者添加到 masterMOC 的保存之后、开始导入之前添加行:[self mainMOC];,将解决 MOC 初始化竞争问题。

您可以阅读THIS用于讨论类似情况下的竞争条件(关于哪个线程并行访问您的上下文)。

Apple 提供的模板代码非常适合只有一个线程初始化上下文(非临时上下文)的情况,否则,您应该使用某种线程锁定机制来同步初始化。

关于ios - 多个 NSManagedObjectContexts 问题,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/23225507/

相关文章:

ios - Xamarin 模板 View

iphone - iOS-覆盖UITextView方法?

ios - 从 CMMotionManager 获取 SCNCamera 的正确值

ios - 如何使用 AFNetworking 缓存文件下载?

objective-c - 当改变方向的内存在增加时

c++ - 如何在多线程应用程序中使用 SQLite?

ios - 有没有办法用脚本创建一个 swift 类并在预构建中添加属性?

java - 是否有静态分析工具可以自动检查 Java 项目中的竞争条件?

python - 我应该使用事件、信号量、锁、条件或其组合来管理安全退出我的多线程 Python 程序吗?

IOS 关闭应用程序时丢失 FBSession