Implementierung eines schnellen und effizienten Kerndatenimports unter iOS 5

101

Frage : Wie kann ich in meinem untergeordneten Kontext feststellen, dass Änderungen im übergeordneten Kontext bestehen bleiben, sodass mein NSFetchedResultsController die Benutzeroberfläche aktualisiert?

Hier ist das Setup:

Sie haben eine App, die viele XML-Daten herunterlädt und hinzufügt (ungefähr 2 Millionen Datensätze, jeder ungefähr so ​​groß wie ein normaler Textabschnitt). Die .sqlite-Datei wird ungefähr 500 MB groß. Das Hinzufügen dieses Inhalts zu Core Data benötigt Zeit, aber Sie möchten, dass der Benutzer die App verwenden kann, während die Daten schrittweise in den Datenspeicher geladen werden. Es muss für den Benutzer unsichtbar und nicht wahrnehmbar sein, dass große Datenmengen verschoben werden, also keine Hänge, keine Jitter: Schriftrollen wie Butter. Die App ist jedoch umso nützlicher, je mehr Daten hinzugefügt werden. Wir können also nicht ewig warten, bis die Daten zum Core Data Store hinzugefügt werden. Im Code bedeutet dies, dass ich Code wie diesen im Importcode wirklich vermeiden möchte:

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

Die App ist nur für iOS 5 verfügbar. Das langsamste Gerät, das unterstützt werden muss, ist ein iPhone 3GS.

Hier sind die Ressourcen, die ich bisher zur Entwicklung meiner aktuellen Lösung verwendet habe:

Apples Core Data Programming Guide: Effizientes Importieren von Daten

  • Verwenden Sie Autorelease-Pools, um den Speicher niedrig zu halten
  • Beziehungskosten. Importieren Sie flach und flicken Sie am Ende die Beziehungen
  • Fragen Sie nicht, ob Sie helfen können, es verlangsamt die Dinge auf eine O (n ^ 2) Weise
  • In Chargen importieren: speichern, zurücksetzen, entleeren und wiederholen
  • Schalten Sie den Rückgängig-Manager beim Import aus

iDeveloper TV - Kerndatenleistung

  • Verwenden Sie 3 Kontexte: Master-, Haupt- und Confinement-Kontexttypen

iDeveloper TV - Kerndaten für Mac, iPhone und iPad Update

  • Das Ausführen von Speichern in anderen Warteschlangen mit performBlock beschleunigt die Arbeit.
  • Die Verschlüsselung verlangsamt die Arbeit. Schalten Sie sie aus, wenn Sie können.

Importieren und Anzeigen großer Datenmengen in Kerndaten von Marcus Zarra

  • Sie können den Import verlangsamen, indem Sie der aktuellen Ausführungsschleife Zeit geben, damit sich der Benutzer reibungslos fühlt.
  • Beispielcode beweist, dass es möglich ist, große Importe durchzuführen und die Benutzeroberfläche ansprechbar zu halten, jedoch nicht so schnell wie bei 3 Kontexten und asynchronem Speichern auf der Festplatte.

Meine aktuelle Lösung

Ich habe 3 Instanzen von NSManagedObjectContext:

masterManagedObjectContext - Dies ist der Kontext, der über den NSPersistentStoreCoordinator verfügt und für das Speichern auf der Festplatte verantwortlich ist. Ich mache das, damit meine Speicherungen asynchron und daher sehr schnell sein können. Ich erstelle es beim Start so:

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

mainManagedObjectContext - Dies ist der Kontext, den die Benutzeroberfläche überall verwendet. Es ist ein untergeordnetes Element des masterManagedObjectContext. Ich erstelle es so:

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

backgroundContext - Dieser Kontext wird in meiner NSOperation-Unterklasse erstellt, die für den Import der XML-Daten in Core Data verantwortlich ist. Ich erstelle es in der Hauptmethode der Operation und verknüpfe es dort mit dem Master-Kontext.

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

Dies funktioniert tatsächlich sehr, sehr schnell. Durch dieses 3-Kontext-Setup konnte ich meine Importgeschwindigkeit um mehr als das 10-fache verbessern! Ehrlich gesagt ist das schwer zu glauben. (Dieses grundlegende Design sollte Teil der Standardvorlage für Kerndaten sein ...)

Während des Importvorgangs speichere ich 2 verschiedene Arten. Alle 1000 Elemente speichere ich im Hintergrundkontext:

BOOL saveSuccess = [backgroundContext save:&error];

Am Ende des Importvorgangs speichere ich dann den Master- / Elternkontext, der angeblich Änderungen an den anderen untergeordneten Kontexten einschließlich des Hauptkontexts überträgt:

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

Problem : Das Problem ist, dass meine Benutzeroberfläche erst aktualisiert wird, wenn ich die Ansicht neu lade.

Ich habe einen einfachen UIViewController mit einem UITableView, dem Daten mit einem NSFetchedResultsController zugeführt werden. Wenn der Importvorgang abgeschlossen ist, werden im NSFetchedResultsController keine Änderungen gegenüber dem übergeordneten / Master-Kontext angezeigt, sodass die Benutzeroberfläche nicht automatisch aktualisiert wird, wie ich es gewohnt bin. Wenn ich den UIViewController vom Stapel nehme und erneut lade, sind alle Daten vorhanden.

Frage : Wie kann ich in meinem untergeordneten Kontext feststellen, dass Änderungen im übergeordneten Kontext bestehen bleiben, sodass mein NSFetchedResultsController die Benutzeroberfläche aktualisiert?

Ich habe folgendes versucht, das nur die App hängt:

- (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
quelle
26
+1000000 für die am besten geformte und am besten vorbereitete Frage aller Zeiten. Ich habe auch eine Antwort ... Es wird ein paar Minuten dauern, bis ich sie abtippe ...
Jody Hagins
1
Wenn Sie sagen, dass die App hängt, wo ist sie? Was macht es?
Jody Hagins
Tut mir leid, dass ich das nach langer Zeit angesprochen habe. Können Sie bitte klarstellen, was "Flat importieren, dann Beziehungen am Ende flicken" bedeutet? Müssen Sie diese Objekte nicht noch im Speicher haben, um Beziehungen aufzubauen? Ich versuche, eine Lösung zu implementieren, die Ihrer sehr ähnlich ist, und ich könnte wirklich Hilfe gebrauchen, um den Speicherbedarf zu verringern.
Andrea Sprega
Weitere Informationen finden Sie in den Apple-Dokumenten, die mit dem ersten Teil dieses Artikels verknüpft sind. Es erklärt dies. Viel Glück!
David Weiss
1
Wirklich gute Frage und ich habe ein paar nette Tricks aus der Beschreibung Ihres Setups
aufgegriffen

Antworten:

47

Sie sollten das Master-MOC wahrscheinlich auch in Schritten speichern. Es macht keinen Sinn, dass dieser MOC bis zum Ende wartet, um zu speichern. Es hat einen eigenen Thread und hilft auch dabei, den Speicher niedrig zu halten.

Sie schrieben:

Am Ende des Importvorgangs speichere ich dann den Master- / Elternkontext, der angeblich Änderungen an den anderen untergeordneten Kontexten einschließlich des Hauptkontexts überträgt:

In Ihrer Konfiguration haben Sie zwei untergeordnete Elemente (das Haupt-MOC und das Hintergrund-MOC), die beide dem "Master" übergeordnet sind.

Wenn Sie ein Kind speichern, werden die Änderungen in das übergeordnete Element übertragen. Andere Kinder dieses MOC sehen die Daten, wenn sie das nächste Mal einen Abruf durchführen ... sie werden nicht explizit benachrichtigt.

Wenn BG speichert, werden seine Daten an MASTER übertragen. Beachten Sie jedoch, dass sich keine dieser Daten auf der Festplatte befindet, bis MASTER sie speichert. Darüber hinaus erhalten neue Elemente erst dann permanente IDs, wenn der MASTER auf der Festplatte gespeichert wird.

In Ihrem Szenario ziehen Sie die Daten in das MAIN MOC, indem Sie sie während der DidSave-Benachrichtigung aus dem MASTER-Speicher zusammenführen.

Das sollte funktionieren, also bin ich gespannt, wo es "aufgehängt" ist. Ich werde bemerken, dass Sie nicht auf kanonische Weise auf dem Haupt-MOC-Thread ausgeführt werden (zumindest nicht für iOS 5).

Außerdem sind Sie wahrscheinlich nur daran interessiert, Änderungen aus dem Master-MOC zusammenzuführen (obwohl Ihre Registrierung ohnehin nur dafür gedacht ist). Wenn ich die Update-on-Did-Save-Benachrichtigung verwenden würde, würde ich dies tun ...

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

Nun, für das, was Ihr eigentliches Problem in Bezug auf den Hang sein könnte ... zeigen Sie zwei verschiedene Anrufe, um sie auf dem Master zu speichern. Der erste ist in seinem eigenen performBlock gut geschützt, der zweite jedoch nicht (obwohl Sie möglicherweise saveMasterContext in einem performBlock aufrufen ...

Ich würde jedoch auch diesen Code ändern ...

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

Beachten Sie jedoch, dass der MAIN ein Kind von MASTER ist. Es sollte also nicht erforderlich sein, die Änderungen zusammenzuführen. Achten Sie stattdessen einfach auf den DidSave auf dem Master und holen Sie ihn erneut! Die Daten befinden sich bereits in Ihrem Elternteil und warten nur darauf, dass Sie danach fragen. Dies ist einer der Vorteile, wenn die Daten in erster Linie im übergeordneten Element gespeichert sind.

Eine weitere Alternative (und ich würde mich über Ihre Ergebnisse freuen - das sind viele Daten) ...

Anstatt den Hintergrund-MOC zu einem Kind des MASTER zu machen, machen Sie ihn zu einem Kind des MAIN.

Bekomme das. Jedes Mal, wenn der Hintergrund gespeichert wird, wird er automatisch in den MAIN verschoben. Jetzt muss der MAIN save aufrufen, und dann muss der Master save aufrufen, aber alles, was diese tun, ist, Zeiger zu verschieben ... bis der Master auf der Festplatte speichert.

Das Schöne an dieser Methode ist, dass die Daten vom Hintergrund-MOC direkt in das MOC Ihrer Anwendung gelangen (und dann zum Speichern weitergeleitet werden).

Es gibt eine Strafe für den Durchgang, aber das ganze schwere Heben wird im MASTER erledigt, wenn es auf die Scheibe trifft. Und wenn Sie diese Sicherungen mit performBlock auf dem Master ausführen, sendet der Hauptthread die Anforderung einfach ab und kehrt sofort zurück.

Bitte lassen Sie mich wissen, wie es geht!

Jody Hagins
quelle
Hervorragende Antwort. Ich werde diese Ideen heute ausprobieren und sehen, was ich entdecke. Danke dir!
David Weiss
Genial! Das hat perfekt funktioniert! Trotzdem werde ich Ihren Vorschlag von MASTER -> MAIN -> BG ausprobieren und sehen, wie diese Leistung funktioniert. Das scheint eine sehr interessante Idee zu sein. Vielen Dank für die tollen Ideen!
David Weiss
4
Aktualisiert, um performBlockAndWait in performBlock zu ändern. Ich bin mir nicht sicher, warum dies wieder in meiner Warteschlange auftauchte, aber als ich es diesmal las, war es offensichtlich ... ich bin mir nicht sicher, warum ich es vorher losgelassen habe. Ja, performBlockAndWait ist wieder am Start. In einer solchen verschachtelten Umgebung können Sie die synchrone Version in einem untergeordneten Kontext jedoch nicht aus einem übergeordneten Kontext heraus aufrufen. Die Benachrichtigung kann (in diesem Fall) aus dem übergeordneten Kontext gesendet werden, was zu einem Deadlock führen kann. Ich hoffe, das ist jedem klar, der mitkommt und dies später liest. Danke, David.
Jody Hagins
1
@ DavidWeiss Haben Sie MASTER -> MAIN -> BG ausprobiert? Ich interessiere mich für dieses Designmuster und hoffe zu wissen, ob es für Sie gut funktioniert. Danke dir.
Nonamelive
2
Das Problem mit MASTER -> MAIN -> BG-Muster ist, wenn Sie aus dem BG-Kontext abrufen, es wird auch aus MAIN abgerufen und das blockiert die Benutzeroberfläche und macht Ihre App nicht mehr ansprechbar
Rostyslav