2010-08-10 4 views
117

Je suis bloqué sur un problème depuis des heures et après avoir lu tout ce qui se passe sur stackoverflow (et appliqué tous les conseils trouvés), j'ai maintenant besoin d'aide. ; O)"Collection a été muté pendant l'énumération" sur executeFetchRequest

Voici le contexte:

Dans mon projet iPhone, j'ai besoin d'importer des données sur le fond et l'insérer dans un contexte d'objet géré. Suivant les conseils trouvés ici, voici ce que je fais:

  • Save the principal moc
  • instancier un moc de fond avec le coordinateur de stockage permanent utilisé par le principal moc
  • Enregistrer mon contrôleur à titre d'observateur de la notification NSManagedObjectContextDidSaveNotification pour l'arrière-plan moc
  • Appelez la méthode d'importation sur un fil de fond
  • Chaque fois que des données sont reçues, insérez-le sur le fond moc
  • Une fois que toutes les données ont été importées, sauf l'arrière-plan moc
  • Fusionner les changements dans la moc principale, sur le thread principal
  • Désenregistrer mon contrôleur à titre d'observateur pour la notification
  • Reset et libérer l'arrière-plan moc

Parfois (et au hasard), l'exception ...

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

... est jeté quand je l'appelle executeFetchRequest sur le moc de fond, pour vérifier si l'ALR de données importées existe déjà dans la base de données. Je me demande ce qui est la mutation de l'ensemble car il n'y a rien qui fonctionne en dehors de la méthode d'importation.

J'ai inclus tout le code de mon contrôleur et mon entité de test (mon projet composé de ces deux classes et le délégué de l'application, qui a été non modifiée):

// 
// 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 

C'est tout! L'ensemble du projet est ici. Pas de vue de table, pas de NSFetchedResultsController, rien d'autre qu'un thread d'arrière-plan qui importe des données sur un moc en arrière-plan.

Qu'est-ce qui pourrait muter l'ensemble dans ce cas? Je suis sûr que je manque quelque chose d'évident et ça me rend fou.

EDIT:

Voici la trace complète de la pile:

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' 
+1

Dans le menu Exécuter de Xcode, activez "Arrêter sur les exceptions Objective-C", puis exécutez votre application sous le débogueur. Que trouvez-vous? –

+1

Il confirme que l'application plante sur la ligne "executeFetchRequest: error:". J'ai ajouté la trace complète de la pile à ma question originale ... –

+0

Et les autres threads? –

Répondre

175

OK, je pense que je l'ai résolu mon problème et je dois remercier ce post de blog de Fred McCann:

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

Le problème semble provenir du fait que j'instancie mon moc d'arrière-plan sur le thread principal au lieu du thread d'arrière-plan. Quand Apple dit que chaque thread doit avoir son propre moc, il faut le prendre au sérieux: chaque moc doit être instancié dans le thread qui va l'utiliser!

Déplacer les lignes suivantes ...

// We instantiate the background moc 

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

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

... dans la méthode _importData (juste avant d'enregistrer le contrôleur en tant qu'observateur de la notification) permet de résoudre le problème.

Merci pour votre aide, Peter. Et merci à Fred McCann pour son précieux article de blog!

+2

OK, après beaucoup de tests, je peux confirmer que cela a résolu mon problème. Je vais marquer ceci comme une réponse acceptée dès que je suis autorisé à ... –

+0

Merci pour cette solution! Ce thread a une très bonne implémentation du contexte de verrouillage/déverrouillage pour éviter les conflits lors de la fusion: http://stackoverflow.com/questions/2009399/cryptic-error-from-core-data-nsinvalidargumentexception-reason-referencedata64 – gonso

+4

+1 Merci beaucoup pour mettre la question, la solution et fournir le lien vers le blog de Fred McCann. Ça m'a beaucoup aidé !!! – learner2010

0

Je travaillais sur l'importation de l'enregistrement & affichage des enregistrements dans tableview. Face même problème quand j'ai essayé de sauver le dossier backgroundThread comme ci-dessous

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

alors que je l'ai déjà créé un PrivateQueueContext. Il suffit de remplacer le code ci-dessus avec en dessous d'un

[self saveObjectContextInDataBaseWithContext:privateQueueContext]; 

C'était vraiment mon travail fou pour sauver le fil de fond pendant que je l'ai déjà créé un privateQueueConcurrencyType pour sauvegarder un enregistrement.

Questions connexes