尝试使用SharedPreferences存储字符串集时的行为不当


71

我正在尝试使用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设置为默认值(刚创建的结尾为empty HashSet),并且存储时没有问题。

第二次和下一次执行此代码时,将s返回一个对象,并添加第一个元素。我可以添加该元素,并且在程序执行期间,该元素显然存储在中SharedPreferences,但是当程序被杀死时,SharedPreferences将从其持久性存储中再次读取并丢失较新的值。

如何存储第二个以及之后的元素,以免丢失?


2
代码不应该调用editor.putStringSet()吗?
bluehallu's

@bluehallu超级旧评论,但由于似乎OP放弃了它,所以我固定了这一行。
托尼·陈

Answers:


161

该“问题”记录在上SharedPreferences.getStringSet

SharedPreferences.getStringSet返回内的所存储的HashSet对象的参考SharedPreferences。当您向此对象添加元素时,它们实际上是添加到的内部SharedPreferences

没关系,但是当您尝试存储它时就会出现问题:Android将您要保存使用的修改后的HashSetSharedPreferences.Editor.putStringSet与上存储的当前HashSet进行了比较SharedPreference,两者都是同一个对象!!!

一种可能的解决方案是制作Set<String>SharedPreferences对象返回的副本:

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

这就形成s了一个不同的对象,添加到的字符串s将不会添加到存储在中的集合中SharedPreferences

其他可行的解决方法是使用同一SharedPreferences.Editor事务存储另一个更简单的首选项(例如整数或布尔值),您唯一需要做的就是强制每个事务的存储值都不同(例如,您可以存储字符串设置尺寸)。


6
我已经+1,但能否请您提供一个链接以支持您的主张:“ Android将您尝试使用SharedPreferences.Editor.putStringSet尝试保存的修改后的HashSet与存储在SharedPreference上的当前HashSet进行了比较”
Mr_and_Mrs_D

第二个选项(保存计数器)对我来说效果很好。谢谢!
MarionaDSR

1
花了9个小时盯着我的这段代码。万分感谢。
AnupamChugh '18

@Mr_and_Mrs_D和任何有兴趣的人,我已经添加了源代码解释答案:stackoverflow.com/a/56553439/708906
Tony Chan

13

记录了此行为,因此是设计使然:

从getStringSet:

“请注意,您一定不能修改此调用返回的设置实例。如果这样做,则不能保证存储数据的一致性,也根本不能保证修改实例的能力。”

而且似乎很合理,特别是如果在API中对其进行了说明,否则此API将必须在每次访问时进行复制。因此,这种设计的原因可能是性能。我想他们应该使此函数返回的结果包装在不可修改的类实例中,但这又需要分配。


2
今天下午我编辑了自己的答案。我意识到它已经被正确地记录了下来,但是当您多次编写代码时,您不会像那样去看文档。这是保留此Ask + answer,恕我直言的唯一原因,因为它是实用程序类,这种解释可以帮助任何人。也感谢您的澄清
JoseLSegura

5

正在寻找相同问题的解决方案,并通过以下方式解决了该问题:

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

为什么


2

我尝试了以上所有答案,但没有一个适合我。所以我做了以下步骤

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

我面临的问题是值不是永久性的,如果从后台清理应用程序后重新打开应用程序,则仅显示添加到列表中的第一个元素。


2

源代码说明

虽然此处的其他好的答案已正确指出该潜在问题记录在SharedPreferences.getStringSet()中,但基本上是“不要修改返回的Set,因为不能保证行为”,但我想实际贡献对于任何想深入研究的人来说,都会导致此问题/行为的源代码。

看一下SharedPreferencesImpl(Android Pie的源代码),我们可以看到SharedPreferencesImpl.commitToMemory()原始值(Set<String>在我们的例子中为a)和新修改的值之间存在比较:

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从此内存映射中获取a 。如果随后将值添加到同一Set实例,则正在修改内存中的Map。然后当你调用editor.putStringSet(),并尝试提交,commitToMemory()被执行,和比较线尝试比较新修改的值,vexistingValue基本上是在内存中相同Set就像刚才修改的一个。对象实例是不同的,因为Set已经在不同的地方复制了,但是内容是相同的。

因此,您正在尝试将新数据与旧数据进行比较,但是您已经通过直接修改该Set实例无意中更新了旧数据。因此,您的新数据将不会写入文件。

但是,为什么这些值最初存储后在应用程序被终止后消失了?

如OP所述,似乎值是在测试应用程序时存储的,但是新值在终止应用程序进程并重新启动后会消失。这是因为在应用程序运行且您要添加值时,您仍在将这些值添加到内存Set结构中,而在调用时,getStringSet()您将获得相同的内存Set。您所有的值都在那里,并且看起来正在运行。但是在终止应用程序之后,此内存结构以及所有新值都将被销毁,因为它们从未写入文件中。

正如其他人所说,只是避免修改内存结构,因为这基本上是在引起副作用。因此,当您致电getStringSet()并希望重复使用内容作为起点时,只需将内容复制到另一个Set实例中,而不是直接修改它:new HashSet<>(getPrefs().getStringSet())。现在,当比较发生时,内存existingValue实际上将与修改后的值不同v

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.