Implémentation d'une importation rapide et efficace des données de base sur iOS 5

101

Question : Comment puis-je obtenir mon contexte enfant pour voir les modifications persistantes sur le contexte parent afin qu'ils déclenchent mon NSFetchedResultsController pour mettre à jour l'interface utilisateur?

Voici la configuration:

Vous avez une application qui télécharge et ajoute beaucoup de données XML (environ 2 millions d'enregistrements, chacun à peu près la taille d'un paragraphe de texte normal). Le fichier .sqlite devient d'environ 500 Mo de taille. L'ajout de ce contenu dans Core Data prend du temps, mais vous voulez que l'utilisateur puisse utiliser l'application pendant que les données se chargent dans le magasin de données de manière incrémentielle. Il doit être invisible et imperceptible pour l'utilisateur que de grandes quantités de données sont déplacées, donc pas de blocage, pas de trac: des défilements comme du beurre. Néanmoins, l'application est plus utile, plus il y a de données ajoutées, nous ne pouvons donc pas attendre indéfiniment que les données soient ajoutées au magasin de données de base. Dans le code, cela signifie que j'aimerais vraiment éviter un code comme celui-ci dans le code d'importation:

[[NSRunLoop currentRunLoop] runUntilDate:[NSDate dateWithTimeIntervalSinceNow:0.25]];

L'application est iOS 5 uniquement, donc l'appareil le plus lent qu'il doit prendre en charge est un iPhone 3GS.

Voici les ressources que j'ai utilisées jusqu'à présent pour développer ma solution actuelle:

Guide de programmation des données de base d'Apple: Importer efficacement des données

  • Utilisez les pools de libération automatique pour réduire la mémoire
  • Coût des relations. Importez à plat, puis corrigez les relations à la fin
  • Ne demandez pas si vous pouvez l'aider, cela ralentit les choses d'une manière O (n ^ 2)
  • Importer par lots: enregistrer, réinitialiser, vidanger et répéter
  • Désactivez le gestionnaire d'annulation lors de l'importation

iDeveloper TV - Performances des données de base

  • Utilisez 3 contextes: types de contexte maître, principal et confinement

iDeveloper TV - Mise à jour des données de base pour Mac, iPhone et iPad

  • L'exécution de sauvegardes sur d'autres files d'attente avec performBlock accélère les choses.
  • Le cryptage ralentit les choses, désactivez-le si vous le pouvez.

Importation et affichage de grands ensembles de données dans les données de base par Marcus Zarra

  • Vous pouvez ralentir l'importation en donnant du temps à la boucle d'exécution actuelle, afin que les choses se passent bien pour l'utilisateur.
  • Un exemple de code prouve qu'il est possible d'effectuer des importations volumineuses et de garder l'interface utilisateur réactive, mais pas aussi rapidement qu'avec 3 contextes et une sauvegarde asynchrone sur disque.

Ma solution actuelle

J'ai 3 instances de NSManagedObjectContext:

masterManagedObjectContext - Il s'agit du contexte contenant le NSPersistentStoreCoordinator et responsable de l'enregistrement sur le disque. Je fais cela pour que mes sauvegardes puissent être asynchrones et donc très rapides. Je le crée au lancement comme ceci:

masterManagedObjectContext = [[NSManagedObjectContext alloc] initWithConcurrencyType:NSPrivateQueueConcurrencyType];
[masterManagedObjectContext setPersistentStoreCoordinator:coordinator];

mainManagedObjectContext - C'est le contexte que l'interface utilisateur utilise partout. Il s'agit d'un enfant de masterManagedObjectContext. Je le crée comme ceci:

mainManagedObjectContext = [[NSManagedObjectContext alloc] initWithConcurrencyType:NSMainQueueConcurrencyType];
[mainManagedObjectContext setUndoManager:nil];
[mainManagedObjectContext setParentContext:masterManagedObjectContext];

backgroundContext - Ce contexte est créé dans ma sous-classe NSOperation qui est responsable de l'importation des données XML dans Core Data. Je le crée dans la méthode principale de l'opération et je le lie au contexte maître.

backgroundContext = [[NSManagedObjectContext alloc] initWithConcurrencyType:NSConfinementConcurrencyType];
[backgroundContext setUndoManager:nil];
[backgroundContext setParentContext:masterManagedObjectContext];

Cela fonctionne vraiment très, TRÈS vite. Simplement en faisant cette configuration à 3 contextes, j'ai pu améliorer ma vitesse d'importation de plus de 10 fois! Honnêtement, c'est difficile à croire. (Cette conception de base doit faire partie du modèle standard de données de base ...)

Pendant le processus d'importation, j'enregistre 2 manières différentes. Tous les 1000 éléments que je sauvegarde dans le contexte d'arrière-plan:

BOOL saveSuccess = [backgroundContext save:&error];

Puis à la fin du processus d'importation, j'enregistre sur le contexte maître / parent qui, ostensiblement, pousse les modifications vers les autres contextes enfants, y compris le contexte principal:

[masterManagedObjectContext performBlock:^{
   NSError *parentContextError = nil;
   BOOL parentContextSaveSuccess = [masterManagedObjectContext save:&parentContextError];
}];

Problème : le problème est que mon interface utilisateur ne sera pas mise à jour tant que je n'aurai pas rechargé la vue.

J'ai un UIViewController simple avec un UITableView qui reçoit des données à l'aide d'un NSFetchedResultsController. Lorsque le processus d'importation est terminé, le NSFetchedResultsController ne voit aucun changement du contexte parent / maître et ainsi l'interface utilisateur ne se met pas à jour automatiquement comme j'ai l'habitude de voir. Si je fais sortir le UIViewController de la pile et le charge à nouveau, toutes les données sont là.

Question : Comment puis-je obtenir mon contexte enfant pour voir les modifications persistantes sur le contexte parent afin qu'ils déclenchent mon NSFetchedResultsController pour mettre à jour l'interface utilisateur?

J'ai essayé ce qui suit qui bloque simplement l'application:

- (void)saveMasterContext {
    NSNotificationCenter *notificationCenter = [NSNotificationCenter defaultCenter];    
    [notificationCenter addObserver:self selector:@selector(contextChanged:) name:NSManagedObjectContextDidSaveNotification object:masterManagedObjectContext];

    NSError *error = nil;
    BOOL saveSuccess = [masterManagedObjectContext save:&error];

    [notificationCenter removeObserver:self name:NSManagedObjectContextDidSaveNotification object:masterManagedObjectContext];
}

- (void)contextChanged:(NSNotification*)notification
{
    if ([notification object] == mainManagedObjectContext) return;

    if (![NSThread isMainThread]) {
        [self performSelectorOnMainThread:@selector(contextChanged:) withObject:notification waitUntilDone:YES];
        return;
    }

    [mainManagedObjectContext mergeChangesFromContextDidSaveNotification:notification];
}
David Weiss
la source
26
+1000000 pour la question la mieux formée et la plus préparée jamais. J'ai aussi une réponse ... Cela prendra quelques minutes pour la taper cependant ...
Jody Hagins
1
Lorsque vous dites que l'application est bloquée, où est-elle? Que fait-il?
Jody Hagins
Désolé d'en parler après un long moment. Pouvez-vous préciser ce que signifie "Importer à plat, puis réparer les relations à la fin"? N'avez-vous pas encore besoin d'avoir ces objets en mémoire pour établir des relations? J'essaie de mettre en œuvre une solution très similaire à la vôtre et je pourrais vraiment utiliser un peu d'aide pour réduire l'empreinte mémoire.
Andrea Sprega
Consultez les documents Apple liés au premier de cet article. Cela explique cela. Bonne chance!
David Weiss
1
Vraiment une bonne question et j'ai repris quelques astuces dans la description que vous avez fournie de votre configuration
djskinner

Réponses:

47

Vous devriez probablement également enregistrer le MOC principal dans les foulées. Cela n'a aucun sens d'avoir ce MOC attendre la fin pour sauver. Il a son propre fil, et cela aidera également à réduire la mémoire.

Tu as écrit:

Puis à la fin du processus d'importation, j'enregistre sur le contexte maître / parent qui, ostensiblement, pousse les modifications vers les autres contextes enfants, y compris le contexte principal:

Dans votre configuration, vous avez deux enfants (le MOC principal et le MOC d'arrière-plan), tous deux parent du «maître».

Lorsque vous enregistrez sur un enfant, les modifications sont transmises au parent. Les autres enfants de ce MOC verront les données la prochaine fois qu'ils effectueront une extraction ... ils ne seront pas explicitement notifiés.

Ainsi, lorsque BG enregistre, ses données sont poussées vers MASTER. Notez, cependant, qu'aucune de ces données n'est sur le disque jusqu'à ce que MASTER enregistre. De plus, les nouveaux éléments ne recevront pas d'identifiant permanent tant que le MASTER ne sera pas enregistré sur le disque.

Dans votre scénario, vous extrayez les données dans le MAIN MOC en fusionnant à partir de l'enregistrement MASTER pendant la notification DidSave.

Cela devrait fonctionner, donc je suis curieux de savoir où il est "accroché". Je noterai que vous n'utilisez pas le thread MOC principal de manière canonique (du moins pas pour iOS 5).

En outre, vous êtes probablement uniquement intéressé par la fusion des modifications du MOC principal (bien que votre enregistrement semble être uniquement pour cela de toute façon). Si je devais utiliser la notification update-on-did-save-notification, je le ferais ...

- (void)contextChanged:(NSNotification*)notification {
    // Only interested in merging from master into main.
    if ([notification object] != masterManagedObjectContext) return;

    [mainManagedObjectContext performBlock:^{
        [mainManagedObjectContext mergeChangesFromContextDidSaveNotification:notification];

        // NOTE: our MOC should not be updated, but we need to reload the data as well
    }];
}

Maintenant, pour ce qui peut être votre vrai problème concernant le blocage ... vous montrez deux appels différents à enregistrer sur le maître. le premier est bien protégé dans son propre performBlock, mais le second ne l'est pas (bien que vous puissiez appeler saveMasterContext dans un performBlock ...

Cependant, je changerais également ce code ...

- (void)saveMasterContext {
    NSNotificationCenter *notificationCenter = [NSNotificationCenter defaultCenter];    
    [notificationCenter addObserver:self selector:@selector(contextChanged:) name:NSManagedObjectContextDidSaveNotification object:masterManagedObjectContext];

    // Make sure the master runs in it's own thread...
    [masterManagedObjectContext performBlock:^{
        NSError *error = nil;
        BOOL saveSuccess = [masterManagedObjectContext save:&error];
        // Handle error...
        [notificationCenter removeObserver:self name:NSManagedObjectContextDidSaveNotification object:masterManagedObjectContext];
    }];
}

Cependant, notez que MAIN est un enfant de MASTER. Ainsi, il ne devrait pas avoir à fusionner les modifications. Au lieu de cela, surveillez simplement le DidSave sur le maître et récupérez simplement! Les données sont déjà dans votre parent, n'attendant que vous les demandez. C'est l'un des avantages d'avoir les données dans le parent en premier lieu.

Une autre alternative à considérer (et je serais intéressé de connaître vos résultats - c'est beaucoup de données) ...

Au lieu de faire du MOC d'arrière-plan un enfant du MASTER, faites-en un enfant du MAIN.

Obtenez ceci. Chaque fois que le BG enregistre, il est automatiquement poussé dans le MAIN. Maintenant, le MAIN doit appeler save, puis le master doit appeler save, mais tout ce qu'ils font est de déplacer des pointeurs ... jusqu'à ce que le master enregistre sur le disque.

La beauté de cette méthode est que les données vont du MOC d'arrière-plan directement dans le MOC de vos applications (puis passent pour être enregistrées).

Il y a une pénalité pour le passage, mais tout le gros du travail se fait dans le MASTER lorsqu'il touche le disque. Et si vous lancez ces sauvegardes sur le maître avec performBlock, le thread principal envoie simplement la requête et retourne immédiatement.

S'il vous plaît laissez-moi savoir comment ça se passe!

Jody Hagins
la source
Excellente réponse. Je vais essayer ces idées aujourd'hui et voir ce que je découvre. Je vous remercie!
David Weiss
Impressionnant! Cela a parfaitement fonctionné! Néanmoins, je vais essayer votre suggestion de MASTER -> MAIN -> BG et voir comment cette performance fonctionne, cela semble être une idée très intéressante. Merci pour les bonnes idées!
David Weiss
4
Mis à jour pour changer performBlockAndWait en performBlock. Je ne sais pas pourquoi cela est réapparu dans ma file d'attente, mais quand je l'ai lu cette fois, c'était évident ... je ne sais pas pourquoi je l'ai laissé passer avant. Oui, performBlockAndWait est ré-entrant. Cependant, dans un environnement imbriqué comme celui-ci, vous ne pouvez pas appeler la version synchrone sur un contexte enfant à partir d'un contexte parent. La notification peut être (dans ce cas) envoyée à partir du contexte parent, ce qui peut provoquer un blocage. J'espère que cela est clair pour quiconque viendra et lira ceci plus tard. Merci, David.
Jody Hagins
1
@DavidWeiss Avez-vous essayé MASTER -> MAIN -> BG? Je suis intéressé par ce modèle de conception et j'espère savoir s'il fonctionne bien pour vous. Je vous remercie.
nonamelive le
2
Le problème avec MASTER -> MAIN -> BG pattern est lorsque vous récupérez à partir du contexte BG, il va également chercher à partir de MAIN et cela bloquera l'interface utilisateur et vous rendra l'application non réactive
Rostyslav