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];
}
Antworten:
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:
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 ...
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 ...
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!
quelle