web-dev-qa-db-fra.com

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

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

Voici la configuration:

Vous disposez d'une application qui télécharge et ajoute de nombreuses données XML (environ 2 millions d'enregistrements, chacun ayant à peu près la taille d'un paragraphe de texte normal) .Le fichier .sqlite atteint une taille d'environ 500 Mo. 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 de manière incrémentielle dans le magasin de données. Il doit être invisible et imperceptible pour l'utilisateur que de grandes quantités de données soient déplacées, donc pas de blocages, pas de tremblements: défile comme du beurre. Pourtant, 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 Core Data. Dans le code, cela signifie que j'aimerais vraiment éviter le code comme celui-ci dans le code d'importation:

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

L'application est uniquement iOS 5, 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: importation efficace de 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ésactiver le gestionnaire d'annulation lors de l'importation

iDeveloper TV - Core Data Performance

  • Utilisez 3 contextes: types de contexte Master, Main 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 en cours, pour que les choses se déroulent bien pour l'utilisateur.
  • L'exemple de code prouve qu'il est possible d'effectuer d'importations importantes et de garder l'interface utilisateur réactive, mais pas aussi rapidement qu'avec 3 contextes et une sauvegarde asynchrone sur le disque.

Ma solution actuelle

J'ai 3 instances de NSManagedObjectContext:

masterManagedObjectContext - C'est le contexte qui a le NSPersistentStoreCoordinator et est responsable de l'enregistrement sur le disque. Je le fais pour que mes sauvegardes soient 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 chargée d'importer les données XML dans Core Data. Je le crée dans la méthode principale de l'opération et le relie au contexte principal là-bas.

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

Cela fonctionne en fait très, TRÈS rapidement. Juste en faisant cette configuration de 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 devrait faire partie du modèle standard de données de base ...)

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

BOOL saveSuccess = [backgroundContext save:&error];

Ensuite, à la fin du processus d'importation, j'économise 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 se mettra pas à jour tant que je ne rechargerai pas la vue.

J'ai un UIViewController simple avec un UITableView qui est alimenté en données à l'aide d'un NSFetchedResultsController. Une fois le processus d'importation terminé, le NSFetchedResultsController ne voit aucune modification par rapport au contexte parent/maître et donc l'interface utilisateur ne se met pas à jour automatiquement comme j'ai l'habitude de le voir. Si je fais sortir l'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 changements persistants 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 juste 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];
}
97
David Weiss

Vous devriez probablement également enregistrer le MOC maître dans les foulées. Cela n'a aucun sens que le MOC attende la fin pour enregistrer. Il a son propre thread, et cela aidera également à réduire la mémoire.

Tu as écrit:

Ensuite, à la fin du processus d'importation, j'économise 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és au "maître".

Lorsque vous économisez sur un enfant, il pousse les modifications vers le haut dans le parent. Les autres enfants de ce MOC verront les données la prochaine fois qu'ils effectueront une extraction ... ils ne sont 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, aucun nouvel élément ne recevra d'ID permanent tant que le MASTER n'aura pas été enregistré sur le disque.

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

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

De plus, vous n'êtes probablement intéressé que par la fusion des modifications du MOC maître (bien que votre inscription semble être uniquement pour cela). Si je devais utiliser la notification de mise à jour sur sauvegarde, 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 affichez deux appels différents pour économiser sur le maître. le premier est bien protégé dans son propre performBlock, mais pas le second (même si vous appelez saveMasterContext dans un performBlock ...

Cependant, je changerais aussi 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 le MAIN est un enfant de MASTER. Il ne devrait donc 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 vos parents, attendant juste que vous les demandiez. 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é d'entendre parler de 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 ça. Chaque fois que le BG enregistre, il est automatiquement poussé dans le MAIN. Maintenant, le MAIN doit appeler save, puis le maître doit appeler save, mais tout ce que cela fait c'est déplacer des pointeurs ... jusqu'à ce que le maître 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 certains pénalité pour la transmission, mais tout le gros du travail se fait dans le MASTER quand il frappe le disque. Et si vous effectuez ces sauvegardes sur le maître avec performBlock, le thread principal envoie simplement la demande et revient immédiatement.

Faites-moi savoir comment ça se passe!

47
Jody Hagins