Utknąłem w problemie od wielu godzin i po przeczytaniu wszystkiego na ten temat w stackoverflow (i zastosowaniu wszystkich znalezionych porad), teraz oficjalnie potrzebuję pomocy. ; o)
Oto kontekst:
W moim projekcie iPhone'a muszę zaimportować dane w tle i wstawić je w kontekście obiektu zarządzanego. Postępując zgodnie z poradami tutaj, oto co robię:
- Zapisz główną moc
- Utwórz wystąpienie mocy w tle za pomocą trwałego koordynatora sklepu używanego przez główną moc
- Zarejestruj mój kontroler jako obserwatora powiadomienia NSManagedObjectContextDidSaveNotification dla funkcji w tle
- Wywołaj metodę importu w wątku w tle
- Za każdym razem, gdy dane są odbierane, wstaw je w tle moc
- Po zaimportowaniu wszystkich danych zapisz plik moc w tle
- Połącz zmiany w głównym moc, w głównym wątku
- Wyrejestruj mojego kontrolera jako obserwatora dla powiadomienia
- Zresetuj i zwolnij moc tła
Czasami (i przypadkowo) wyjątek ...
*** Terminating app due to uncaught exception 'NSGenericException', reason: '*** Collection <__NSCFSet: 0x5e0b930> was mutated while being enumerated...
... jest generowany, gdy wywołuję executeFetchRequest w tle moc, aby sprawdzić, czy zaimportowane dane już istnieją w bazie danych. Zastanawiam się, co powoduje mutację zestawu, skoro nie ma nic, co działałoby poza metodą importu.
Dołączyłem cały kod mojego kontrolera i mojej jednostki testowej (mój projekt składający się z tych dwóch klas i delegata aplikacji, który został niezmodyfikowany):
//
// 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
To wszystko ! Cały projekt jest tutaj. Brak widoku tabeli, brak NSFetchedResultsController, nic innego jak wątek w tle, który importuje dane w tle.
Co mogłoby zmutować zestaw w tym przypadku?
Jestem prawie pewien, że brakuje mi czegoś oczywistego i doprowadza mnie to do szału.
EDYTOWAĆ:
Oto pełny ślad stosu:
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'
źródło
Odpowiedzi:
OK, myślę, że rozwiązałem swój problem i muszę podziękować za ten post Freda McCanna:
http://www.duckrowing.com/2010/03/11/using-core-data-on-multiple-threads/
Wydaje się, że problem wynika z faktu, że tworzę instancję mojej mocy w tle w wątku głównym zamiast wątku w tle. Kiedy Apple mówi, że każdy wątek musi mieć swoją własną moc, musisz potraktować to poważnie: każda moc musi zostać utworzona w wątku, który będzie jej używał!
Przenoszę następujące linie ...
... w metodzie _importData (tuż przed zarejestrowaniem kontrolera jako obserwatora dla powiadomienia) rozwiązuje problem.
Dzięki za pomoc, Peter. Podziękowania dla Freda McCanna za cenny wpis na blogu!
źródło
each moc must be instantiated in the thread that will be using it
Myślę, że tylko operacja na MOC powinna być w tym samym wątku, ale tworzenie samego MOCa też, jeśli jest to prywatny MOC, powiązana kolejka jeszcze nie istnieje ..Pracowałem nad importem rekordów i wyświetlaniem rekordów w widoku tabeli. Napotkałem ten sam problem, gdy próbowałem zapisać rekord w tle, jak poniżej
podczas gdy już utworzyłem PrivateQueueContext. Po prostu zastąp powyższy kod poniższym
Naprawdę głupią pracą było zapisywanie w wątku w tle, podczas gdy już utworzyłem privateQueueConcurrencyType do zapisywania rekordu.
źródło