Fehlverhalten beim Versuch, einen String-Satz mit SharedPreferences zu speichern

71

Ich versuche, eine Reihe von Zeichenfolgen mithilfe der SharedPreferencesAPI zu speichern .

Set<String> s = sharedPrefs.getStringSet("key", new HashSet<String>());
s.add(new_element);

SharedPreferences.Editor editor = sharedPrefs.edit();
editor.putStringSet(s);
edit.commit()

Wenn ich den obigen Code zum ersten Mal ausführe, swird er auf den Standardwert gesetzt (das gerade erstellte Ende leer HashSet) und ohne Probleme gespeichert.

Beim zweiten und nächsten Ausführen dieses Codes wird ein sObjekt mit dem ersten hinzugefügten Element zurückgegeben. Ich kann das Element hinzufügen, und während der Programmausführung wird es anscheinend im gespeichert SharedPreferences, aber wenn das Programm beendet wird, gehen das SharedPreferencesLesen aus seinem dauerhaften Speicher erneut und die neueren Werte gehen verloren.

Wie können die zweite und die darauf folgenden Elemente gespeichert werden, damit sie nicht verloren gehen?

JoseLSegura
quelle
2
Sollte der Code nicht editor.putStringSet () aufrufen?
Bluehallu
@bluehallu super alter Kommentar, aber ich habe diese Zeile korrigiert, da es scheint, dass das OP sie aufgegeben hat.
Tony Chan

Antworten:

161

Dieses "Problem" ist am dokumentiert SharedPreferences.getStringSet.

Das SharedPreferences.getStringSetgibt eine Referenz des gespeicherten HashSet-Objekts im zurück SharedPreferences. Wenn Sie diesem Objekt Elemente hinzufügen, werden diese tatsächlich innerhalb des Objekts hinzugefügt SharedPreferences.

Das ist in Ordnung, aber das Problem tritt auf, wenn Sie versuchen, es zu speichern: Android vergleicht das modifizierte HashSet, das Sie speichern möchten, SharedPreferences.Editor.putStringSetmit dem aktuellen, das auf dem gespeichert ist SharedPreference, und beide sind dasselbe Objekt !!!

Eine mögliche Lösung besteht darin, eine Kopie des Set<String>vom SharedPreferencesObjekt zurückgegebenen Objekts zu erstellen:

Set<String> s = new HashSet<String>(sharedPrefs.getStringSet("key", new HashSet<String>()));

Dadurch wird sein anderes Objekt erstellt, und die hinzugefügten Zeichenfolgen swerden nicht zu der in der gespeicherten Gruppe hinzugefügt SharedPreferences.

Eine andere Problemumgehung besteht darin, dieselbe SharedPreferences.EditorTransaktion zum Speichern einer anderen einfacheren Einstellung (wie einer Ganzzahl oder eines Booleschen Werts) zu verwenden. Sie müssen lediglich erzwingen, dass die gespeicherten Werte bei jeder Transaktion unterschiedlich sind (z. B. können Sie die Zeichenfolge speichern eingestellte Größe).

JoseLSegura
quelle
6
Ich habe bereits +1, aber könnten Sie bitte einen Link zu der Quelle angeben, die Ihre Behauptung bestätigt: "Android vergleicht das modifizierte HashSet, das Sie mit SharedPreferences.Editor.putStringSet speichern möchten, mit dem aktuellen, das in der SharedPreference gespeichert ist"
Mr_and_Mrs_D
Die zweite Option (einen Zähler speichern) hat bei mir gut funktioniert. Vielen Dank!
MarionaDSR
1
Ich habe 9 Stunden damit verbracht, auf meinen Code zu starren. Danke vielmals.
AnupamChugh
@ Mr_and_Mrs_D und alle Interessierten Ich habe eine Antwort auf die Erklärung des Quellcodes hinzugefügt: stackoverflow.com/a/56553439/708906
Tony Chan
13

Dieses Verhalten ist so dokumentiert, dass es beabsichtigt ist:

von getStringSet:

"Beachten Sie, dass Sie die von diesem Aufruf zurückgegebene festgelegte Instanz nicht ändern dürfen. Die Konsistenz der gespeicherten Daten wird in diesem Fall nicht garantiert, und Sie können die Instanz auch nicht ändern."

Und es erscheint ziemlich vernünftig, insbesondere wenn es in der API dokumentiert ist, da diese API sonst bei jedem Zugriff eine Kopie erstellen müsste. Der Grund für dieses Design war also wahrscheinlich die Leistung. Ich nehme an, sie sollten dafür sorgen, dass diese Funktion ein Ergebnis zurückgibt, das in eine nicht modifizierbare Klasseninstanz eingeschlossen ist, aber dies erfordert erneut eine Zuordnung.

marcinj
quelle
2
Ich habe heute Nachmittag meine eigene Antwort bearbeitet. Ich habe festgestellt, dass es richtig dokumentiert ist, aber wenn Sie oft Code schreiben, schauen Sie in solchen Dingen nicht auf die Dokumentation. Dies ist der einzige Grund, diese Frage + Antwort beizubehalten, IMHO, da es sich um eine Utility-Klasse handelt und diese Art der Erklärung jedem helfen kann. Vielen Dank auch für Ihre Klarstellung
JoseLSegura
5

War auf der Suche nach einer Lösung für das gleiche Problem und löste es durch:

1) Rufen Sie den vorhandenen Satz aus den freigegebenen Einstellungen ab

2) Machen Sie eine Kopie davon

3) Aktualisieren Sie die Kopie

4) Speichern Sie die Kopie

SharedPreferences.Editor editor = sharedPrefs.edit();
Set<String> oldSet = sharedPrefs.getStringSet("key", new HashSet<String>());

//make a copy, update it and save it
Set<String> newStrSet = new HashSet<String>();    
newStrSet.add(new_element);
newStrSet.addAll(oldSet);

editor.putStringSet("key",newStrSet); edit.commit();

Warum

Rangierer
quelle
2

Ich habe alle oben genannten Antworten ausprobiert. Keine hat für mich funktioniert. Also habe ich die folgenden Schritte gemacht

  1. Erstellen Sie eine Kopie davon, bevor Sie der Liste der alten freigegebenen Einstellungen ein neues Element hinzufügen
  2. Rufen Sie eine Methode mit der obigen Kopie als Parameter für diese Methode auf.
  3. Löschen Sie innerhalb dieser Methode die gemeinsamen Einstellungen, die diese Werte enthalten.
  4. Fügen Sie die in der Kopie enthaltenen Werte zu der gelöschten gemeinsamen Einstellung hinzu, die als neu behandelt wird.

    public static void addCalcsToSharedPrefSet(Context ctx,Set<String> favoriteCalcList) {
    
    ctx.getSharedPreferences(FAV_PREFERENCES, 0).edit().clear().commit();
    
    SharedPreferences sharedpreferences = ctx.getSharedPreferences(FAV_PREFERENCES, Context.MODE_PRIVATE);
    SharedPreferences.Editor editor = sharedpreferences.edit();
    editor.putStringSet(FAV_CALC_NAME, favoriteCalcList);
    editor.apply(); }
    

Ich hatte ein Problem mit den Werten, die nicht dauerhaft sind. Wenn ich die App nach dem Bereinigen der App aus dem Hintergrund erneut öffne, wird nur das erste Element angezeigt, das der Liste hinzugefügt wurde.

Swapnil
quelle
2

Erklärung des Quellcodes

Während die anderen guten Antworten hier richtig darauf hingewiesen haben, dass dieses potenzielle Problem in SharedPreferences.getStringSet () dokumentiert ist, möchte ich im Grunde genommen "Das zurückgegebene Set nicht ändern, da das Verhalten nicht garantiert ist" Quellcode, der dieses Problem / Verhalten für jeden verursacht, der tiefer eintauchen möchte.

Ein Blick auf SharedPreferencesImpl (Quellcode ab Android Pie) zeigt, dass SharedPreferencesImpl.commitToMemory()ein Vergleich zwischen dem ursprünglichen Wert ( Set<String>in unserem Fall a) und dem neu geänderten Wert erfolgt:

private MemoryCommitResult commitToMemory() {
    // ... other code

    // mModified is a Map of all the key/values added through the various put*() methods.
    for (Map.Entry<String, Object> e : mModified.entrySet()) {
        String k = e.getKey();
        Object v = e.getValue();
        // ... other code

        // mapToWriteToDisk is a copy of the in-memory Map of our SharedPreference file's
        // key/value pairs.
        if (mapToWriteToDisk.containsKey(k)) {
            Object existingValue = mapToWriteToDisk.get(k);
            if (existingValue != null && existingValue.equals(v)) {
                continue;
            }
        }
        mapToWriteToDisk.put(k, v);
    }

Wenn Sie also versuchen, Ihre Änderungen in die Datei zu schreiben, durchläuft dieser Code im Grunde genommen Ihre geänderten / hinzugefügten Schlüssel / Wert-Paare und prüft, ob sie bereits vorhanden sind, und schreibt sie nur in die Datei, wenn dies nicht der Fall ist oder ist unterscheidet sich von dem vorhandenen Wert, der in den Speicher eingelesen wurde.

Die wichtigste Zeile, auf die Sie hier achten sollten, ist if (existingValue != null && existingValue.equals(v)). Ihr neuer Wert wird nur dann auf die Festplatte geschrieben, wenn er vorhanden existingValueist null(noch nicht vorhanden ist) oder wenn sich existingValueder Inhalt vom Inhalt des neuen Werts unterscheidet.

Dies ist der Kern des Problems. existingValuewird aus dem Speicher gelesen. Die SharedPreferences-Datei, die Sie ändern möchten, wird in den Speicher eingelesen und als gespeichert Map<String, Object> mMap;(später bei mapToWriteToDiskjedem Versuch, in eine Datei zu schreiben, kopiert ). Wenn Sie anrufen, erhalten getStringSet()Sie eine Setvon dieser In-Memory-Map zurück. Wenn Sie dann derselben SetInstanz einen Wert hinzufügen , ändern Sie die speicherinterne Zuordnung. Wenn Sie dann aufrufen editor.putStringSet()und versuchen, ein Commit durchzuführen, commitToMemory()wird es ausgeführt, und die Vergleichszeile versucht, Ihren neu geänderten Wert zu vergleichen v, existingValueder im Grunde derselbe im Speicher ist Set, den Sie gerade geändert haben. Die Objektinstanzen sind unterschiedlich, da die Sets an verschiedenen Stellen kopiert wurden, der Inhalt jedoch identisch ist.

Also Sie versuchen , Ihre neue Daten zu Ihren alten Daten zu vergleichen, aber Sie haben bereits unbeabsichtigt Ihre alten Daten aktualisiert , indem Sie direkt , dass die Modifizierung SetInstanz. Somit werden Ihre neuen Daten nicht in die Datei geschrieben.

Aber warum werden die Werte anfänglich gespeichert, verschwinden aber, nachdem die App beendet wurde?

Wie im OP angegeben, scheinen die Werte während des Testens der App gespeichert zu sein, aber die neuen Werte verschwinden, nachdem Sie den App-Prozess abgebrochen und neu gestartet haben. Dies liegt daran , während die App laufen und Sie hinzufügen Werte, du bist immer noch die Werte in die In-Memory - Zugabe SetStruktur, und wenn Sie anrufen getStringSet()werden Sie das gleiche im Speicher immer wieder Set. Alle Ihre Werte sind da und es sieht so aus, als ob es funktioniert. Nachdem Sie die App beendet haben, wird diese speicherinterne Struktur zusammen mit allen neuen Werten zerstört, da sie nie in eine Datei geschrieben wurden.

Lösung

Wie andere bereits gesagt haben, vermeiden Sie es einfach, die In-Memory-Struktur zu ändern, da Sie im Grunde genommen einen Nebeneffekt verursachen. Wenn Sie also getStringSet()den Inhalt aufrufen und als Ausgangspunkt wiederverwenden möchten, kopieren Sie ihn einfach in eine andere SetInstanz, anstatt ihn direkt zu ändern : new HashSet<>(getPrefs().getStringSet()). Wenn der Vergleich nun stattfindet, unterscheidet sich der In-Memory- existingValueWert tatsächlich von Ihrem geänderten Wert v.

Tony Chan
quelle