Ошибка: NSInternalInconsistencyException - Недопустимая строка rowCache равна нулю

У меня проблема с CoreData concurrency на iOS 8.1.

Я получаю следующую трассировку стека для сбоя:

NSInternalInconsistencyException - Invalid rowCache row is nil

0     CoreFoundation                        0x0000000183b6659c __exceptionPreprocess + 132
1     libobjc.A.dylib                       0x00000001942640e4 objc_exception_throw + 56
2     CoreData                              0x000000018385b8b8 -[NSSQLCore _newRowCacheRowForToManyUpdatesForRelationship:rowCacheOriginal:originalSnapshot:value:added:deleted:sourceRowPK:properties:sourceObject:newIndexes:reorderedIndexes:] + 6668
3     CoreData                              0x00000001838fbea0 -[NSSQLCore recordToManyChangesForObject:inRow:usingTimestamp:inserted:] + 2604
4     CoreData                              0x0000000183857638 -[NSSQLCore prepareForSave:] + 1052
5     CoreData                              0x00000001838569b4 -[NSSQLCore saveChanges:] + 520
6     CoreData                              0x000000018381f078 -[NSSQLCore executeRequest:withContext:error:] + 716
7     CoreData                              0x00000001838e6254 __65-[NSPersistentStoreCoordinator executeRequest:withContext:error:]_block_invoke + 4048
8     CoreData                              0x00000001838ed654 gutsOfBlockToNSPersistentStoreCoordinatorPerform + 176
9     libdispatch.dylib                     0x00000001948a936c _dispatch_client_callout + 12
10   libdispatch.dylib                      0x00000001948b26e8 _dispatch_barrier_sync_f_invoke + 72
11   CoreData                               0x00000001838e0cb4 _perform + 176
12   CoreData                               0x000000018381ec34 -[NSPersistentStoreCoordinator executeRequest:withContext:error:] + 296
13   CoreData                               0x0000000183845400 -[NSManagedObjectContext save:] + 1280

Этот сбой происходит в нескольких местах (около 20), но наиболее заметен в моей функции импорта данных, которая импортирует 1000 записей и сохраняет каждые 100.

Несколько заметок о моей установке CoreData:

Код, в котором происходят все эти сохранения, имеет следующий базовый шаблон:

[backgroundContext performBlock:^{
    ...
    NSArray *result = [backgroundContext fetch...];
    ...

    if ([backgroundContext save:&error]) { // <-- App is crashing here

    }
}];

Насколько я понимаю, не должно быть никаких проблем concurrency с NSPersistentStore позади backgroundContext, но это то, что говорит мне об аварии.

Кроме того, это происходит только для менее 0,02% моей пользовательской базы. Это довольно редко (я не могу воспроизвести), но пользователи не могут восстановиться после этого, не удаляя и не переустанавливая приложение. Он будет последовательно открываться и разбиваться для них.

Обратите внимание, что это только для 64-разрядных iOS 8.1.X - iOS 7.X и 8.0.X не демонстрируют этого поведения, и я не вижу его ни на чем старше iPhone 5s.


Уточнение: удаление постоянного хранилища устраняет эту проблему для всех пользователей. Это, по-видимому, указывает, что это не проблема concurrency.


Создание хранилища и контекстов.

[_persistenStoreCoordinator addPersistentStoreWithType:NSSQLiteStoreType
                                     configuration:nil
                                               URL:[DatabaseManager storeURL]
                                           options:@{NSMigratePersistentStoresAutomaticallyOption:@YES,
                                                     NSInferMappingModelAutomaticallyOption:@YES}
                                             error:&error];

Создание контекстов

_backgroundContext = [[NSManagedObjectContext alloc] initWithConcurrencyType:NSPrivateQueueConcurrencyType];
[_backgroundContext setMergePolicy:NSMergeByPropertyObjectTrumpMergePolicy];
[_backgroundContext setPersistentStoreCoordinator:_persistenStoreCoordinator];
if ([_backgroundContext respondsToSelector:@selector(setName:)]) {
    [_backgroundContext setName:@"DatabaseManager.BackgroundQueue"];
}

_mainContext = [[NSManagedObjectContext alloc] initWithConcurrencyType:NSMainQueueConcurrencyType];
[_mainContext setMergePolicy:NSMergeByPropertyObjectTrumpMergePolicy];
[_mainContext setPersistentStoreCoordinator:_persistenStoreCoordinator];
if ([_mainContext respondsToSelector:@selector(setName:)]) {
    [_mainContext setName:@"DatabaseManager.MainQueue"];
}

[[NSNotificationCenter defaultCenter] addObserver:self
                                         selector:@selector(mainContextDidSave:)
                                             name:NSManagedObjectContextDidSaveNotification
                                           object:_mainContext];

[[NSNotificationCenter defaultCenter] addObserver:self
                                         selector:@selector(backgroundContextDidSave:)
                                             name:NSManagedObjectContextDidSaveNotification
                                           object:_backgroundContext];

Прослушивание изменений

- (void) mainContextDidSave:(NSNotification *)notification
{
    [_backgroundContext performBlock:^{
        [self->_backgroundContext mergeChangesFromContextDidSaveNotification:notification];
    }];
}

- (void) backgroundContextDidSave:(NSNotification*)notification
{
    [_mainContext performBlock:^{
        NSArray* updated = [notification.userInfo valueForKey:NSUpdatedObjectsKey];

        // Fault all objects that will be updated.
        for (NSManagedObject* obj in updated) {
            NSManagedObject* mainThreadObject = [self->_mainContext objectWithID:obj.objectID];
            [mainThreadObject willAccessValueForKey:nil];
        }

        [self->_mainContext mergeChangesFromContextDidSaveNotification:notification];
    }];
}

Как выполняется код в каждом из контекстов. Я передаю блок здесь:

- (void)executeBackgroundOperation:(void (^)(NSManagedObjectContext *))operation {

    NSAssert(operation, @"No Background operation to perform");

    [_backgroundContext performBlock:^{
        operation(self->_backgroundContext);
    }];
}

- (void)executeMainThreadOperation:(void (^)(NSManagedObjectContext *))operation {
    NSAssert(operation, @"No Main Thread operation to perform");

    [_mainContext performBlock:^{
        operation(self->_mainContext);
    }];
}

Единственными другими потоками в отчете о сбое, которые имеют lock в них, являются два потока Javascript, которые выглядят следующим образом:

0   libsystem_kernel.dylib 0x3a77cb38 __psynch_cvwait + 24
1   libsystem_pthread.dylib 0x3a7fa2dd pthread_cond_wait + 38
2   libc++.1.dylib 0x39a11e91 _ZNSt3__118condition_variable4waitERNS_11unique_lockINS_5mutexEEE + 34
3   JavaScriptCore 0x2dcd4cb5 _ZN3JSC8GCThread16waitForNextPhaseEv + 102
4   JavaScriptCore 0x2dcd4d19 _ZN3JSC8GCThread12gcThreadMainEv + 50
5   JavaScriptCore 0x2db09597 _ZN3WTFL19wtfThreadEntryPointEPv + 12
6   libsystem_pthread.dylib 0x3a7f9e93 _pthread_body + 136
7   libsystem_pthread.dylib 0x3a7f9e07 _pthread_start + 116
8   libsystem_pthread.dylib 0x3a7f7b90 thread_start + 6

Ответ 1

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

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

Следуйте этому руководству и посмотрите, исправляет ли он вашу проблему. http://www.raywenderlich.com/27657/how-to-perform-a-lightweight-core-data-migration

Ответ 2

@StephenFurlani,

Я не эксперт по Core-data. Но мне нравится решать проблему. Я думаю, что приложение получает сбой при объединении ManagedObject из backgroundContext в MainContext (как вы упомянули ваш код):

- (void) backgroundContextDidSave:(NSNotification*)notification
{
    [_mainContext performBlock:^{
        NSArray* updated = [notification.userInfo valueForKey:NSUpdatedObjectsKey];

        // Fault all objects that will be updated.
        for (NSManagedObject* obj in updated) {
            NSManagedObject* mainThreadObject = [self->_mainContext objectWithID:obj.objectID];
            [mainThreadObject willAccessValueForKey:nil];
        }

        [self->_mainContext mergeChangesFromContextDidSaveNotification:notification];
    }];
}

Вот несколько ссылок:

Решение 1: Попробуйте этот код ниже:

NSMangedObjectContext *temporaryContext = [[NSManagedObjectContext alloc] initWithConcurrencyType:NSPrivateQueueConcurrencyType];
temporaryContext.parentContext = mainMOC;

[temporaryContext performBlock:^{
   // do something that takes some time asynchronously using the temp context

   // push to parent
   NSError *error;
   if (![temporaryContext save:&amp;error])
   {
      // handle error
   }

   // save parent to disk asynchronously
   [mainMOC performBlock:^{
      NSError *error;
      if (![mainMOC save:&amp;error])
      {
         // handle error
      }
   }];
}];

Решение 2: обновить значение из backgroundContext NSManagedObject в mainContext, а затем вызвать processPendingChanges ИЛИ mergeChangesFromContextDidSaveNotification mainContext. Попробуйте использовать этот код вместо кода (метод backgroundContextDidSave):

- (void) backgroundContextDidSave:(NSNotification*)notification
    {
        [_mainContext performBlock:^{
            NSArray* updated = [notification.userInfo valueForKey:NSUpdatedObjectsKey];

            // Fault all objects that will be updated.
            for (NSManagedObject* obj in updated) {
                NSManagedObject* mainThreadObject = [self->_mainContext objectWithID:obj.objectID];
                if (mainThreadObject) {
                    [self->_mainContext refreshObject: mainThreadObject mergeChanges:NO];
                }
            }

            [self->_mainContext processPendingChanges];
        }];
    }

Очень хорошая справочная-полная ссылка: CORE DATA с несколькими контекстами управляемых объектов

Ответ 3

Я столкнулся с подобным сбоем, не то же самое, но я также использовал установку Stack # 3. Авария произошла в той же строке, что и ваша, во время сохранения с фона. Я обнаружил, что причина в том, что когда операции CoreData объединяли несколько потоков, потоки заканчивались в неправильном порядке, но только иногда. Для нас это была проблема, поскольку в нашей модели были некоторые зависимые операции и объектные отношения.

Я исправил это, управляя собственным потоком и собственной очереди для загрузки фона. Таким образом, я мог справиться с порядком и всегда знал, что у меня есть поток типа FIFO. Это также устранило сбой при сохранении.

Я добавил код ниже, если вы хотите попробовать.

- (void) keepAlive 
{
    [self performSelector:@selector(keepAlive) withObject:nil afterDelay:60];
}

- (void)backgroundThreadMain
{
    // Add selector to prevent CFRunLoopRunInMode from returning immediately
    [self performSelector:@selector(keepAlive) withObject:nil afterDelay:60];
    BOOL done = NO;

    do
    {
        // Start the run loop but return after each source is handled.
        SInt32    result = CFRunLoopRunInMode(kCFRunLoopDefaultMode, 10, YES);   

        // If a source explicitly stopped the run loop, or if there are no
        // sources or timers, go ahead and exit.
        if ((result == kCFRunLoopRunStopped) || (result == kCFRunLoopRunFinished))
        done = YES;
    }
    while (!done);
}

self.backgroundThread = [[NSThread alloc] initWithTarget:self selector:@selector(backgroundThreadMain) object:nil];
[self.backgroundThread start];

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

[[CoreDataControl coreDataControlManager] performSelector:@selector(saveSite:) onThread:[CoreDataControl coreDataControlManager].backgroundThread withObject:site waitUntilDone:NO];

В худшем случае вы можете обернуть сохранение в блок catch try, и если исключение вызывает вызов reset в обоих контекстах. Это очистит любой rowcache, который может вызвать ошибку.