web-dev-qa-db-ja.com

SharedPreferencesを使用して文字列セットを保存しようとするときの誤動作

SharedPreferences AP​​I を使用して一連の文字列を保存しようとしています。

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

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

上記のコードを初めて実行すると、sがデフォルト値(作成されたばかりの最後の空のHashSet)に設定され、問題なく保存されます。

このコードを2回目に実行すると、最初の要素が追加されたsオブジェクトが返されます。要素を追加できます。プログラムの実行中にSharedPreferencesに保存されているようですが、プログラムが強制終了されると、SharedPreferencesが永続ストレージから再度読み取られ、新しい値が失われた。

2番目以降の要素を失わないように保存するにはどうすればよいですか?

62
JoseLSegura

この「問題」は SharedPreferences.getStringSet で文書化されています。

SharedPreferences.getStringSetは、SharedPreferences内に格納されているHashSetオブジェクトの参照を返します。このオブジェクトに要素を追加すると、実際にはSharedPreferences内に追加されます。

それは大丈夫ですが、それを保存しようとすると問題が発生します:Android SharedPreferences.Editor.putStringSetを使用して保存しようとしている変更されたHashSetとSharedPreference、および両方が同じオブジェクトです!!!

可能な解決策は、SharedPreferencesオブジェクトによって返されるSet<String>のコピーを作成することです。

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

これにより、sは別のオブジェクトになり、sに追加された文字列は、SharedPreferences内に格納されているセットに追加されません。

動作する他の回避策は、同じSharedPreferences.Editorトランザクションを使用して別のより単純な設定(整数またはブール値など)を保存することです。必要なのは、保存された値が各トランザクションで異なることを強制することです(たとえば、文字列セットのサイズを保存できます)。

141
JoseLSegura

この動作は文書化されているため、仕様によるものです。

getStringSetから:

「この呼び出しによって返されるセットインスタンスを変更しないでください。保存したデータの一貫性は保証されません。また、インスタンスを変更する能力もまったく保証されません。」

また、特にAPIで文書化されている場合は非常に合理的です。そうでない場合、このAPIはアクセスごとにコピーを作成する必要があります。したがって、この設計の理由はおそらくパフォーマンスにあります。この関数は、変更不可能なクラスインスタンスにラップされた結果を返すようにする必要がありますが、これには再度割り当てが必要です。

13
marcinj

同じ問題の解決策を探していましたが、次の方法で解決しました。

1)共有設定から既存のセットを取得する

2)コピーを作成する

3)コピーを更新する

4)コピーを保存する

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();

理由

4
s-hunter

上記の回答をすべて試してみましたが、私にはうまくいきませんでした。そこで、次の手順を実行しました

  1. 古い共有設定のリストに新しい要素を追加する前に、そのコピーを作成します
  2. 上記のコピーをそのメソッドのパラメーターとして使用してメソッドを呼び出します。
  3. そのメソッド内で、その値を保持している共有設定をクリアします。
  4. コピーに存在する値を、新しいものとして扱うクリアされた共有設定に追加します。

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

バックグラウンドからアプリを削除した後にアプリを再度開くと、リストに追加された最初の要素のみが表示された場合、値が永続的ではないという問題に直面していました。

0
Swapnil

ソースコードの説明

ここでの他の良い答えは、この潜在的な問題が SharedPreferences.getStringSet() で文書化されていることを正しく指摘していますが、基本的に「動作が保証されていないため、返されたSetを変更しないでください」、より深く掘り下げたい人のために、この問題/行動を引き起こすソースコードを実際に提供したいと思います。

SharedPreferencesImpl (Android Pie)時点のソースコード)を見ると、SharedPreferencesImpl.commitToMemory()で元の値( Set<String>この場合)および新しく変更された値:

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);
    }

基本的にここで起こっているのは、ファイルに変更を書き込もうとすると、このコードは変更/追加されたキー/値のペアをループし、それらが既に存在するかどうかを確認し、存在しないか存在する場合にのみファイルに書き込みますメモリに読み込まれた既存の値とは異なります。

ここで注意を払うべき重要な行はif (existingValue != null && existingValue.equals(v))です。 existingValuenullである(まだ存在しない)場合、またはexistingValueの内容が新しい値の内容と異なる場合にのみ、新しい値がディスクに書き込まれます。

これが問題の核心です。existingValueはメモリから読み取られます。変更しようとしているSharedPreferencesファイルはメモリに読み込まれ、Map<String, Object> mMap;として保存されます(ファイルに書き込むたびにmapToWriteToDiskにコピーされます)。 getStringSet()を呼び出すと、このメモリ内マップからSetが返されます。次に、この同じSetインスタンスに値を追加すると、in-memoryマップが変更されます。次に、editor.putStringSet()を呼び出してコミットしようとすると、commitToMemory()が実行され、比較行は新しく変更された値vを、基本的にメモリ内のexistingValueと同じSetと比較しようとします修正したばかりです。 Setsはさまざまな場所にコピーされているため、オブジェクトインスタンスは異なりますが、内容は同じです。

そのため、新しいデータを古いデータと比較しようとしていますが、Setインスタンスを直接変更することで、古いデータを意図せずに更新しています。したがって、新しいデータはファイルに書き込まれません。

しかし、なぜ値は最初に保存されているのに、アプリが終了すると消えてしまうのでしょうか?

OPが述べたように、アプリのテスト中に値が保存されているように見えますが、アプリプロセスを強制終了して再起動すると、新しい値は消えます。これは、アプリの実行中に値を追加しているときに、stillがメモリ内のSet構造体に値を追加し、 getStringSet()を呼び出すと、これと同じインメモリSetが返されます。すべての価値があり、機能しているように見えます。ただし、アプリを強制終了すると、このメモリ内構造は、ファイルに書き込まれなかったため、すべての新しい値とともに破棄されます。

解決

他の人が述べたように、メモリ内の構造を変更することは避けてください。基本的に副作用を引き起こすからです。したがって、getStringSet()を呼び出して、コンテンツを開始点として再利用する場合は、コンテンツを直接変更するのではなく、別のSetインスタンスにコピーしてください:new HashSet<>(getPrefs().getStringSet())。これで比較が行われると、メモリ内のexistingValueは実際に変更された値vと異なります。

0
Tony Chan