Правильная работа с SharedPreference в двух процессах

SQLite, Preferences, файлы, SD, Content Provider, XML, JSON
Ответить
Аватара пользователя
Mikhail_dev
Сообщения: 2386
Зарегистрирован: 09 янв 2012, 14:45
Откуда: Самара

Правильная работа с SharedPreference в двух процессах

Сообщение Mikhail_dev » 15 май 2013, 13:32

(Update 12.08.2013. Изменена логика работы с устройствами версии API>=11, всё таки флаг MODE_MULTI_PROCESS понадобился в полной мере).

Заметка касается работы с данными именно в одном приложении. (Если вам надо "расшарить" настройки на другие приложения, то вам наверное подойдет ContentProvider, но тема не про это)
1. Чего бы хотелось.
Думаю многие согласятся, что чем больше приложение, тем больше настроек оно может использовать в разных местах программы. Для этого нам может пригодиться единый класс управления настройками, будь он статический, либо singleton. Отлично, давайте возьмем singleton, будем обновлять настройки, после чего через геттеры и сеттеры использовать значения. Есть одна проблема.
2. Подводные камни.
Dalvik машина запускает каждый процесс (не путать с потоком) в отдельной своей копии, и как следствие мы получаем не один объект синглтона, а два (если пользуемся настройками с двух процессов). Это очень плохо. И это нужно понимать. С потоками всё в порядке. В рамках одного процесса все его потоки используют один экземпляр (один объект синглтона).
Да, но причем тут собственно настройки, если они сохраняются на носитель сразу? Ведь без разницы должно быть, записываем на носитель и читаем с любого процесса тот файл, что с настройками. И да, это действительно работало. Если кому интересно, вот информация о том, когда это работает, а когда нет.
Кто не силён в английском, я поясню всю тяжесть проблемы при работе с двумя процессами.
- До версии 2.3 это прекрасно работало.
- Вресия 2.3 - не работает.
- Версия >2.3 - надо использовать флаг MODE_MULTI_PROCESS
На вопрос "почему?", скажу что Google скорее всего с версии 2.3 начал использовать кеширование в память настроек и не сразу сохранять настройки. А это, как я уже указал, в двух процессах разные объекты. В итоге в одном процессе меняем - в другом читаем старые значения.
Но во всей это истории я не понял гугла. Ну сделали они кеширование с API 9, но почему флаг MODE_MULTI_PROCESS появился только с 11? Они полностью проигнорировали 2.3 линейку, которая на март 2013 составляет 38 процентов от всех пользователей!
3. Как обойти.
А всё гениальное просто. Сделайте commit настройкам после изменения (смотреть второй кусок кода!). Вот тут нашел ответ на данный вопрос. Немного кода:
[syntax=java]
/* Класс - менеджер для работы с данными настроек */
public class Preferences {
private static final Context c = App.getInstance().getApplicationContext();
public static final String NAVI_ALTERNATIVE_ROUTES_KEY = c.getString(R.string.navi_alternative_routes_key);
public static final String NAVI_RECALCULATION_ROUTE_KEY = c.getString(R.string.navi_recalculation_route_key);
private SharedPreferences mPreferences;
private boolean useAlternateRoutes;
private int alternateRouteDistance;

private Preferences() { }

public static synchronized Preferences getInstance() {
if (instance==null) {
instance = new Preferences();
}
return instance;
}

/* Обновляет переменные класса в соответствии с выбранными настройками */
public synchronized void updatePreferences() {
if (Build.VERSION.SDK_INT < 11) {
this.mPreferences = PreferenceManager.getDefaultSharedPreferences(c);
} else {
this.mPreferences = c.getSharedPreferences(c.getPackageName() + "_preferences",Context.MODE_MULTI_PROCESS);
}
useAlternateRoutes = mPreferences.getBoolean(NAVI_ALTERNATIVE_ROUTES_KEY, false);
alternateRouteDistance = Integer.parseInt(mPreferences.getString(NAVI_RECALCULATION_ROUTE_KEY, "100"));
}

public int getAlternateRouteDistance() {
return alternateRouteDistance;
}

public boolean isUseAlternateRoutes() {
return useAlternateRoutes;
}
}
[/syntax]
Как-то так, урезанно, выглядит singleton у нас. Думаю тут пояснять нечего. Скажу лишь что названия ключей я беру с ресурсов. Это по желанию. Таким способом просто надо будет в дальнейшем менять только ресурс. Ах да, App класс - это вспомогательный класс, унаследованный от Application, через который мы получаем applicationContext. Вам решать, как получить доступ к ресурсам. Можете, как уже сказал, работать через обычные строки.
А теперь непосредственно к тому, где делать коммит настроек, потому как если это сделать в конце метода updatePreferences, то проблема не уходит.
[syntax=java]
public class SettingsActivity extends PreferenceActivity implements OnSharedPreferenceChangeListener {

@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
addPreferencesFromResource(R.xml.preferences); //ссылка на ваши настройки
}

@Override
protected void onResume() {
super.onResume();
getPreferenceScreen().getSharedPreferences().registerOnSharedPreferenceChangeListener(this);
}

@Override
protected void onPause() {
super.onPause();
getPreferenceScreen().getSharedPreferences().unregisterOnSharedPreferenceChangeListener(this);
}


/**
* Раcсылает Broadcast, уведомляющий о изменении настроек.
* Содержит в себе ключ, который изменился
*/
@Override
public void onSharedPreferenceChanged(SharedPreferences sharedPreferences, String key) {
//тут берем измененный sharedPreference и делаем этим настройкам коммит
sharedPreferences.edit().commit();
//а после бросаем бродкаст с уведомлением о том, что настройки изменились.
Intent intent = new Intent("settings have changed");
intent.putExtra("key", key);
sendBroadcast(intent);
}
}
[/syntax]
Создаем новую активность, которая наследуется от PreferenceActivity и регистрирует слушатель OnSharedPreferenceChangeListener. В метод onSharedPreferenceChanged будет передаваться тот самый объект настроек, который изменился. Берем его и делаем commit, после чего бросаем Broadcast с уведомлением о том, что настройки изменились. Ну а после, как поймали Broadcast, делаем Preferences.getInstance().updatePreferences();
Последний раз редактировалось Mikhail_dev 12 авг 2013, 22:19, всего редактировалось 3 раза.

Аватара пользователя
klblk
Сообщения: 1097
Зарегистрирован: 18 окт 2012, 11:17
Откуда: г. Красноярск

Re: Правильная работа с SharedPreference в двух процессах

Сообщение klblk » 02 июл 2013, 05:14

Хорошая статья.
Единственное в тексте говориться о методе updateSettings() (причем в одном случае "S" заглавная, а в другом прописная), в то время как в классе Preferences метод называется updatePreferences(), и еще объявления instance не вижу.

Аватара пользователя
Mikhail_dev
Сообщения: 2386
Зарегистрирован: 09 янв 2012, 14:45
Откуда: Самара

Re: Правильная работа с SharedPreference в двух процессах

Сообщение Mikhail_dev » 02 июл 2013, 07:24

Благодарю за указанные ошибки. Действительно, updatePreferences(), а не updateSettings().

Аватара пользователя
Mikhail_dev
Сообщения: 2386
Зарегистрирован: 09 янв 2012, 14:45
Откуда: Самара

Re: Правильная работа с SharedPreference в двух процессах

Сообщение Mikhail_dev » 12 авг 2013, 22:26

Статья обновлена. С третьего андроида всё таки необходимо использовать флаг MODE_MULTI_PROCESS, поэтому немного изменен код.

w201
Сообщения: 9
Зарегистрирован: 08 дек 2013, 12:43

Re: Правильная работа с SharedPreference в двух процессах

Сообщение w201 » 08 дек 2013, 23:57

Кстати было бы неплохо отметить, что singleton вещь практически не реализуемая в андроиде. Единственный возможный вариант - через Application.
Вот тут хорошо написано.

Аватара пользователя
Mikhail_dev
Сообщения: 2386
Зарегистрирован: 09 янв 2012, 14:45
Откуда: Самара

Re: Правильная работа с SharedPreference в двух процессах

Сообщение Mikhail_dev » 10 дек 2013, 19:49

Application тоже не может быть сингтоном в полной мере. У нас в проекте два процесса, что влечет за собой два объекта Application, и да, это действительно проблема...

Ответить