ios objective-c uitableview core-data nsfetchedresultscontroller

ios - NSFetchedResultsController que intenta insertar un objeto nil



objective-c uitableview (1)

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 delegado, Core Data puede terminar capturando excepciones que su código arrojó.

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

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

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

En iOS 8 y OSX Yosemite, Core Data obtiene la capacidad de detectar e informar violaciones de su modelo de simultaneidad. Funciona lanzando una excepción siempre que su aplicación acceda a un contexto de objeto gestionado u objeto gestionado desde la cola de despacho incorrecta. Habilita las aserciones pasando -com.apple.CoreData.ConcurrencyDebug 1 a su aplicación en la línea de comando a través del Editor de esquemas de Xcode.

Ole Begemann tiene una gran descripción de la nueva característica .

Editar 7:

Aquí está mi método de guardar. Es bastante repetitivo. Las macros DEBUG_LOG () solo se ejecutan si es una compilación de depuración.

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

Editar 6:

iOS 8 está aquí y este problema ha vuelto. Suerte la mía. Anteriormente, reduje el problema al uso de estimateRowHeight en las vistas de tabla (por cierto, nunca solucioné completamente el problema. Dejé de usar estimadoRowHeight). Ahora estoy viendo este problema nuevamente bajo diferentes circunstancias. Lo rastreé hasta un compromiso de hace unos días cuando hice translúcidas mis barras de navegación / tabulación. Esto incluía la desactivación de ''Ajustar las configuraciones de la vista de desplazamiento'' en el guión gráfico y marcar las casillas para que mis vistas se muestren debajo de las barras superiores y las barras inferiores. Hay una serie de pasos que debo hacer para que esto ocurra, pero puedo reproducirlo todo el tiempo con mi guión gráfico configurado de esa manera. Si revertí esa confirmación, ya no sucede.

Mientras digo, "ya no sucede", lo que realmente pienso es que esto solo hace que sea menos probable que suceda. Este error es un b absoluto. Mi reacción visceral ahora es que esto es un error de iOS. Simplemente no sé qué puedo hacer para convertir esto en un informe de error. Es una locura.

Editar 5:

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

Mi última edición notó que cuando usé una celda de vista de tabla básica, todo funcionó bien. Mi siguiente línea de acción iba a ser intentar desde cero construir una nueva celda personalizada pieza por pieza y ver dónde se equivocó. Por el placer de hacerlo, volví a habilitar mi antiguo código de celda personalizado y funcionó bien. Uhhh? Oh, espera, todavía tengo estimatedHeightForRowAtIndexPath comentado. Cuando eliminé esos comentarios y habilité el valor estimatedHeightForRowAtIndexPath , se volvió horrible nuevamente. Interesante.

Busqué ese método en el documento API, y mencionó algo sobre una constante llamada UITableViewAutomaticDimension . El valor que estaba estimando era realmente solo una de las alturas de celda comunes, por lo que no estaría de más alterar esa constante. Después de cambiar a esa constante, funciona correctamente. No hay excepciones extrañas / problemas técnicos gráficos para informar.

Publicación original

Tengo una aplicación de iPhone bastante estándar que obtiene datos de un servicio web en segundo plano y muestra datos en una vista de tabla. El trabajo de actualización de fondo tiene su propio contexto de objeto gestionado configurado para NSPrivateQueueConcurrencyType. El controlador de resultados obtenidos de mi vista de tabla tiene su propio contexto de objeto gestionado configurado para NSMainQueueConcurrencyType. Cuando el contexto de fondo analiza datos nuevos, pasa esos datos al contexto principal a través de mergeChangesFromContextDidSaveNotification . A veces, durante la fusión, mi aplicación hace una excepción 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 excepción 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á llegando a la excepción en controllerDidChangeContent, en mi llamada a endUpdates. Básicamente estoy viendo lo mismo que esto (¿ NSFetchedResultsController intenta insertar un objeto nil? ), Pero tengo más información y un caso que es reproducible. Todos mis eventos de fusión son insertos. Durante la fusión, no parece haber inserciones, eliminaciones o actualizaciones pendientes en el contexto de fondo. Inicialmente estaba usando performBlockAndWait por todas partes hasta que supe la diferencia entre performBlock y performBlockAndWait del video WWDC. Cambié a performBlock, y eso lo hizo un poco mejor. Inicialmente me acerqué a esto como un problema de enhebrado, divergí en la posibilidad de que se tratara de un extraño problema de memoria causado por no entender completamente los bloques, y ahora vuelvo a que es una condición de carrera. Parece que hay una sola pieza que me estoy perdiendo. Hay dos formas en que no sucede ...

(1) Registrarse para el contexto guardará la notificación, anulará el delegado de FRC cuando lo obtenga, y restablecerá al delegado después de la fusión. Esto no está lejos de no usar un FRC en absoluto, así que esto realmente no es una opción para una solución alternativa.

(2) Haga cosas que bloqueen el hilo principal el tiempo suficiente, por lo que la condición de carrera no ocurre. Por ejemplo, cuando agrego muchos mensajes de registro de depuración a mi delegado de vista de tabla, eso lo ralentiza lo suficiente como para que no suceda.

Aquí están los que creo que son las piezas importantes de código (he reducido algunos puntos para reducir esta publicación ya grande).

Después de varios puntos durante el desplazamiento, el controlador de vista solicitará más datos llamando a una función que tiene esto en ella ...

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 en eso ...

- (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 guardar y fusionar es donde podría ser interesante. Save espera ser llamado desde dentro del performBlock apropiado, por lo que no hace nada con performBlock.

- (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 guardar, la notificación de fusión se activa. Solo me estoy fusionando de fondo a principal, así que solo quiero saber si puedo alinear la llamada de fusión o si tengo que hacerlo dentro de performBlock.

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

Mis métodos de delegado del controlador de resultados obtenidos son cosas bonitas en la placa de la caldera ...

- (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 mis celdas de vista de tabla, y la nueva API estimada de HeighForRowAtIndexPath para altura de celda dinámica. Lo que esto significa es que durante la llamada a [self.tableView endUpdates], el último paso realmente se extiende hacia abajo en algunos objetos administrados, mientras que las otras llamadas para el número de secciones / filas solo necesitan conocer los recuentos del 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 atascado en esto por casi una semana. Aprendí un montón en el proceso, pero caramba estoy listo para seguir adelante. Cualquier sugerencia sería muy apreciada.

Editar

Junté un registro de depuración bastante grande para tratar de contar la historia de lo que está pasando con los udpates. Estoy viendo algo realmente extraño. Estoy actualizando la tabla con 50 filas a la vez, así que solo incluiré la parte interesante de mi resultado de depuración. Cada vez que se configura una celda, estoy imprimiendo cuál fue el título para la celda que acabo de quitar, así como cuál será el nuevo título. Cuando presiono la última celda en la vista de tabla, realizo una consulta al servicio web para obtener más información. Esta salida está relacionada con la actualización final antes de llegar a la excepción ...

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

En la actualización final está intentando insertar en s5 r17, pero ya tenía una celda en esa fila. También intenta insertar 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 inserciones está dejando un espacio en la mesa. ¿No deberían las celdas anteriores en esos índices tener eventos para mover a r23 y r24?

El controlador de resultados que obtuve está utilizando un descriptor de clasificación que ordena por fecha y hora de inicio. Quizás los eventos existentes que estaban en r17 y r22 no están recibiendo eventos de movimiento porque no hubo ningún cambio relacionado con sus NSManagedObjects. Esencialmente, se les requiere que se muevan debido a mi tipo de descriptor para eventos anteriores a ellos y no porque sus datos hayan cambiado.

Editar 2:

Parece que esas inserciones solo activan las celdas existentes para desplazarse hacia abajo :(

Editar 3:

Cosas que intenté hoy ...

  1. El bloque de éxito de AFNetworking espera a que termine la fusión antes de que regrese
  2. Made cellForRowAtIndexPath devuelve una celda obsoleta (esencialmente dequeue y la devuelve de inmediato) si el controlador de resultados obtenido está en el medio de beginUpdates / endUpdates. Pensando que extra random cellForRowAtIndexPath que se llama durante la actualización puede haber estado haciendo cosas raras.
  3. Eliminando el contexto de fondo por completo. Esto es interesante. Si realizo todas las actualizaciones de UI Y el análisis de JSON en el contexto principal, aún 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 deshacerme de mi uso de estimateHeightForRowAtIndexPath, lo que significaba simplemente suministrar una altura de fila estática en lugar de utilizar el autolayout para determinar la altura de fila dinámica. Ninguno de los dos apareció nada. También traté de deshacerme de mi celda personalizada por completo, y simplemente usar una celda de vista de tabla básica.

Eso funciono.

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

Eso funciono.

Intenté una celda de vista de tabla básica con subtítulos e imagen.

Eso funciono.

La parte superior de mi rastro de pila que está cerca de todos los elementos relacionados con la animación está empezando a tener más sentido. Parece que esto está relacionado con el diseño automático.