【问题标题】:Misbehavior when trying to store a string set using SharedPreferences尝试使用 SharedPreferences 存储字符串集时的不当行为
【发布时间】:2012-12-25 23:51:33
【问题描述】:

我正在尝试使用 SharedPreferences API 存储一组字符串。

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

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

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

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

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

【问题讨论】:

  • 代码不应该调用 editor.putStringSet() 吗?
  • @bluehallu 超级旧评论,但我修复了该行,因为 OP 似乎已经放弃了它。

标签: android android-preferences


【解决方案1】:

这个“问题”记录在SharedPreferences.getStringSet

SharedPreferences.getStringSet 返回存储的 HashSet 对象的引用 在SharedPreferences 内。当您向该对象添加元素时,它们实际上是添加到 SharedPreferences 中的。

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

一种可能的解决方案是复制SharedPreferences 对象返回的Set&lt;String&gt;

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

这使得s 成为一个不同的对象,添加到s 的字符串将不会添加到存储在SharedPreferences 中的集合中。

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

【讨论】:

  • 我已经 +1 了,但您能否提供一个链接来支持您的声明:“Android 将您尝试使用 SharedPreferences.Editor.putStringSet 保存的修改后的 HashSet 与当前的进行比较存储在 SharedPreference"
  • 第二个选项(保存计数器)对我来说效果很好。谢谢!
  • 花了 9 个小时盯着我的代码。非常感谢。
  • @Mr_and_Mrs_D 和任何感兴趣的人我已经添加了源代码解释答案:stackoverflow.com/a/56553439/708906
【解决方案2】:

此行为已记录在案,因此是设计使然:

来自 getStringSet:

“请注意,您不得修改此调用返回的集合实例。如果您这样做,则无法保证存储数据的一致性,您也无法修改该实例。”

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

【讨论】:

  • 今天下午我编辑了自己的答案。我意识到它有正确的文档记录,但是当您多次编写代码时,您不会像那样查看文档。这是保留此问题的唯一原因,恕我直言,因为它是一个实用程序类,这种解释可以帮助任何人。也感谢您的澄清
【解决方案3】:

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

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

Why

【讨论】:

    【解决方案4】:

    源码说明

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

    查看SharedPreferencesImpl(Android Pie 的源代码),我们可以看到在SharedPreferencesImpl.commitToMemory() 中,原始值(在我们的例子中为Set&lt;String&gt;)和新修改的值之间发生了比较:

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

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

    这里要注意的关键行是if (existingValue != null &amp;&amp; existingValue.equals(v))。只有当existingValuenull(尚不存在)或existingValue 的内容与新值的内容不同时,您的新值才会写入磁盘。

    这是问题的症结所在。 existingValue 是从内存中读取的。您尝试修改的 SharedPreferences 文件被读入内存并存储为Map&lt;String, Object&gt; mMap;(以后每次尝试写入文件时复制到mapToWriteToDisk)。当你调用getStringSet() 时,你会从这个内存映射中得到一个Set。如果您随后将值添加到同一 Set 实例,则您正在修改 in-memory 映射。然后,当您调用 editor.putStringSet() 并尝试提交时,commitToMemory() 会被执行,并且比较行会尝试将您新修改的值 vexistingValue 进行比较,这与内存中的 Set 基本相同你刚刚修改的那个。对象实例不同,因为Sets被复制到各个地方,但内容相同。

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

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

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

    解决方案

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

    【讨论】:

    • 基本上它不会起作用,因为 Set 是一个引用类型,而 SharedPreferences 的实现只比较引用而不是内容。如果您更改内容,引用不会更改。
    【解决方案5】:

    我尝试了上述所有答案,但都不适合我。 所以我做了以下步骤

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

    我遇到了值不持久的问题,如果我在从后台清理应用程序后重新打开应用程序,则只会显示添加到列表中的第一个元素。

    【讨论】:

      【解决方案6】:

      请注意,共享首选项不能被直接覆盖。 如果你已经给它赋值了,你必须先用remove(KEY)方法删除它,然后commit()来销毁密钥。 然后你可以给它分配一个新的值。

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

      【讨论】:

        猜你喜欢
        • 1970-01-01
        • 2014-12-25
        • 1970-01-01
        • 2015-09-10
        • 1970-01-01
        • 2011-12-17
        • 2018-10-06
        • 2017-09-04
        相关资源
        最近更新 更多