Core Data Multithreading mit MagicalRecord

Nach einigen Abstürzen aufgrund von Zugriffen auf einen NSManagedObjectContext (Kontext) durch mehrere Threads haben wir nach einer Lösung gesucht, die unseren Code robuster macht. Das durch nutzlose Fehlermeldungen mühsame und zeitaufwändige Debugging solcher Fehler wollten wir unbedingt vermeiden.

Die Grundregeln bei dem Umgang mit Core Data Multithreading lesen sich einfach:

  • Jeder Kontext darf nur von dem Thread aus verwendet werden, auf dem er erstellt wurde.
  • NSManagedObjects (Objekte) sind fest an ihren Kontext gebunden. Nur ihre IDs dürfen Kontext- und damit Threadgrenzen passieren.

Unser Projekt enthält zahlreiche NSOperations (Operationen), die vom System auf einem Hintergrund-Thread ausgeführt werden.  Zur Einhaltung der oben genannten Regeln wird am Anfang jeder Operation ein neuer Kontext erstellt. Dieser Kontext wird nun über mehrere Hierarchien an Hilfsobjekte weitergereicht, um beispielsweise Objekte über NSFetchRequests zu holen.

Folgende Probleme ergeben sich dadurch:

  1. Die Signaturen vieler Methoden werden durch einen Kontext-Parameter aufgebläht.
  2. Eine Hilfsmethode, die intern den Standard-Kontext verwendet, wird von einem zweiten Entwickler in gutem Glauben aus einer Operation heraus aufgerufen.
  3. Die Übergabe von nil als Kontext an Hilfsmethoden ist zum Synonym für den Main-Thread-Kontext (Standard-Kontext) geworden. Dieser Automatismus kann aber für Fehler sorgen, wenn man vergisst den korrekten Kontext mitzugeben.

 

Magical Record

Offensichtlich gibt es einen engen Zusammenhang zwischen Kontext und Thread. Also warum wird der Kontext nicht einfach an den Thread gebunden und passend zum aktuellen Thread automatisch gewählt. Mit diesem Gedanken stößt man bei der Recherche auf Github schnell auf MagicalRecord.

Inspiriert von der Active Record Implementierung in Ruby on Rails bietet MagicalRecord zum einen Methoden zum Importieren von Daten und zum anderen die kompakte Erstellung von Fetch-Requests wie im folgenden Beispiel:

[User findFirstByAttribute:@“phone“ withValue:@“012“]

Ohne MagicalRecord wäre die Erstellung des gleichen Fetch-Requests mindestens fünf Zeilen lang.

Unterliegend verwendet MagicalRecord sogenannte Nested-Contexts, die Apple mit iOS 5 eingeführt hat. Dabei hat ein Kontext keinen NSPersistentStore zum Speichern, sondern einen Eltern-Kontext. Beim Aufruf von „save:“ werden die Änderungen in den Eltern-Kontext und nicht direkt in den NSPersistentStore geschrieben.

Das Einbinden von MagicalRecord gestaltete sich in unserem Fall unter iOS 6 erstaunlich einfach. Nach der Initialisierung des defaultContext mit Hilfe des eigenen NSPersistentStoreCoordinators, erstellt MagicalRecord alle weiteren Kontexte automatisch.

Diese bekommt man sehr einfach mit der statischen Methode contextForCurrentThread der NSManagedObjectContext-Klasse geliefert. Gibt es für den aktuellen Thread keinen Kontext, wird einer erstellt.

In unserem Projekt haben wir nun alle Zugriffe auf den Standard-Kontext durch den aktuellen Thread-Kontext ersetzt. Beim Zugriff vom Main-Thread entspricht dies dem defaultContext.

Der erstellte Thread-Kontext hat automatisch den defaultContext als Eltern-Kontext. Dies entspricht in etwa dem häufigen Fall (ohne Nested-Contexts), dass der Standard-Kontext den Thread-Kontext observiert und alle Änderungen abspeichert.

Es hat sich bewährt, am Anfang und am Ende jeder Operation den contextForCurrentThread mit reset zurück zu setzen, damit nicht versehentlich alte Änderungen im Kontext verbleiben, denn der contextForCurrentThread wird wiederverwendet, solange der Thread nicht vom System abgeräumt wurde.

Zum Speichern bietet MagicalRecord verschiedene Methoden an. Bei Umstellung von einem bestehenden Projekt ohne Nested-Contexts sollte man alle Kontext save: Methoden durch saveToPersistentStoreAndWait ersetzen, da bei beiden Methoden alle späteren Aufrufe davon ausgehen können, dass alle Daten in den NSPersistentStore gespeichert wurden.

Will man den aktuellen Thread nicht blockieren, so kann man auch asynchron mit saveToPersistentStoreWithCompletion: speichern. Aufwändiger wird es, wenn man bisher den Erfolg der save: Methode ausgewertet hat. In diesem Fall muss man alles was nach dem Speichern passieren soll manuell in den completionBlock von saveToPersistentStoreWithCompletion: verlegen.


[context saveToPersistentStoreWithCompletion:^(BOOL success, NSError *error) {
      if (success) ...
}];

 

Ergebnis iOS 6

Ein Abgleich des contextForCurrentThread mit unseren manuell durchgereichten Kontexten hat einige Fehler in unserem alten Code aufgespürt. Alle drei am Anfang aufgezählten Probleme sind aufgetreten und durch threadgebundene Kontexte gelöst worden.

Man muss natürlich weiterhin selbst darauf achten, dass man NSManagedObjects nicht über Threadgrenzen hinweg verwendet.

Will man explizit einen anderen Kontext verwenden, so kann man eine Version des persistierten Objekts durch die Methode inContext: im gewünschten Kontext bekommen.

[[Customer findFirst] inContext:differentContext]

 

Desaster auf iOS 5

Nach den Umstellungen lief die App unter iOS 6 problemlos. Ein Test auf iOS 5 war dagegen ein Desaster. Die App ist nicht einmal mehr gestartet.

Innerhalb von performFetch: eines NSFetchedResultsControllers ist es reproduzierbar zu Deadlocks gekommen. Sinnvolle Fehlermeldungen gab es nicht. Aber das war nicht das einzige Problem: Nested-Contexts auf iOS 5 führen zu Deadlocks, fehlerhaften Suchergebnissen, falschen Sortierungen und Abstürzen.

Eines wird daraus klar: Nested-Contexts wurden zwar unter iOS 5 eingeführt, aber sind so fehleranfällig, dass man sie erst ab iOS 6 in einer produktiven Umgebung einsetzen sollte. Apple hat an dieser Stelle leider geschlafen.

Dieser Blog liefert eine Beschreibung der oben genannten Probleme.

 

Magical Record ohne Nested-Contexts

Da wir MagicalRecords Fetching, Logging, ErrorHandling, etc. weiter verwenden wollten, haben wir alle Abhängigkeiten zu Nested-Contexts aus dem Framework entfernt und lassen den defaultContext andere Kontexte observieren.

Das Erstellen von Kontexten, Speichern und alle blockbasierten Methoden mussten dazu angepasst oder entfernt werden.

 

Automatische Tests

Beim automatischen Testen eines Projekts ersetzen wir vor jedem Test MagicalRecords defaultContext durch einen neuen Kontext der einen NSInMemoryStore verwendet. Da alle anderen Kontexte vom defaultContext abgeleitet werden und daher den selben NSPersistentStore verwenden, muss man ansonsten nichts weiter beachten.

 

Fazit

Trotz der wichtigen Lehre, dass Nested-Contexts unter iOS 5 unbrauchbar sind und des damit verbundenen zusätzlichen Aufwands hat sich die Arbeit gelohnt. Dank MagicalRecord und etwas Handarbeit ist der Code jetzt zentralisierter, kürzer und robuster geworden. Kontexte werden passend zum Thread automatisch gewählt, Fehler werden vermieden, Abstürze wurden beseitigt und als Entwickler kann man wieder ruhig schlafen.

Corporate Language: Mit konsequenter Tonalität zu den Sternen.

Per Anhalter durch die Galaxis - Lapke - Namics

Um in den unendlichen Weiten des Internets zu strahlen wie die Sonnen von Soulianis und Rahm, hebt qualitativ hochwertiger Content ein Unternehmen hervorragend von der Konkurrenz ab. Doch damit Content funktioniert, kommt es auf eine konstante, zielgruppen- und kanalspezifische Ansprache an. Hier helfen die Sprachklimazonen. Weiterlesen

jQuery Europe 2013, Tag 2

jquery-europe

Am 20 bis 22. Februar fand der europäische Ableger der jQuery Conference statt. Dieser Post besteht aus einer Zusammenfassung von Tag 2 der Konferenz. Die Zusammenfassung von Tag 1 wurde letzte Woche veröffentlicht. jQuery Mobile and Responsive Web Design – … Weiterlesen

jQuery Europe 2013, Tag 1

jqconfeu13

Am 20 bis 22. Februar fand der europäische Ableger der jQuery Conference statt. Als Austragungsort wurde das verschneite Wien gewählt; und mit dem Palais Liechtenstein ein würdiger Veranstaltungsort gefunden (siehe Bilder unten). Rund 400 Besucher trafen sich in Österreich um sich an einem … Weiterlesen

Direct-to-consumer – Chancen und Herausforderungen

Iterative Einführung von D2C-Kanälen

Sowohl im B2C- als auch B2B-Bereich stehen viele Unternehmen vor grossen Herausforderungen, wenn sie direkt mit Endkunden in Kontakt treten wollen. Ein häufiger Fall: Die eigenen Produkte und Services sollen zukünftig über einen eigenen Online-Shop vertrieben werden. Zentraler Absatzkanal für … Weiterlesen

Seite 1 von 812345...Letzte »