"Die Auflistung wurde während der Aufzählung mutiert" in executeFetchRequest

121

Ich habe jetzt stundenlang ein Problem und nachdem ich im Stackoverflow alles darüber gelesen habe (und alle gefundenen Ratschläge angewendet habe), brauche ich jetzt offiziell Hilfe. ;Ö)

Hier ist der Kontext:

In meinem iPhone-Projekt muss ich Daten im Hintergrund importieren und in einen verwalteten Objektkontext einfügen. Den folgenden Ratschlägen folgend, mache ich Folgendes:

  • Speichern Sie den Haupt-Moc
  • Instanziieren Sie einen Hintergrund-Moc mit dem persistenten Speicherkoordinator, der vom Haupt-Moc verwendet wird
  • Registrieren Sie meinen Controller als Beobachter der Benachrichtigung NSManagedObjectContextDidSaveNotification für den Hintergrund-Moc
  • Rufen Sie die Importmethode in einem Hintergrundthread auf
  • Fügen Sie jedes Mal, wenn Daten empfangen werden, diese in den Hintergrund ein
  • Speichern Sie nach dem Importieren aller Daten den Hintergrund-Moc
  • Führen Sie die Änderungen im Haupt-Moc im Haupt-Thread zusammen
  • Heben Sie die Registrierung meines Controllers als Beobachter für die Benachrichtigung auf
  • Setzen Sie den Hintergrund-Moc zurück und lassen Sie ihn los

Manchmal (und zufällig) ist die Ausnahme ...

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

... wird ausgelöst, wenn ich executeFetchRequest im Hintergrund-Moc aufrufe, um zu überprüfen, ob die importierten Daten bereits in der Datenbank vorhanden sind. Ich frage mich, was die Menge mutiert, da außerhalb der Importmethode nichts ausgeführt wird.

Ich habe den gesamten Code meines Controllers und meiner Testentität eingefügt (mein Projekt besteht aus diesen beiden Klassen und dem App-Delegaten, der nicht geändert wurde):

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

Das ist alles ! Das ganze Projekt ist da. Keine Tabellenansicht, kein NSFetchedResultsController, nichts anderes als ein Hintergrundthread, der Daten in einen Hintergrund-Moc importiert.

Was könnte das Set in diesem Fall mutieren?

Ich bin mir ziemlich sicher, dass mir etwas Offensichtliches fehlt und es macht mich verrückt.

BEARBEITEN:

Hier ist die vollständige Stapelverfolgung:

    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'
Eric MORAND
quelle
2
Aktivieren Sie im Xcode-Menü "Ausführen" die Option "Bei Objective-C-Ausnahmen stoppen" und führen Sie dann Ihre App unter dem Debugger aus. Was findest du?
Peter Hosey
1
Es bestätigt, dass die App in der Zeile "executeFetchRequest: error:" abstürzt. Ich habe die vollständige Stapelverfolgung zu meiner ursprünglichen Frage hinzugefügt ...
Eric MORAND
Und was ist mit den anderen Threads?
Peter Hosey
Hmmm, hier ist der Haupt - Thread - Stack: # 0 0x958490fa in mach_msg_trap # 1 0x95849867 in mach_msg # 2 0x0253f206 in __CFRunLoopServiceMachPort # 3 0x0249c8b4 in __CFRunLoopRun # 4 0x0249c280 in CFRunLoopRunSpecific # 5 0x0249c1a1 in CFRunLoopRunInMode # 6 0x027a82c8 in GSEventRunModal # 7 0x027a838d in GSEventRun # 8 0x00021b58 in UIApplicationMain # 9 0x00001edc in main at main.m: 16 Es gibt zwei weitere Threads (libdispatch-manager und "WebThread"), die jedoch keine weiteren Informationen enthalten.
Eric MORAND

Antworten:

182

OK, ich denke, ich habe mein Problem gelöst und ich muss mich bei diesem Blog-Beitrag von Fred McCann bedanken:

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

Das Problem scheint von der Tatsache zu kommen, dass ich meinen Hintergrund-Moc auf dem Haupt-Thread anstelle des Hintergrund-Threads instanziiere. Wenn Apple sagt, dass jeder Thread einen eigenen Moc haben muss, muss man das ernst nehmen: Jeder Moc muss in dem Thread instanziiert werden, der ihn verwenden wird!

Verschieben der folgenden Zeilen ...

// We instantiate the background moc

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

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

... in der _importData-Methode (kurz bevor der Controller als Beobachter für die Benachrichtigung registriert wird) löst das Problem.

Danke für deine Hilfe, Peter. Und danke an Fred McCann's für seinen wertvollen Blog-Beitrag!

Eric MORAND
quelle
2
OK, nach vielen Tests kann ich bestätigen, dass dies mein Problem absolut gelöst hat. Ich werde dies als akzeptierte Antwort markieren, sobald ich ...
Eric MORAND
Danke für diese Lösung! Dieser Thread hat eine sehr gute Implementierung des Sperr- / Entsperrkontexts, um Konflikte während des Zusammenführens zu vermeiden: stackoverflow.com/questions/2009399/…
gonso
4
+1 Vielen Dank, dass Sie die Frage, die Lösung und den Link zu Fred McCanns Blog-Beitrag gestellt haben. Es hat mir sehr geholfen !!!
Learner2010
3
each moc must be instantiated in the thread that will be using itIch denke, nur die Operation auf MOC sollte sich auf demselben Thread befinden, aber das MOC selbst zu erstellen, wenn dies ein privates MOC ist, ist die zugehörige Warteschlange noch nicht vorhanden.
János
@ János Ich habe hier die gleiche Frage. Wie können Sie den Kontext in dem Thread instanziieren, der ihn verwenden wird? Der Thread ist noch nicht vorhanden. Ich verwende Swift und verstehe nicht, was "Verschieben in der _importData-Methode" bedeutet.
Todanley
0

Ich habe am Importieren von Datensätzen und Anzeigen von Datensätzen in der Tabellenansicht gearbeitet. Beim Versuch, einen Datensatz auf backgroundThread wie unten zu speichern, trat dasselbe Problem auf

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

während ich bereits einen PrivateQueueContext erstellt habe. Ersetzen Sie einfach den obigen Code durch den folgenden

[self saveObjectContextInDataBaseWithContext:privateQueueContext];

Es war wirklich meine dumme Arbeit, im Hintergrund-Thread zu speichern, während ich bereits einen privateQueueConcurrencyType zum Speichern von Datensätzen erstellt habe.

Gagan_iOS
quelle