Core Data / NSOperation: сбой при перечислении и удалении объектов - PullRequest
8 голосов
/ 26 мая 2011

У меня есть приложение на основе основных данных, которое имеет отношение одного объекта (списка) ко многим объектам (элементам списка). Я работаю над синхронизацией данных между устройствами, и как часть этого я импортирую списки из файлов XML в фоновых потоках (через подкласс NSOperation).

Когда я обновляю существующий список, я удаляю все его старые элементы списка (из NSManagedObjectContext, характерного для этого потока) и заменяю их новыми из файла XML ... удаление обрабатывается путем перечисления через элементы для этого списка:

for (ListItemCD *item in listToUpdate.listItems) {
    [self.importContext deleteObject:item];
}

Однако, время от времени я получаю сбой во время этого перечисления:

* Завершение работы приложения из-за необработанного исключения «NSGenericException», причина: «* Коллекция <_NSFaultingMutableSet: 0x4fcfcb0> была мутирована при перечислении.

Я не уверен, с чего начать искать причину этой проблемы. Я не изменяю список в любой другой части кода, пока происходит перечисление. Одновременно может быть несколько потоков, так как импортируются / обновляются разные списки ... это может привести к сохранению контекста в другом потоке, поскольку он также уведомляет основной контекст (если это произошло одновременно с перечислением)

Если это поможет, вот код из функции "main" моего подкласса NSOperation (где я удаляю старые элементы списка из Core Data и обновляю список, анализируя данные XML):

- (void)main {

    // input the xml data into GDataXML
    NSData *xmlData = [[NSMutableData alloc] initWithContentsOfFile:self.filePath];
    NSError *error;
    GDataXMLDocument *doc = [[GDataXMLDocument alloc] initWithData:xmlData options:0 error:&error];



    // get the list name (so that I know which list to update)
    NSString *listName;
    NSArray *listNames = [doc.rootElement elementsForName:@"listName"];
    if (listNames.count > 0) {
        GDataXMLElement *listNameElement = (GDataXMLElement *) [listNames objectAtIndex:0];
        listName = listNameElement.stringValue;
        // NSLog(@"listName: %@", listName);




        // perform a fetch to find the old list with the same name (if there is one)
        NSFetchRequest *fetchRequest = [[NSFetchRequest alloc] init];
        NSEntityDescription *entity = [NSEntityDescription entityForName:@"SubListCD" inManagedObjectContext:self.importContext];
        [fetchRequest setEntity:entity];

        NSPredicate *predicate = [NSPredicate predicateWithFormat:@"%K like %@", @"listName", listName];
        [fetchRequest setPredicate:predicate];

        NSError *error;
        NSArray *fetchedObjects = [self.importContext executeFetchRequest:fetchRequest error:&error];
        // NSLog(@"fetchedObjects count: %d", [fetchedObjects count]);
        [fetchRequest release];



        /*
         // if I found the list, update its data
         */

        if ([fetchedObjects count] == 1) {
            SubListCD *listToUpdate = [fetchedObjects objectAtIndex:0];

            // get the list icon name
            NSArray *listIconNames = [doc.rootElement elementsForName:@"listIconName"];
            if (listIconNames.count > 0) {
                GDataXMLElement *listIconNameElement = (GDataXMLElement *) [listIconNames objectAtIndex:0];
                NSString *listIconName = listIconNameElement.stringValue;
                // NSLog(@"listIconName: %@", listIconName);
                listToUpdate.listIconName = [NSString stringWithString:listIconName];
            }

            // get the isChecklist BOOL
            NSArray *isChecklistBools = [doc.rootElement elementsForName:@"isChecklist"];
            if (isChecklistBools.count > 0) {
                GDataXMLElement *isChecklistElement = (GDataXMLElement *) [isChecklistBools objectAtIndex:0];
                NSString *isChecklist = isChecklistElement.stringValue;
                // NSLog(@"isChecklist: %@", isChecklist);
                listToUpdate.isCheckList = [NSNumber numberWithBool:[isChecklist isEqualToString:@"YES"]];
            }

            // get the itemsToTop BOOL
            NSArray *itemsToTopBools = [doc.rootElement elementsForName:@"itemsToTop"];
            if (itemsToTopBools.count > 0) {
                GDataXMLElement *itemsToTopElement = (GDataXMLElement *) [itemsToTopBools objectAtIndex:0];
                NSString *itemsToTop = itemsToTopElement.stringValue;
                // NSLog(@"itemsToTop: %@", itemsToTop);
                listToUpdate.itemsToTop = [NSNumber numberWithBool:[itemsToTop isEqualToString:@"YES"]];
            }

            // get the includeInBadgeCount BOOL
            NSArray *includeInBadgeCountBools = [doc.rootElement elementsForName:@"includeInBadgeCount"];
            if (includeInBadgeCountBools.count > 0) {
                GDataXMLElement *includeInBadgeCountElement = (GDataXMLElement *) [includeInBadgeCountBools objectAtIndex:0];
                NSString *includeInBadgeCount = includeInBadgeCountElement.stringValue;
                // NSLog(@"includeInBadgeCount: %@", includeInBadgeCount);
                listToUpdate.includeInBadgeCount = [NSNumber numberWithBool:[includeInBadgeCount isEqualToString:@"YES"]];
            }

            // get the list's creation date
            NSArray *listCreatedDates = [doc.rootElement elementsForName:@"listDateCreated"];
            if (listCreatedDates.count > 0) {
                GDataXMLElement *listDateCreatedElement = (GDataXMLElement *) [listCreatedDates objectAtIndex:0];
                NSString *listDateCreated = listDateCreatedElement.stringValue;
                // NSLog(@"listDateCreated: %@", listDateCreated);
                listToUpdate.dateCreated = [self dateFromString:listDateCreated];
            }

            // get the list's modification date
            NSArray *listModifiedDates = [doc.rootElement elementsForName:@"listDateModified"];
            if (listModifiedDates.count > 0) {
                GDataXMLElement *listDateModifiedElement = (GDataXMLElement *) [listModifiedDates objectAtIndex:0];
                NSString *listDateModified = listDateModifiedElement.stringValue;
                // NSLog(@"listDateModified: %@", listDateModified);
                listToUpdate.dateModified = [self dateFromString:listDateModified];
            }



            // NOTE: it's okay to get the displayOrder from index.plist here, since these update operations aren't called until after index.plist is loaded from Dropbox

            // get a reference to the documents directory
            NSArray *paths = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES);
            NSString *documentsDirectory = [paths objectAtIndex:0];

            // get the file path of the index.plist file
            NSString *indexFilePath = [documentsDirectory stringByAppendingPathComponent:@"index.plist"];

            // build an array with the names of the lists in the index
            NSMutableArray *listsIndexArray = [NSMutableArray arrayWithContentsOfFile:indexFilePath];

            int listIndex = [listsIndexArray indexOfObject:listName];

            listToUpdate.displayOrder = [NSNumber numberWithInt:listIndex];





            // remove the old list items from the listToUpdate, since I'll be adding them from scratch from the XML file
            for (ListItemCD *item in listToUpdate.listItems) {
                [self.importContext deleteObject:item];
            }




            // get an array of the list items so I can add them all
            NSArray *listItems = [doc.rootElement elementsForName:@"item"];
            if (listItems.count > 0) {
                int counter = 0;
                for (GDataXMLElement *item in listItems) {

                    // create the new item
                    ListItemCD *newItem = [NSEntityDescription insertNewObjectForEntityForName:@"ListItemCD" inManagedObjectContext:self.importContext];

                    // item name
                    NSArray *itemNames = [item elementsForName:@"itemName"];
                    if (itemNames.count > 0) {
                        GDataXMLElement *itemNameElement = (GDataXMLElement *) [itemNames objectAtIndex:0];
                        NSString *itemName = itemNameElement.stringValue;
                        // NSLog(@"itemName: %@", itemName);
                        newItem.itemName = [NSString stringWithString:itemName];
                    } else continue;

                    // item note
                    NSArray *itemNotes = [item elementsForName:@"itemNote"];
                    if (itemNotes.count > 0) {
                        GDataXMLElement *itemNoteElement = (GDataXMLElement *) [itemNotes objectAtIndex:0];
                        NSString *itemNote = itemNoteElement.stringValue;
                        // NSLog(@"itemNote: %@", itemNote);
                        newItem.itemNote = [NSString stringWithString:itemNote];
                    } else continue;

                    // itemReadOnly BOOL
                    NSArray *itemReadOnlyBools = [item elementsForName:@"itemReadOnly"];
                    if (itemReadOnlyBools.count > 0) {
                        GDataXMLElement *itemReadOnlyElement = (GDataXMLElement *) [itemReadOnlyBools objectAtIndex:0];
                        NSString *itemReadOnly = itemReadOnlyElement.stringValue;
                        // NSLog(@"itemReadOnly: %@", itemReadOnly);
                        newItem.itemReadOnly = [NSNumber numberWithBool:[itemReadOnly isEqualToString:@"YES"]];
                    } else continue;

                    // TODO: check my dates.. not sure if this will hold up in other locales

                    // item creation date
                    NSArray *itemCreatedDates = [item elementsForName:@"dateCreated"];
                    if (itemCreatedDates.count > 0) {
                        GDataXMLElement *dateCreatedElement = (GDataXMLElement *) [itemCreatedDates objectAtIndex:0];
                        NSString *dateCreated = dateCreatedElement.stringValue;
                        // NSLog(@"dateCreated: %@", dateCreated);
                        newItem.dateCreated = [self dateFromString:dateCreated];
                    } else continue;

                    // item modification date
                    NSArray *itemModifiedDates = [item elementsForName:@"dateModified"];
                    if (itemModifiedDates.count > 0) {
                        GDataXMLElement *dateModifiedElement = (GDataXMLElement *) [itemModifiedDates objectAtIndex:0];
                        NSString *dateModified = dateModifiedElement.stringValue;
                        // NSLog(@"dateModified: %@", dateModified);
                        newItem.dateModified = [self dateFromString:dateModified];
                    } else continue;

                    // item completed BOOL
                    NSArray *itemCompletedBools = [item elementsForName:@"itemCompleted"];
                    if (itemCompletedBools.count > 0) {
                        GDataXMLElement *itemCompletedElement = (GDataXMLElement *) [itemCompletedBools objectAtIndex:0];
                        NSString *itemCompleted = itemCompletedElement.stringValue;
                        // NSLog(@"itemCompleted: %@", itemCompleted);
                        newItem.itemCompleted = [NSNumber numberWithBool:[itemCompleted isEqualToString:@"YES"]];
                    } else continue;

                    // item completed date
                    NSArray *itemCompletedDates = [item elementsForName:@"dateCompleted"];
                    if (itemCompletedDates.count > 0) {
                        GDataXMLElement *dateCompletedElement = (GDataXMLElement *) [itemCompletedDates objectAtIndex:0];
                        NSString *dateCompleted = dateCompletedElement.stringValue;
                        // NSLog(@"dateCompleted string: %@", dateCompleted);
                        newItem.dateCompleted = [self dateFromString:dateCompleted];
                        // NSLog(@"dateCompleted: %@", newItem.dateCompleted);
                    } else continue;


                    // display order
                    newItem.displayOrder = [NSNumber numberWithInt:counter];
                    counter++;


                    // assign the new item to the listToUpdate
                    newItem.list = listToUpdate;
                }
            }



            // the list is now imported, so set isUpdating back to NO
            listToUpdate.isUpdating = [NSNumber numberWithBool:NO];



            // Save the context.
            NSError *saveError = nil;
            if (![self.importContext save:&saveError]) {
                NSLog(@"Unresolved error %@, %@", saveError, [saveError userInfo]);
                abort();
            }
            else {
                NSLog(@"saved after UPDATING a list while syncing!");
            }


        }
        else {
            NSLog(@"UpdateOperation - couldn't find an old version of the list to update!: %@", listName);
        }
    }

    [doc release];
    [xmlData release];
}

Спасибо за любой совет.

1 Ответ

30 голосов
/ 26 мая 2011

В сообщении об ошибке есть небольшая подсказка, где вы можете увидеть класс NSFaultingMutableSet в списке. Действительно, набор, который вы перечисляете, на самом деле является просто прокси для отношения «многие-многие», который потенциально может загружать данные по требованию. Поскольку элементы в коллекции помечаются как удаленные во время перечисления, существует вероятность того, что часть коллекции будет «изменяться», пока вы перечисляете ее, и вы увидите эту ошибку.

Распространенным способом решения этой проблемы является создание копии коллекции и ее перечисление. Наивный подход к этому был бы просто:

NSSet *iterItems = [[list.items copy] autorelease];
for (ListItemCD *item in iterItems) { ... }

Но при работе с базовыми данными я обнаружил, что -copy на самом деле не возвращает копию, а часто просто другой неисправный прокси. Поэтому я вместо этого решил скопировать коллекцию следующим образом:

NSSet *iterItems = [NSSet setWithSet:list.items];
for (ListItemCD *item in iterItems) { ... }
...