NSFetchedResultsController tentant d'insert un object nil

Modifier 7:

Voici ma méthode de sauvegarde. C'est plutôt cool. Les macros DEBUG_LOG () ne sont exécutées que s'il s'agit d'une version 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); } } 

Modifier 6:

iOS 8 est ici et ce problème est de return. J'ai de la chance. Auparavant, j'avais réduit le problème à l'utilisation de estiméRowHeight sur les vues de table (btw, je n'ai jamais complètement résolu le problème, j'ai juste arrêté d'utiliser estiméRowHeight). Maintenant, je vois ce problème dans d'autres circonstances. Je l'ai suivi jusqu'à un commit d'il y a quelques jours quand j'ai rendu mes barres de navigation / tabulation translucides. Cela incluait la désactivation de «Ajuster les incrustations de vue de défilement» dans le storyboard et la sélection des cases pour que mes vues s'affichent sous les barres supérieures et inférieures. Il y a une série de mesures que je dois faire pour y arriver, mais je peux le reproduire à chaque fois avec mon storyboard configuré de cette façon. Si je returnne ce commit, cela ne se produit plus.

Même si je dis «ça n'arrive plus», ce que je pense vraiment, c'est que cela rend les choses less probables. Ce bug est un b **** absolu. Mon intuition est maintenant que c'est un bug iOS. Je ne sais pas ce que je peux faire pour transformer ceci en rapport de bogue. C'est de la folie.

Modifier 5:

Si vous voulez lire l'intégralité de ma misère, s'il vous plaît continuer tout au long de ce post. Si vous rencontrez ce problème et que vous voulez simplement de l'aide, voici quelque chose à regarder.

Ma dernière vérification a noté que lorsque j'ai utilisé une cellule de base de table, tout a bien fonctionné. Mon prochain plan d'action allait consister à essayer de build une nouvelle cellule personnalisée pièce par pièce et de voir où elle se trouvait. Pour le coup, j'ai réactivé mon ancien code de cellule personnalisé et cela a très bien fonctionné. Euhhh? Oh attends, j'ai encore estimatedHeightForRowAtIndexPath commenté. Lorsque j'ai supprimé ces commentaires et activé estimatedHeightForRowAtIndexPath , il est redevenu nul. Intéressant.

J'ai recherché cette méthode dans le doc API, et il a mentionné quelque chose à propos d'une constante appelée UITableViewAutomaticDimension . La valeur que j'estimais était vraiment juste une des hauteurs de cellules communes, donc ça ne ferait pas de mal de passer à cette constante. Après avoir basculé vers cette constante, cela fonctionne correctement. Pas d'exceptions bizarres / pépins charts à signaler.

Message original

J'ai une application iPhone assez standard qui récupère datatables d'un service Web en arrière-plan et affiche datatables dans une vue de table. Le travail de mise à jour en arrière-plan possède son propre context d'object géré configuré pour NSPrivateQueueConcurrencyType. Le controller de résultats récupérés de ma vue de table possède son propre context d'object géré configuré pour NSMainQueueConcurrencyType. Lorsque le context d'arrière-plan parsing de nouveldatatables, il transmet ces données au context principal via mergeChangesFromContextDidSaveNotification . Parfois, lors de la fusion, mon application frappe une exception ici …

 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 

Voici l'exception que je vois …

 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) 

Mon application est en train de bash l'exception dans controllerDidChangeContent, lors de mon appel à endUpdates. Je vois fondamentalement la même chose que ceci ( NSFetchedResultsController essayant d'insert l'object nil? ), Mais j'ai plus d'information et un cas qui est reproductible. Tous mes events de fusion sont des inserts. Pendant la fusion, il ne semble pas y avoir d'insertions en attente, de suppressions ou de mises à jour sur le context d'arrière-plan. Au début, j'utilisais performBlockAndWait partout jusqu'à ce que j'apprenne la différence entre performBlock et performBlockAndWait de la video WWDC. Je suis passé à performBlock, et ça l'a rendu un peu meilleur. Au départ, j'ai abordé cette question comme un problème de threading, divergé dans la possibilité d'être un problème de memory étrange causé par ne pas comprendre complètement les blocs, et maintenant je suis de return à ce qu'il s'agit d'une condition de course. Il semble qu'il n'y ait qu'une seule pièce qui me manque. Il y a deux façons de ne pas arriver …

(1) Enregistrer pour le context va save la notification, éliminer le délégué FRC quand je l'obtiens, et redéfinir le délégué après la fusion. Ce n'est pas loin de ne pas utiliser de FRC du tout, donc ce n'est vraiment pas une option pour une solution de contournement.

(2) Faites des choses qui bloquent le fil principal assez longtime, de sorte que la condition de course ne se produise pas. Par exemple, lorsque j'ajoute beaucoup de messages de journal de debugging à mon délégué de vue de table, cela le ralentit suffisamment pour que cela ne se produise pas.

Voici ce que je crois être les morceaux importants de code (j'ai raccourci certaines taches pour rétrécir ce post déjà grand).

Après différents points pendant le défilement, le controller de vue requestra plus de données en appelant une fonction qui a ceci dedans …

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

Pour la plupart, parseJSON n'a vraiment rien d'intéressant …

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

L'implémentation pour save et merge est là où elle pourrait devenir intéressante. Save s'attend à être déjà appelé depuis performBlock, donc il ne fait rien avec 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); } } } 

Lors de l'logging, la notification de fusion est déclenchée. Je ne fais que merge d'arrière-plan à principal, donc je veux juste juste savoir si je peux intégrer l'appel de fusion ou si j'ai besoin de le faire à l'intérieur de performBlock.

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

Mes methods de délégué de controller de résultats récupérées sont de jolis trucs de plaque de chaudière …

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

Un autre morceau de code qui pourrait être intéressant. J'utilise autolayout pour mes cellules de vue de table, et la nouvelle API estiméHeightForRowAtIndexPath pour la hauteur de cellule dynamic. Cela signifie que lors de l'appel de [self.tableView endUpdates], la dernière étape atteint effectivement certains objects gérés, alors que les autres appels pour le nombre de sections / lignes n'ont besoin de connaître que les numbers de la 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; } 

Été bloqué sur cela pendant presque une semaine maintenant. Appris une tonne dans le process, mais je suis prêt à passer à autre chose. Toutes les suggestions seraient grandement appréciées.

modifier

J'ai mis en place un assez gros journal de debugging pour essayer de raconter l'histoire de ce qui se passe avec les mises à jour. Je vois quelque chose de vraiment étrange. Je mets à jour la table avec 50 lignes à la fois, donc j'inclurai seulement la partie intéressante de ma sortie de debugging. Chaque fois qu'une cellule est configurée, j'imprime le titre de la cellule que je viens de déquiler ainsi que le nouveau titre. Lorsque j'atteins la dernière cellule de la vue tableau, je fais une requête au service web pour plus de données. Cette sortie est liée à la mise à jour finale avant que j'atteigne l'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 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 sortinggger 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 sortingggered ** 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 

Dans la mise à jour finale, il essaie d'insert à s5 r17, mais j'avais déjà une cellule à cette rangée. Il essaye aussi d'insert à s5 r22, mais j'avais déjà aussi une cellule à cette rangée. Enfin, il insère une ligne à s5 r25, qui est en fait une nouvelle ligne. Il me semble que considérer les r17 et les r22 comme des inserts, c'est laisser un vide dans le tableau. Les cellules précédentes de ces index ne devraient-elles pas avoir des events à déplacer vers r23 et r24?

Mon controller de résultats récupérés utilise un descripteur de sorting qui sortinge par date et heure de début. Peut-être que les events existants qui se trouvaient à r17 et r22 n'obtiennent pas d'events de déplacement car il n'y a eu aucun changement lié à leur NSManagedObjects. Essentiellement, ils sont obligés de se déplacer en raison de mon descripteur de sorting pour les events antérieurs à eux et non parce que leurs données ont changé.

Édition 2:

On dirait que ces inserts ne font que triggersr le déplacement des cellules existantes 🙁

Édition 3:

Choses que j'ai essayé aujourd'hui …

  1. Le bloc de succès AFNetworking fait attendre la fin de la fusion avant son return
  2. Fait cellForRowAtIndexPath returnner une cellule périmée (essentiellement la déqueue et la renvoyer tout de suite) si le controller de résultats récupérés est au milieu de beginUpdates / endUpdates. Penser que cellForRowAtIndexPath extra-random qui est appelé au cours de la mise à jour peut avoir fait des choses étranges.
  3. Suppression totale du context d'arrière-plan. C'est intéressant. Si je fais toutes les mises à jour de l'interface user et l'parsing JSON sur le context principal, cela arrive toujours.

Modifier 4:

Maintenant ça devient intéressant.

J'ai essayé de supprimer des composants randoms dans ma vue de table tels que le contrôle d'actualisation. Également essayé de se débarrasser de mon utilisation de estiméHeightForRowAtIndexPath, ce qui signifiait simplement fournir une hauteur de ligne statique au lieu d'utiliser autolayout pour déterminer la hauteur de ligne dynamic. Les deux n'ont rien changé. J'ai également essayé de me débarrasser entièrement de ma cellule personnalisée, et d'utiliser simplement une cellule de vue de table de base.

Ça a marché.

J'ai essayé une cellule de base de vue de table avec des sous-titres.

Ça a marché.

J'ai essayé une cellule de vue de table de base avec sous-titre et image.

Ça a marché.

Le haut de ma trace de stack étant proche de tous ces éléments liés à l'animation commence à avoir plus de sens. Il semble que ceci soit lié à la layout automatique.

D'un ingénieur de support technique Apple:

Pour protéger l'intégrité du magasin de données, Core Data récupère certaines exceptions qui se produisent pendant ses opérations. Cela signifie parfois que si datatables de base appellent votre code via une méthode déléguée, datatables de base peuvent finir par intercepter les exceptions que votre code a générées.

Les erreurs multi-threading sont la cause la plus commune des problèmes mystérieux de données de base.

Dans ce cas, datatables de base ont intercepté une exception par le biais de votre controllerDidChangeContent: méthode, provoquée en essayant d'utiliser insertObject:atIndex .

La solution la plus probable consiste à s'assurer que tout votre code NSManagedObject est encapsulé dans performBlock: ou performBlockAndWait: calls.

Dans iOS 8 et OSX Yosemite, Core Data a la capacité de détecter et de signaler les violations de son model de concurrency. Il fonctionne en lançant une exception chaque fois que votre application accède à un context d'object géré ou à un object géré à partir de la mauvaise queue de répartition. Vous activez les assertions en passant -com.apple.CoreData.ConcurrencyDebug 1 à votre application sur la command line via l'éditeur de schéma de Xcode.

CoreData ConcurrencyDebug

Ole Begemann a un bon résumé de la nouvelle fonctionnalité .