Misbehavior при попытке сохранить набор строк с помощью SharedPreferences

Я пытаюсь сохранить набор строк с SharedPreferences API SharedPreferences.

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), и он сохраняется без проблем.

Во второй и следующий раз, когда я выполняю этот код, возвращается объект s с добавлением первого элемента. Я могу добавить элемент, и во время выполнения программы он, очевидно, сохраняется в SharedPreferences, но когда программа уничтожается, SharedPreferences снова считывает данные из своего постоянного хранилища, и новые значения теряются.

Как можно хранить второе и последующие элементы, чтобы они не потерялись?

Ответ 1

Эта "проблема" документирована на SharedPreferences.getStringSet.

SharedPreferences.getStringSet возвращает ссылку на сохраненный объект HashSet внутри SharedPreferences. Когда вы добавляете элементы к этому объекту, они фактически добавляются внутри SharedPreferences.

Это нормально, но проблема возникает, когда вы пытаетесь его сохранить: Android сравнивает измененный HashSet, который вы пытаетесь сохранить, используя SharedPreferences.Editor.putStringSet с текущим, хранящимся в SharedPreference, и оба являются одним и тем же объектом!!!

Возможным решением является создание копии Set<String>, возвращаемой объектом SharedPreferences:

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

Это делает s другим объектом, а строки, добавленные в s, не будут добавлены в набор, хранящийся внутри SharedPreferences.

Другим обходным решением, которое будет работать, является использование одной и той же транзакции SharedPreferences.Editor для хранения другого более простого предпочтения (например, целого или логического), единственное, что вам нужно, это заставить запомненное значение отличаться для каждой транзакции (например, вы можете сохранить размер набора строк).

Ответ 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();

Почему

Ответ 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(); }
    

Я столкнулся с проблемой, когда значения не были постоянными, если я снова открываю приложение после очистки приложения из фона, был показан только первый элемент, добавленный в список.

Ответ 5

Исходный код Объяснение

В то время как другие хорошие ответы здесь правильно указали, что эта потенциальная проблема задокументирована в ) rel="nofollow noreferrer">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)). Ваше новое значение будет записано на диск только в том случае, если existingValue равно null (еще не существует) или если existingValue отличается от содержимого нового значения.

Это суть вопроса. existingValue читается из памяти. Файл SharedPreferences, который вы пытаетесь изменить, считывается в память и сохраняется как Map<String, Object> mMap; (позже копируется в mapToWriteToDisk каждой попытке записи в файл). Когда вы вызываете getStringSet() вы получаете Set из этой карты в памяти. Если затем вы добавите значение к этому же экземпляру Set, вы модифицируете карту в памяти. Затем, когда вы вызываете editor.putStringSet() и пытаетесь выполнить коммит, commitToMemory(), и строка сравнения пытается сравнить ваше вновь измененное значение v с existingValue которое в основном совпадает с установленным в памяти Set который вы Мы только что изменили. Экземпляры объекта различны, потому что Set был скопирован в разных местах, но содержимое идентично.

Таким образом, вы пытаетесь сравнить свои новые данные со старыми, но вы уже непреднамеренно обновили свои старые данные, непосредственно изменив этот экземпляр Set. Таким образом, ваши новые данные не будут записаны в файл.

Но почему значения сохраняются изначально, но исчезают после закрытия приложения?

Как указывалось в OP, кажется, что значения сохраняются во время тестирования приложения, но затем новые значения исчезают после того, как вы завершаете процесс приложения и перезапускаете его. Это происходит потому, что, пока приложение работает и вы добавляете значения, вы все равно добавляете значения в структуру Set в памяти, и когда вы вызываете getStringSet() вы возвращаете тот же самый Set в памяти. Все ваши ценности есть, и похоже, что это работает. Но после того, как вы убьете приложение, эта структура в памяти уничтожается вместе со всеми новыми значениями, поскольку они никогда не записывались в файл.

Решение

Как уже говорили другие, просто избегайте изменения структуры в памяти, потому что вы в основном вызываете побочный эффект. Поэтому, когда вы вызываете getStringSet() и хотите повторно использовать содержимое в качестве отправной точки, просто скопируйте содержимое в другой экземпляр Set вместо прямой его модификации: new HashSet<>(getPrefs().getStringSet()). Теперь, когда сравнение происходит, в памяти existingValue фактически будет отличаться от модифицированного значения v.