“集合在枚举时发生了变异”

我被一个问题困住了好几个小时,在阅读了关于 stackoverflow 的所有相关内容(并应用了所有找到的建议)之后,我现在正式需要帮助了。; o)

背景是这样的:

在我的 iPhone 项目中,我需要在后台导入数据并将其插入到托管对象上下文中。根据这里发现的建议,以下是我正在做的:

  • 保存主机
  • Instantiate a background moc with the persistent store coordinator used by the main moc
  • 将我的控制器注册为后台 moc 的 NSManagedObjectContextDidSaveNotification 通知的观察者
  • Call the import method on a background thread
  • 每次接收到数据时,将其插入到后台 moc 中
  • 导入所有数据后,保存后台 moc
  • Merge the changes into the main moc, on the main thread
  • 将我的控制器注销为通知的观察者
  • Reset and release the background moc

有时候(随机) ,例外..。

*** Terminating app due to uncaught exception 'NSGenericException', reason: '*** Collection <__NSCFSet: 0x5e0b930> was mutated while being enumerated...

... 在后台 moc 上调用 ExecuteFetchRequest 时引发,以检查导入的数据是否已经存在于数据库中。我想知道是什么在变异集合,因为在 import 方法之外没有运行任何东西。

我已经包含了我的控制器和测试实体的全部代码(我的项目由这两个类和应用程序委托组成,它们没有被修改) :

//
//  RootViewController.h
//  FK1
//
//  Created by Eric on 09/08/10.
//  Copyright (c) 2010 __MyCompanyName__. All rights reserved.
//




#import <CoreData/CoreData.h>


@interface RootViewController : UITableViewController <NSFetchedResultsControllerDelegate> {
NSManagedObjectContext *managedObjectContext;
NSManagedObjectContext *backgroundMOC;
}




@property (nonatomic, retain) NSManagedObjectContext *managedObjectContext;
@property (nonatomic, retain) NSManagedObjectContext *backgroundMOC;


@end




//
//  RootViewController.m
//  FK1
//
//  Created by Eric on 09/08/10.
//  Copyright (c) 2010 __MyCompanyName__. All rights reserved.
//




#import "RootViewController.h"
#import "FK1Message.h"


@implementation RootViewController


@synthesize managedObjectContext;
@synthesize backgroundMOC;


- (void)viewDidLoad {
[super viewDidLoad];


self.navigationController.toolbarHidden = NO;


UIBarButtonItem *refreshButton = [[UIBarButtonItem alloc] initWithBarButtonSystemItem:UIBarButtonSystemItemRefresh target:self action:@selector(refreshAction:)];


self.toolbarItems = [NSArray arrayWithObject:refreshButton];
}


#pragma mark -
#pragma mark ACTIONS


- (void)refreshAction:(id)sender {
// If there already is an import running, we do nothing


if (self.backgroundMOC != nil) {
return;
}


// We save the main moc


NSError *error = nil;


if (![self.managedObjectContext save:&error]) {
NSLog(@"error = %@", error);


abort();
}


// We instantiate the background moc


self.backgroundMOC = [[[NSManagedObjectContext alloc] init] autorelease];


[self.backgroundMOC setPersistentStoreCoordinator:[self.managedObjectContext persistentStoreCoordinator]];


// We call the fetch method in the background thread


[self performSelectorInBackground:@selector(_importData) withObject:nil];
}


- (void)_importData {
NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];


[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(backgroundMOCDidSave:) name:NSManagedObjectContextDidSaveNotification object:self.backgroundMOC];


FK1Message *message = nil;


NSFetchRequest *fetchRequest = nil;
NSEntityDescription *entity = [NSEntityDescription entityForName:@"FK1Message" inManagedObjectContext:self.backgroundMOC];
NSPredicate *predicate = nil;
NSArray *results = nil;


// fake import to keep this sample simple


for (NSInteger index = 0; index < 20; index++) {
predicate = [NSPredicate predicateWithFormat:@"msgId == %@", [NSString stringWithFormat:@"%d", index]];


fetchRequest = [[[NSFetchRequest alloc] init] autorelease];


[fetchRequest setEntity:entity];
[fetchRequest setPredicate:predicate];


// The following line sometimes randomly throw the exception :
// *** Terminating app due to uncaught exception 'NSGenericException', reason: '*** Collection <__NSCFSet: 0x5b71a00> was mutated while being enumerated.


results = [self.backgroundMOC executeFetchRequest:fetchRequest error:NULL];


// If the message already exist, we retrieve it from the database
// If it doesn't, we insert a new message in the database


if ([results count] > 0) {
message = [results objectAtIndex:0];
}
else {
message = [NSEntityDescription insertNewObjectForEntityForName:@"FK1Message" inManagedObjectContext:self.backgroundMOC];
message.msgId = [NSString stringWithFormat:@"%d", index];
}


// We update the message


message.updateDate = [NSDate date];
}


// We save the background moc which trigger the backgroundMOCDidSave: method


[self.backgroundMOC save:NULL];


[[NSNotificationCenter defaultCenter] removeObserver:self name:NSManagedObjectContextDidSaveNotification object:self.backgroundMOC];


[self.backgroundMOC reset]; self.backgroundMOC = nil;


[pool drain];
}


- (void)backgroundMOCDidSave:(NSNotification*)notification {
if (![NSThread isMainThread]) {
[self performSelectorOnMainThread:@selector(backgroundMOCDidSave:) withObject:notification waitUntilDone:YES];
return;
}


// We merge the background moc changes in the main moc


[self.managedObjectContext mergeChangesFromContextDidSaveNotification:notification];
}


@end


//
//  FK1Message.h
//  FK1
//
//  Created by Eric on 09/08/10.
//  Copyright 2010 __MyCompanyName__. All rights reserved.
//


#import <CoreData/CoreData.h>


@interface FK1Message :  NSManagedObject
{
}


@property (nonatomic, retain) NSString * msgId;
@property (nonatomic, retain) NSDate * updateDate;


@end


//
//  FK1Message.m
//  FK1
//
//  Created by Eric on 09/08/10.
//  Copyright 2010 __MyCompanyName__. All rights reserved.
//


#import "FK1Message.h"


@implementation FK1Message


#pragma mark -
#pragma mark PROPERTIES


@dynamic msgId;
@dynamic updateDate;


@end

这就是全部!整个项目都在这里。没有表视图,没有 NSFetcheResultsController,只有一个在后台 moc 上导入数据的后台线程。

在这种情况下,什么会使集合发生突变?

我很确定我错过了一些显而易见的事情,这让我很抓狂。

EDIT:

下面是完整的堆栈跟踪:

    2010-08-10 10:29:11.258 FK1[51419:1b6b] *** Terminating app due to uncaught exception 'NSGenericException', reason: '*** Collection <__NSCFSet: 0x5d075b0> was mutated while being enumerated.<CFBasicHash 0x5d075b0 [0x25c6380]>{type = mutable set, count = 0,
entries =>
}
'
*** Call stack at first throw:
(
0   CoreFoundation                      0x0255d919 __exceptionPreprocess + 185
1   libobjc.A.dylib                     0x026ab5de objc_exception_throw + 47
2   CoreFoundation                      0x0255d3d9 __NSFastEnumerationMutationHandler + 377
3   CoreData                            0x02287702 -[NSManagedObjectContext executeFetchRequest:error:] + 4706
4   FK1                                 0x00002b1b -[RootViewController _fetchData] + 593
5   Foundation                          0x01d662a8 -[NSThread main] + 81
6   Foundation                          0x01d66234 __NSThread__main__ + 1387
7   libSystem.B.dylib                   0x9587681d _pthread_start + 345
8   libSystem.B.dylib                   0x958766a2 thread_start + 34
)
terminate called after throwing an instance of 'NSException'
55125 次浏览

好了,我想我已经解决了我的问题,我必须感谢 Fred McCann 的这篇博客文章:

Http://www.duckrowing.com/2010/03/11/using-core-data-on-multiple-threads/

问题似乎来自于我在主线程上而不是在后台线程上实例化我的后台 moc。当苹果告诉每个线程需要有自己的 moc 时,你必须认真对待: 每个 moc 必须在使用它的线程中实例化!

移动下面的线条..。

// We instantiate the background moc


self.backgroundMOC = [[[NSManagedObjectContext alloc] init] autorelease];


[self.backgroundMOC setPersistentStoreCoordinator:[self.managedObjectContext persistentStoreCoordinator]];

...in the _importData method (just before to register the controller as observer for the notification) solves the problem.

谢谢你的帮助,彼得。也谢谢弗雷德麦肯的宝贵的博客文章!

我的工作是进口记录和显示记录在桌面视图。面对同样的问题,当我试图保存记录在后台像下面的线程

 [self performSelectorInBackground:@selector(saveObjectContextInDataBaseWithContext:) withObject:privateQueueContext];

我已经创建了一个 PrivateQueueContext,只需将上面的代码替换为下面的代码即可

[self saveObjectContextInDataBaseWithContext:privateQueueContext];

Really it was my foolish work to save on background thread while I already created a privateQueueConcurrencyType for saving record.