74

SharedPreferencesAPIを使用して文字列のセットを保存しようとしています。

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番目以降の要素をどのように保存して、失われないようにすることができますか?

4

6 に答える 6

169

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

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

それは問題ありませんが、保存しようとすると問題が発生します。Androidは、保存しようとしている変更されたHashSetを、にSharedPreferences.Editor.putStringSet保存されている現在のHashSetと比較しますSharedPreference。どちらも同じオブジェクトです!!!

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

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

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

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

于 2012-12-25T23:51:33.137 に答える
13

This behaviour is documented so it is by design:

from getStringSet:

"Note that you must not modify the set instance returned by this call. The consistency of the stored data is not guaranteed if you do, nor is your ability to modify the instance at all."

And it seems quite reasonable especially if it is documented in the API, otherwise this API would have to make copy on each access. So the reason for this design was probably performance. I suppose they should make this function return result wrapped in unmodifiable class instance, but this once again requires allocation.

于 2012-12-26T08:07:41.247 に答える
5

Was searching for a solution for the same issue, resolved it by:

1) Retrieve the existing set from the shared preferences

2) Make a copy of it

3) Update the copy

4) Save the copy

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

Why

于 2016-11-18T01:15:36.293 に答える
5

Source Code Explanation

While the other good answers on here have correctly pointed out that this potential issue is documented in SharedPreferences.getStringSet(), basically "Don't modify the returned Set because the behavior isn't guaranteed", I'd like to actually contribute the source code that causes this problem/behavior for anyone that wants to dive deeper.

Taking a look at SharedPreferencesImpl (source code as of Android Pie) we can see that in SharedPreferencesImpl.commitToMemory() there is a comparison that occurs between the original value (a Set<String> in our case) and the newly modified value:

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

So basically what's happening here is that when you try to write your changes to file this code will loop through your modified/added key/value pairs and check if they already exist, and will only write them to file if they don't or are different from the existing value that was read into memory.

The key line to pay attention to here is if (existingValue != null && existingValue.equals(v)). You're new value will only be written to disk if existingValue is null (doesn't already exist) or if existingValue's contents are different from the new value's contents.

This the the crux of the issue. existingValue is read from memory. The SharedPreferences file that you are trying to modify is read into memory and stored as Map<String, Object> mMap; (later copied into mapToWriteToDisk each time you try to write to file). When you call getStringSet() you get back a Set from this in-memory Map. If you then add a value to this same Set instance, you are modifying the in-memory Map. Then when you call editor.putStringSet() and try to commit, commitToMemory() gets executed, and the comparison line tries to compare your newly modified value, v, to existingValue which is basically the same in-memory Set as the one you've just modified. The object instances are different, because the Sets have been copied in various places, but the contents are identical.

So you're trying to compare your new data to your old data, but you've already unintentionally updated your old data by directly modifying that Set instance. Thus your new data will not be written to file.

But why are the values stored initially but disappear after the app is killed?

As the OP stated, it seems as if the values are stored while you're testing the app, but then the new values disappear after you kill the app process and restart it. This is because while the app is running and you're adding values, you're still adding the values to the in-memory Set structure, and when you call getStringSet() you're getting back this same in-memory Set. All your values are there and it looks like it's working. But after you kill the app, this in-memory structure is destroyed along with all the new values since they were never written to file.

Solution

As others have stated, just avoid modifying the in-memory structure, because you're basically causing a side-effect. So when you call getStringSet() and want to reuse the contents as a starting point, just copy the contents into a different Set instance instead of directly modifying it: new HashSet<>(getPrefs().getStringSet()). Now when the comparison happens, the in-memory existingValue will actually be different from your modified value v.

于 2019-06-12T01:14:12.013 に答える
2

I tried all the above answers none worked for me. So I did the following steps

  1. before adding new element to the list of old shared pref, make a copy of it
  2. call a method with the above copy as a param to that method.
  3. inside that method clear the shared pref which are holding that values.
  4. add the values present in copy to the cleared shared preference it will treat it as new.

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

I was facing issue with the values not being persistent, if i reopen the app after cleaning the app from background only first element added to the list was shown.

于 2017-09-23T10:27:46.433 に答える
1

Just as a note, Shared Preferences can't just be overwritten. If you have assigned a value to it, you have to remove it first by the method remove(KEY) and then commit() to destroy the key. Then you can assign a new value to it.

https://developer.android.com/reference/android/content/SharedPreferences.html#getStringSet(java.lang.String,%20java.util.Set%3Cjava.lang.String%3E)

于 2021-06-24T05:34:54.617 に答える