NSFetchedResultsController intentando insert un object nil

Edición 7:

Aquí está mi método de save. Es bastante repetitivo. Las macros DEBUG_LOG () solo se ejecutan si es una compilation de debugging.

- (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); } } 

Edición 6:

iOS 8 está aquí y este problema está de vuelta. Suerte la mía. Anteriormente, había networkingucido el problema a usar estimatedRowHeight en las vistas de tabla (por cierto, nunca reparé completamente el problema. Simplemente dejé de usar estimatedRowHeight). Ahora estoy viendo este problema nuevamente bajo diferentes circunstancias. Llegué a un compromiso de hace unos días cuando hice translúcidas las barras de mi nav / tab. Esto incluyó la desactivación de 'Ajustar vista de desplazamiento' en el guión gráfico y la comprobación de los cuadros para que se muestren mis vistas debajo de las barras superiores y inferiores. Hay una serie de pasos que debo hacer para que esto ocurra, pero puedo reproducirlo cada vez que mi guión gráfico se configura de esa manera. Si vuelvo a cometer, ya no pasa.

Mientras digo, "ya no sucede", lo que realmente creo es que esto solo hace que sea less probable que ocurra. Este error es un absoluto b ****. Mi reacción intestinal ahora es que este es un error de iOS. Simplemente no sé qué puedo hacer para convertir esto en un informe de errores. Es una locura.

Edición 5:

Si desea leer la totalidad de mi desgracia, continúe todo el path a través de esta publicación. Si te encuentras con este problema y solo quieres ayuda, aquí hay algo para investigar.

Mi última edición notó que cuando usaba una celda de vista de tabla básica, todo funcionaba bien. Mi próximo curso de acción consistiría en intentar desde cero build una nueva célula personalizada pieza por pieza y ver dónde se arruinó. Para el diablo, volví a habilitar mi antiguo código de celda personalizado y funcionó bien. ¿Uhhh? Oh, espera, todavía he estimatedHeightForRowAtIndexPath HeightsForRowAtIndexPath comentó. Cuando quité esos comentarios y habilité estimatedHeightForRowAtIndexPath , volvió a ser horrible. Interesante.

Busqué ese método en el API doc, y mencionó algo sobre una constante llamada UITableViewAutomaticDimension . El valor que estuve estimando era realmente solo una de las celdas comunes, por lo que no estaría de más cambiar esa constante. Después de cambiar a esa constante, está funcionando correctamente. No hay excepciones extrañas / fallas gráficas para informar.

Publicación original

Tengo una aplicación de iPhone bastante estándar que obtiene datos de un service web en segundo plano y muestra datos en una vista de tabla. El trabajo de actualización en segundo plano tiene su propio context de object gestionado configurado para NSPrivateQueueConcurrencyType. El controller de resultados traídos de la vista de tabla tiene su propio context de object gestionado configurado para NSMainQueueConcurrencyType. Cuando el context contextual analiza nuevos datos, pasa esos datos al context principal a través de mergeChangesFromContextDidSaveNotification . A veces durante la combinación, mi aplicación golpea una exception aquí …

 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 

Esta es la exception que veo …

 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) 

Mi aplicación está realmente marcando la exception en controllerDidChangeContent, en mi llamada a endUpdates. Básicamente, estoy viendo lo mismo que esto ( NSFetchedResultsController intentando insert ningún object ), pero tengo más información y un caso que es reproducible. Todos mis events de combinación son inserciones. Durante la combinación, no parece haber ninguna inserción, eliminación o actualización pendientes en el context de background. Inicialmente estaba usando performBlockAndWait por todas partes hasta que aprendí sobre la diferencia entre performBlock y performBlockAndWait del video de WWDC. Me cambié a performBlock, y eso lo hizo un poco mejor. Inicialmente abordé esto como un problema de subprocesss, divergió en la posibilidad de que fuera un problema de memory extraño causado por no entender completamente los bloques, y ahora estoy de vuelta a que es una condición de carrera. Parece que solo me falta una pieza. Hay dos forms en que no sucede …

(1) Registrarse para el context saveá la notificación, nula el delegado de FRC cuando lo consiga y establezca el delegado nuevamente después de la fusión. Esto no está lejos de usar un FRC en absoluto, así que esto realmente no es una opción para una solución alternativa.

(2) Haga las cosas que bloquean el hilo principal el time suficiente, por lo que la condición de carrera no ocurre. Por ejemplo, cuando agrego muchos posts de logging de debugging a mi delegado de vista de tabla, lo ralentizo lo suficiente para que no ocurra.

Estas son las que creo que son las piezas importantes de código (acorté ciertos puntos para networkingucir esta publicación ya grande).

Después de varios puntos durante el desplazamiento, el controller de vista solicitará más datos llamando a una function que tenga esto en él …

 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]; 

En su mayor parte, parseJSON realmente no tiene nada interesante …

 - (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]; } 

La implementación para save y fusionar es donde puede resultar interesante. Guardar espera que se llame desde el bloque de ejecución adecuado ya, por lo que no hace nada con el bloque de ejecución.

 - (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); } } } 

Al save, se desencadena la notificación de fusión. Solo me estoy uniendo de background a principal, así que solo quiero saber si puedo alinear la llamada de combinación o si necesito hacerlo dentro de performBlock.

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

Mis methods de delegado de controlleres de resultados buscados son cosas de placas de calderas bastante …

 - (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]; } 

Otra pieza de código que podría ser de interés. Estoy usando autolayout para las celdas de vista de tabla y la nueva API estimatedHeightForRowAtIndexPath para la altura de celda dinámica. Lo que esto significa es que durante la llamada a [self.tableView endUpdates], el último paso realmente llega a algunos objects gestionados, mientras que el otro requiere una cantidad de secciones / filas que solo necesitan saber los conteos desde el 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; } 

Estuve atrapado en esto por casi una semana ahora. Aprendí una tonelada en el process, pero estoy listo para seguir adelante. Cualquier sugerencia sería muy apreciada.

Editar

Preparé un logging de debugging bastante grande para tratar de contar la historia de lo que está sucediendo con los udpates. Estoy viendo algo realmente extraño. Estoy actualizando la tabla con 50 filas a la vez, así que solo includeé la parte interesante de mi salida de debugging. Cada vez que se configura una celda, estoy imprimiendo cuál era el título para la celda que acabo de eliminar, así como cuál será el nuevo título. Cuando presiono la última celda en la vista de tabla, hago una consulta al service web para get más datos. Este resultado está relacionado con la actualización final antes de que golpee la exception …

 // 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 configunetworking 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 triggenetworking ** 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 

En la actualización final está intentando insert en s5 r17, pero ya tenía una celda en esa fila. También intenta insert en s5 r22, pero también tenía una celda en esa fila. Por último, inserta una fila en s5 r25, que en realidad es una nueva fila. Me parece que considerar r17 y r22 como insertos está dejando un vacío en la tabla. ¿No deberían las celdas previas en esos índices tener events para moverse a r23 y r24?

Mi controller de resultados buscado está utilizando un descriptor de sorting que ordera por date y hora de inicio. Tal vez los events existentes que estaban en r17 y r22 no reciben events de movimiento porque no hubo cambios relacionados con sus NSManagedObjects. Básicamente, están obligados a moverse debido a mi tipo de descriptor para events anteriores a ellos y no porque sus datos hayan cambiado.

Editar 2:

Parece que esas inserciones simplemente activan las celdas existentes para desplazarse hacia abajo 🙁

Editar 3:

Cosas que probé hoy …

  1. El bloque de éxito AFNetworking espera que se complete la combinación antes de que regrese
  2. Hecho cellForRowAtIndexPath devuelve una celda obsoleta (básicamente, la dequeue y la devuelve de inmediato) si el controller de resultados obtenido está en el medio de beginUpdates / endUpdates. Pensando que extra random cellForRowAtIndexPath que recibe llamadas durante la actualización puede haber estado haciendo cosas extrañas.
  3. Eliminando por completo el context de background. Esto es interesante. Si hago todas las actualizaciones de la interfaz de usuario y el análisis de JSON en el context principal, todavía sucede.

Editar 4:

Ahora se está poniendo interesante.

Traté de eliminar componentes aleatorios en mi vista de tabla, como el control de actualización. También traté de deshacerse de mi uso de estimatedHeightForRowAtIndexPath, lo que significaba simplemente proporcionar una altura de fila estática en lugar de utilizar la function de reproducción automática para determinar la altura dinámica de las filas. Ambos no aparecieron nada. También intenté deshacerme completamente de mi celda personalizada y simplemente usar una celda de vista de tabla básica.

Eso funciono.

Probé una celda de vista de tabla básica con subtítulos.

Eso funciono.

Probé una celda de vista de tabla básica con subtítulos e imágenes.

Eso funciono.

La parte superior de mi seguimiento de stack que se encuentra cerca de todos los elementos relacionados con la animation comienza a tener más sentido. Parece que está relacionado con el layout automático.

Solutions Collecting From Web of "NSFetchedResultsController intentando insert un object nil"

De un ingeniero de soporte técnico de Apple:

Para proteger la integridad del almacén de datos, Core Data detecta algunas excepciones que ocurren durante sus operaciones. A veces, esto significa que si Core Data llama a su código a través de un método de delegado, Core Data puede terminar capturando las excepciones que arrojó su código.

Los errores de subprocesss múltiples son la causa más común de misteriosos problemas de datos de núcleo.

En este caso, Core Data detectó una exception a través de su método controllerDidChangeContent: causado al tratar de usar insertObject:atIndex .

La solución más probable es asegurarse de que todo su código NSManagedObject esté encapsulado dentro de performBlock: o performBlockAndWait: llamadas.

En iOS 8 y OSX Yosemite, Core Data gana la capacidad de detectar e informar violaciones de su model de concurrency. Funciona lanzando una exception cada vez que su aplicación accede a un context de object gestionado o un object gestionado desde la queue de envío incorrecta. Habilita las aserciones pasando -com.apple.CoreData.ConcurrencyDebug 1 a tu aplicación en la línea de command a través del Editor de esquemas de Xcode.

CoreData ConcurrencyDebug

Ole Begemnn tiene una excelente descripción de la nueva característica .