尝试使用SharedPreferences存储字符串集时出现错误行为

Jos*_*ura 62 android android-preferences

我正在尝试使用SharedPreferencesAPI存储一组字符串.

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

SharedPreferences.Editor editor = sharedPrefs.edit();
editor.putStringSet(s);
edit.commit()
Run Code Online (Sandbox Code Playgroud)

我第一次执行上面的代码时,s设置为默认值(刚刚创建的结束为空HashSet)并且存储没有问题.

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

第二个以及之后的元素如何存储以便它们不会丢失?

Jos*_*ura 142

这个"问题"记录在案SharedPreferences.getStringSet.

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

这是确定的,但是当你尝试存储它的问题就来了:安卓您尝试使用保存修改后的HashSet的比较SharedPreferences.Editor.putStringSet与存储在当前的一个SharedPreference,又都是同一个对象!

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

Set<String> s = new HashSet<String>(sharedPrefs.getStringSet("key", new HashSet<String>()));
Run Code Online (Sandbox Code Playgroud)

这会产生s一个不同的对象,添加的字符串s将不会被添加到存储在其中的集合中SharedPreferences.

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

  • 我已经+1了,但是请你提供一个指向备份你声称的源的链接:"Android将你尝试使用SharedPreferences.Editor.putStringSet保存的修改后的HashSet与存储在SharedPreference上的当前值进行比较" (5认同)
  • 花了 9 个小时盯着我的代码。万分感谢。 (2认同)

mar*_*inj 13

记录此行为是因为它是设计的:

来自getStringSet:

"请注意,您不能修改此调用返回的set实例.如果您这样做,则无法保证存储数据的一致性,也无法完全修改实例."

它似乎很合理,特别是如果它在API中记录,否则此API必须在每次访问时进行复制.所以这种设计的原因可能就是性能.我想他们应该使这个函数返回包含在不可修改的类实例中的结果,但这又需要分配.

  • 我今天下午编辑了自己的答案.我意识到它已被正确记录,但是当您多次编写代码时,您不会在这样的事情中查看文档.这是保持这个问题+答案的唯一理由,恕我直言,因为它是一个实用工具类,这种解释可以帮助任何人.谢谢你的澄清 (2认同)

s-h*_*ter 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();
Run Code Online (Sandbox Code Playgroud)

为什么


Ton*_*han 5

源代码说明

虽然此处的其他好答案已正确指出此潜在问题已记录在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);
    }
Run Code Online (Sandbox Code Playgroud)

所以基本上这里发生的事情是,当您尝试将更改写入文件时,此代码将遍历您修改/添加的键/值对并检查它们是否已经存在,并且只有在它们不存在或存在时才会将它们写入文件与读入内存的现有值不同。

这里要注意的重点是if (existingValue != null && existingValue.equals(v))。如果existingValuenull(尚不存在)或existingValue的内容与新值的内容不同,则您的新值只会写入磁盘。

这是问题的症结所在。 existingValue是从内存中读取的。您尝试修改的 SharedPreferences 文件被读入内存并存储为Map<String, Object> mMap;(以后mapToWriteToDisk每次尝试写入文件时都会复制到该文件中)。当你打电话时,getStringSet()你会Set从这个内存映射中得到一个。如果您随后向同一个Set实例添加一个值,则您正在修改内存中的Map。然后当你调用editor.putStringSet(),并尝试提交,commitToMemory()被执行,和比较线尝试比较新修改的值,vexistingValue基本上是在内存中相同Set就像刚才修改的一个。对象实例不同,因为Sets已经被复制到了不同的地方,但是内容是一样的。

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

但是为什么最初存储的值会在应用程序被杀死后消失?

正如 OP 所说,似乎在您测试应用程序时存储了这些值,但是在您终止应用程序进程并重新启动它后,新值就会消失。这是因为当应用程序正在运行并且您正在添加值时,您仍在将值添加到内存Set结构中,并且当您调用时,getStringSet()您将返回相同的 in-memory Set。你所有的价值观都在那里,看起来它正在发挥作用。但是在你杀死应用程序后,这个内存结构与所有新值一起被销毁,因为它们从未被写入文件。

解决方案

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