Подтвердить что ты не робот

NSFetchedResultsController пытается вставить нулевой объект

Изменить 7:

Здесь мой метод сохранения. Это довольно шаблонный. Макросы DEBUG_LOG() выполняются только в том случае, если это отладочная сборка.

- (void)saveManagedObjectContext:(NSManagedObjectContext *)moc
{
    if ([moc hasChanges]) {
        DEBUG_LOG(@"Saving managed object context %@", moc);
        NSError *error;
        BOOL success = [moc save:&error];
        if (!success || error) {
            DEBUG_LOG(@"ERROR: Couldn't save to managed object context %@: %@",
                  moc, error.localizedDescription);
        }
        DEBUG_LOG(@"Finished saving managed object context %@", moc);
    } else {
        DEBUG_LOG(@"Managed object context %@ had no changes", moc);
    }
}

Изменить 6:

iOS 8 здесь, и эта проблема вернулась. Мне повезло. Раньше я сузил проблему до использования оценочногоRowHeight в табличных представлениях (кстати, я никогда не исправлял проблему. Я просто прекратил использование оценочногоRowHeight). Теперь я снова вижу эту проблему в разных обстоятельствах. Я проследил его до фиксации с нескольких дней назад, когда я сделал прозрачные маркеры навигационных/табуляторов. Это включало отключение "Настроить прокрутки" в раскадровке и проверку ящиков, чтобы мои представления отображались под верхними барами и нижними столбцами. Там есть ряд шагов, которые мне нужно сделать, чтобы это произошло, но я могу воспроизвести его каждый раз, когда моя раскадровка настроена таким образом. Если я верну это совершение, это больше не произойдет.

Пока я говорю, "это уже не происходит", я действительно думаю, что это просто делает его менее вероятным. Эта ошибка является абсолютной b ****. Моя реакция кишки теперь заключается в том, что это ошибка iOS. Я просто не знаю, что я могу сделать, чтобы превратить это в отчет об ошибке. Это безумие.

Изменить 5:

Если вы хотите прочитать всю мою страдания, пожалуйста, продолжайте все это через этот пост. Если вы столкнулись с этой проблемой, и вам просто нужна помощь, вот что нужно изучить.

Мое последнее изменение отметило, что когда я использовал базовую ячейку таблицы, все работало нормально. Следующим моим шагом было бы попытаться с нуля создать новую обычную ячейку по частям и посмотреть, где она испортилась. К черту это, я повторно включил свой старый пользовательский код ячейки, и это сработало отлично. Уххх? О, подождите, я все еще прокомментировал estimatedHeightForRowAtIndexPath. Когда я удалил эти комментарии и включил estimatedHeightForRowAtIndexPath, он снова стал дерьмовым. Интересно.

Я просмотрел этот метод в документе API, и он упомянул что-то о константе, называемой UITableViewAutomaticDimension. Значение, которое я оценивал, было на самом деле только одной из общих высот ячеек, поэтому было бы не помешать переключиться на эту константу. После переключения на эту константу он работает правильно. Нет никаких странных исключений/графических сбоев для отчета.

Оригинальное сообщение

У меня довольно стандартное приложение для iPhone, которое извлекает данные из веб-службы в фоновом режиме и отображает данные в виде таблицы. Работа по обновлению фона имеет свой собственный контекст управляемого объекта, настроенный для NSPrivateQueueConcurrencyType. В моем представлении данных для чтения таблицы есть свой собственный контекст управляемого объекта, настроенный для NSMainQueueConcurrencyType. Когда фоновый контекст анализирует новые данные, он передает эти данные в основной контекст через mergeChangesFromContextDidSaveNotification. Иногда во время слияния мое приложение получает исключение здесь...

Thread 1, Queue : com.apple.main-thread
#0  0x3ac1b6a0 in objc_exception_throw ()
#1  0x308575ac in -[__NSArrayM insertObject:atIndex:] ()
#2  0x33354306 in __46-[UITableView _updateWithItems:updateSupport:]_block_invoke687 ()
#3  0x330d88d2 in +[UIView(UIViewAnimationWithBlocks)     _setupAnimationWithDuration:delay:view:options:factory:animations:start:animationStateGenerator:completion:] ()
#4  0x330ef7e4 in +[UIView(UIViewAnimationWithBlocks) animateWithDuration:delay:options:animations:completion:] ()
#5  0x3329e908 in -[UITableView _updateWithItems:updateSupport:] ()
#6  0x332766c6 in -[UITableView _endCellAnimationsWithContext:] ()
#7  0x0005ae72 in -[ICLocalShowsTableViewController controllerDidChangeContent:] at ICLocalShowsTableViewController.m:475
#8  0x3069976c in -[NSFetchedResultsController(PrivateMethods) _managedObjectContextDidChange:] ()
#9  0x308dfe78 in __CFNOTIFICATIONCENTER_IS_CALLING_OUT_TO_AN_OBSERVER__ ()
#10 0x30853b80 in _CFXNotificationPost ()
#11 0x3123a054 in -[NSNotificationCenter postNotificationName:object:userInfo:] ()
#12 0x306987a2 in -[NSManagedObjectContext(_NSInternalNotificationHandling) _postObjectsDidChangeNotificationWithUserInfo:] ()
#13 0x306f952a in -[NSManagedObjectContext _mergeChangesFromDidSaveDictionary:usingObjectIDs:] ()
#14 0x306f9734 in -[NSManagedObjectContext mergeChangesFromContextDidSaveNotification:] ()
#15 0x0006b5be in __65-[ICManagedObjectContexts backgroundManagedObjectContextDidSave:]_block_invoke at ICManagedObjectContexts.m:133
#16 0x306f9854 in developerSubmittedBlockToNSManagedObjectContextPerform ()
#17 0x3b1000ee in _dispatch_client_callout ()
#18 0x3b1029a8 in _dispatch_main_queue_callback_4CF ()
#19 0x308e85b8 in __CFRUNLOOP_IS_SERVICING_THE_MAIN_DISPATCH_QUEUE__ ()
#20 0x308e6e84 in __CFRunLoopRun ()
#21 0x30851540 in CFRunLoopRunSpecific ()
#22 0x30851322 in CFRunLoopRunInMode ()
#23 0x355812ea in GSEventRunModal ()
#24 0x331081e4 in UIApplicationMain ()
#25 0x000554f4 in main at main.m:16

Здесь исключение, которое я вижу...

CoreData: error: Serious application error.  An exception was caught from the delegate of NSFetchedResultsController during a call to -controllerDidChangeContent:.  *** -[__NSArrayM insertObject:atIndex:]: object cannot be nil with userInfo (null)

Мое приложение фактически удаляет исключение в контроллереDidChangeContent при моем вызове endUpdates. Я в основном вижу то же самое (NSFetchedResultsController, пытающееся вставить нулевой объект?), но у меня есть дополнительная информация и случай, который воспроизводится. Все мои события слияния - это вставки. Во время слияния в фоновом контексте, похоже, нет ожидающих вставок, удалений или обновлений. Сначала я использовал performBlockAndWait повсюду, пока не узнал о различии между performBlock и performBlockAndWait от видео WWDC. Я переключился на performBlock, и это сделало его немного лучше. Первоначально я подходил к этому как проблема с потоками, расходился с возможностью того, что это была странная проблема с памятью, вызванная не полностью понятными блоками, и теперь я вернулся к этому как условие гонки. Кажется, есть только одна часть, которую мне не хватает. Есть два пути, которых это не происходит...

(1) Регистр для контекста сохранит уведомление, но не будет делегат FRC, когда я его получу, и вернусь делегат после слияния. Это недалеко от того, чтобы не использовать FRC вообще, так что это действительно не вариант для обхода.

(2) Делайте то, что блокирует основной поток достаточно долго, поэтому состояние гонки не происходит. Например, когда я добавляю много сообщений журнала отладки в делегат моего представления таблицы, это замедляет его, чтобы этого не произошло.

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

После различных точек во время прокрутки контроллер просмотра запрашивает больше данных, вызывая функцию, которая имеет это в нем...

AFJSONRequestOperation *operation =
    [AFJSONRequestOperation JSONRequestOperationWithRequest:request
        success:^(NSURLRequest *request, NSHTTPURLResponse *response, id JSON) {
            // Parsing happens on MOC background queue
            [backgroundMOC performBlock:^ {
                [self parseJSON:JSON];

                // Handle everything else on the main thread
                [mainMOC performBlock:^ {
                    if (completion) {
                        // Remove activitiy indicators and such from the main thread
                    }
                }];
            }];
        }

        failure:^(NSURLRequest *request, NSHTTPURLResponse *response, NSError *error, id JSON) {
            [[NSOperationQueue mainQueue] performBlock:^ {
                if (completion) {
                    // Remove activitiy indicators and such from the main thread
                }
                // Show an alert view saying that the request failed
            }];
        }
     ];

[operation setCacheResponseBlock:^NSCachedURLResponse *(NSURLConnection *connection, NSCachedURLResponse *cachedResponse) {
    return nil;
}];

[_operationQueue addOperation:operation];

По большей части, parseJSON действительно не имеет в этом ничего интересного...

- (void)parseJSON:(NSDictionary *)json
{       
    NSError *error;
    NSArray *idExistsResults;
    NSNumber *eventId;
    NSFetchRequest *idExistsFetchRequest;
    LastFMEvent *event;
    NSManagedObjectModel *model = backgroundMOC.persistentStoreCoordinator.managedObjectModel;
    for (NSDictionary *jsonEvent in jsonEvents) {
        eventId = [NSNumber numberWithInt:[jsonEvent[@"id"] intValue]];
        idExistsFetchRequest = [model fetchRequestFromTemplateWithName:kGetEventByIDFetchRequest substitutionVariables:@{@"eventID" : eventId}];
        idExistsResults  = [backgroundMOC executeFetchRequest:idExistsFetchRequest error:&error];
        // Here I check for errors - omitted that part

        if ([idExistsResults count] == 0) {
            // Add a new event
            event = [NSEntityDescription insertNewObjectForEntityForName:[LastFMEvent entityName] inManagedObjectContext:backgroundMOC];
            [event populateWithJSON:jsonEvent];
        } else if ([idExistsResults count] == 1) {
            // Get here if I knew about the event already, so I update a few fields
        }
    }
    [self.mocManager saveManagedObjectContext:backgroundMOC];
}

Реализация для сохранения и слияния - это то место, где это может стать интересным. Сохранить ожидает, что он будет вызван из соответствующего исполняемого блока, поэтому он ничего не делает с помощью функции executeBlock.

- (void)saveManagedObjectContext:(NSManagedObjectContext *)moc
{
    if ([moc hasChanges]) {
        NSError *error;
        BOOL success = [moc save:&error];
        if (!success || error) {
            NSLog(@"ERROR: Couldn't save to managed object context %@: %@",
                  moc, error.localizedDescription);
        }
    }
}

При сохранении инициируется уведомление о слиянии. Я просто объединяюсь из фона в основной, поэтому я просто хочу знать, могу ли я встроить вызов слияния или если мне нужно сделать это внутри executeBlock.

- (void)backgroundManagedObjectContextDidSave:(NSNotification *)notification
{
    if (![NSThread isMainThread]) {
        [mainMOC performBlock:^ {
            [self.mainMOC mergeChangesFromContextDidSaveNotification:notification];
        }];
    } else {
        [mainMOC mergeChangesFromContextDidSaveNotification:notification];
    }
}

Мои избранные методы делегирования контроллеров результатов - вот что такое плита котла...

- (void)controller:(NSFetchedResultsController *)controller
   didChangeObject:(id)anObject
       atIndexPath:(NSIndexPath *)indexPath
     forChangeType:(NSFetchedResultsChangeType)type
      newIndexPath:(NSIndexPath *)newIndexPath
{
    UITableView *tableView = self.tableView;

    switch (type) {

        case NSFetchedResultsChangeInsert:
            [tableView insertRowsAtIndexPaths:@[newIndexPath]
                             withRowAnimation:UITableViewRowAnimationAutomatic];
            break;
        case NSFetchedResultsChangeDelete:
            [tableView deleteRowsAtIndexPaths:@[indexPath]
                             withRowAnimation:UITableViewRowAnimationAutomatic];
            break;
        case NSFetchedResultsChangeUpdate:
            [self configureCell:(ICLocalShowsTableViewCell *)[tableView cellForRowAtIndexPath:indexPath]
                          atIndexPath:indexPath];
            break;
        case NSFetchedResultsChangeMove:
            [tableView deleteRowsAtIndexPaths:@[indexPath]
                             withRowAnimation:UITableViewRowAnimationAutomatic];
            [tableView insertRowsAtIndexPaths:@[newIndexPath]
                             withRowAnimation:UITableViewRowAnimationAutomatic];
            break;
    }
}

- (void)controller:(NSFetchedResultsController *)controller
  didChangeSection:(id )sectionInfo
           atIndex:(NSUInteger)sectionIndex
     forChangeType:(NSFetchedResultsChangeType)type
{
    switch(type) {

        case NSFetchedResultsChangeInsert:
            [self.tableView insertSections:[NSIndexSet indexSetWithIndex:sectionIndex]
                          withRowAnimation:UITableViewRowAnimationAutomatic];
            break;

        case NSFetchedResultsChangeDelete:
            [self.tableView deleteSections:[NSIndexSet indexSetWithIndex:sectionIndex]
                          withRowAnimation:UITableViewRowAnimationAutomatic];
            break;
    }
}

- (void)controllerWillChangeContent:(NSFetchedResultsController *)controller
{
    [self.tableView beginUpdates];
}

- (void)controllerDidChangeContent:(NSFetchedResultsController *)controller
{
    [self.tableView endUpdates];
}

Еще один фрагмент кода, который может представлять интерес. Я использую autolayout для своих ячеек просмотра таблицы и новый API оценочной оценкиHeightForRowAtIndexPath для динамической высоты ячейки. Это означает, что во время вызова [self.tableView endUpdates] последний шаг фактически попадает в некоторые управляемые объекты, в то время как другой вызывает число секций/строк, только нужно знать подсчеты из FRC.

- (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath
{
    NSAssert([NSThread isMainThread], @"");
    LastFMEvent *event = [self.fetchedResultsController objectAtIndexPath:indexPath];

    if (!_offscreenLayoutCell) {
        _offscreenLayoutCell = [self.tableView dequeueReusableCellWithIdentifier:kLocalShowsCellIdentifier];
    }

    [_offscreenLayoutCell configureWithLastFMEvent:event];
    [_offscreenLayoutCell setNeedsLayout];
    [_offscreenLayoutCell layoutIfNeeded];

    CGSize cellSize = [_offscreenLayoutCell.contentView systemLayoutSizeFittingSize:UILayoutFittingCompressedSize];
    return cellSize.height;
}

Застрял на этом почти неделю. Выучил тонну в процессе, но geez, я готов двигаться дальше. Любые предложения были бы с благодарностью.

Edit

Я собрал довольно большой журнал отладки, чтобы попытаться рассказать историю о том, что происходит с udpates. Я вижу что-то действительно странное. Я обновляю таблицу по 50 строк за раз, поэтому я включу только интересную часть моего отладочного вывода. Каждый раз, когда ячейка настраивается, я печатаю, что это за название для ячейки, которую я только что выделил, а также то, чем будет новый заголовок. Когда я ударяю последнюю ячейку в представлении таблицы, я делаю запрос к веб-службе для получения дополнительных данных. Этот результат связан с окончательным обновлением, прежде чем я удалю исключение...

// Lots of output was here that I omitted

configure cell at sect 5 row 18 WAS Suphala NOW Keller Williams
configure cell at sect 5 row 19 WAS Advocate Of Wordz NOW Gates
configure cell at sect 5 row 20 WAS Emanuel and the Fear NOW Beats Antique
configure cell at sect 5 row 21 WAS The Julie Ruin NOW Ashrae Fax

// At this point I hit the end of the table and query for more data - for some reason row 18 gets configured again. Possibly no big deal.

configure cell at sect 5 row 18 WAS Keller Williams NOW Keller Williams
configure cell at sect 5 row 22 WAS Old Wounds NOW Kurt Vile

JSON size 100479
Starting JSON parsing
page 3 of 15. total events 709. events per page 50. current low idx 100 next trigger idx 149

// Parsing data finished, saving background context

Saving managed object context <NSManagedObjectContext: 0x17e912f0>
Background context will save
Finished saving managed object context <NSManagedObjectContext: 0x17e912f0>
Merging background context into main context
JSON parsing finished

** controllerWillChangeContent called **
** BEGIN UPDATES triggered **

inserting SECTION 6
inserting SECTION 7
inserting SECTION 8
inserting ROW sect 5 row 17
inserting ROW sect 5 row 22
inserting ROW sect 5 row 25
inserting ROW sect 5 row 26
inserting ROW sect 5 row 27
inserting ROW sect 5 row 28
inserting ROW sect 5 row 29

// A bunch more rows added here that I omitted

** controllerDidChangeContent called **

// This configure cell happens before the endUpdates call has completed

configure cell at sect 5 row 18 WAS Conflict NOW Conflict

В последнем обновлении он пытается вставить в s5 r17, но у меня уже была ячейка в этой строке. Он также пытается вставить на s5 r22, но у меня также была ячейка в этой строке. Наконец, он вставляет строку в s5 r25, которая на самом деле представляет собой новую строку. Мне кажется, что с учетом r17 и r22, поскольку вставки оставляют пробел в таблице. Должны ли предыдущие ячейки в этих индексах перемещаться событиями на r23 и r24?

Контроллер получаемых результатов использует дескриптор сортировки, который сортирует по дате и времени начала. Возможно, существующие события, которые были на r17 и r22, не получают события перемещения, потому что не было никаких изменений, связанных с их NSManagedObjects. По существу, они должны двигаться из-за моего дескриптора сортировки для событий раньше, чем они, а не потому, что их данные изменились.

Изменить 2:

Похоже, что эти вставки просто заставляют существующие ячейки сдвигаться вниз: (

Изменить 3:

Вещи, которые я пробовал сегодня...

  • Сделанный блок успеха AFNetworking ждет завершения слияния до его возврата
  • Сделано cellForRowAtIndexPath возвращает устаревшую ячейку (по сути, деактивировать ее и сразу вернуть), если выбранный контроллер результатов находится в середине beginUpdates/endUpdates. Думая, что лишний случайный cellForRowAtIndexPath, который вызывается во время обновления, возможно, делал странные вещи.
  • Удаление фонового контекста. Это интересно. Если я делаю все обновления пользовательского интерфейса и разбор JSON в основном контексте, это все равно происходит.

Изменить 4:

Теперь это становится интересным.

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

Это сработало.

Я попробовал базовую ячейку представления таблицы с субтитрами.

Это сработало.

Я попробовал базовую ячейку представления таблицы с субтитрами и изображением.

Это сработало.

Верхняя часть моей трассировки стека находится рядом со всеми этими элементами, связанными с анимацией, начинает иметь больше смысла. Он выглядит так: это авто-макет.

4b9b3361

Ответ 1

От инженера технической поддержки Apple:

Чтобы защитить целостность хранилища данных, Core Data улавливает некоторые исключения, которые происходят во время его операций. Иногда это означает что если Core Data вызывает ваш код через метод делегата, Core Data может закончиться перехватыванием исключений, которые ваш код бросил.

Ошибки многопоточности являются наиболее распространенной причиной таинственных проблем с основными данными.

В этом случае Core Data поймал исключение с помощью метода controllerDidChangeContent:, вызванного попыткой использования insertObject:atIndex.

Наиболее вероятным решением является обеспечение того, чтобы весь ваш NSManagedObject код был инкапсулирован внутри вызовов performBlock: или performBlockAndWait:.

В iOS 8 и OSX Yosemite Core Data получает возможность обнаруживать и сообщать о нарушениях своей модели concurrency. Он работает, бросая исключение, когда ваше приложение обращается к контексту управляемого объекта или управляемому объекту из неправильной очереди отправки. Вы включаете утверждения, передавая -com.apple.CoreData.ConcurrencyDebug 1 вашему приложению в командной строке с помощью редактора схем Xcode.

CoreData ConcurrencyDebug

Ole Begemann имеет отличную запись новой функции.