SharedPreferences.onSharedPreferenceChangeListener не викликається послідовно


267

Я реєструю прослуховувача зміни на зразок (наприклад, в onCreate()основному):

SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(this);

prefs.registerOnSharedPreferenceChangeListener(
   new SharedPreferences.OnSharedPreferenceChangeListener() {
       public void onSharedPreferenceChanged(
         SharedPreferences prefs, String key) {

         System.out.println(key);
       }
});

Біда в тому, що слухача не завжди викликають. Він працює перший раз, коли налаштування змінюються, а потім його більше не викликають, поки я не видалюю та перевстановлюю додаток. Здається, жодна кількість перезапуску програми не виправить це.

Я знайшов нитку списку розсилки, яка повідомила про ту саму проблему, але ніхто насправді не відповів на нього. Що я роблю неправильно?

Відповіді:


612

Це підлий. SharedPreferences тримає слухачів у WeakHashMap. Це означає, що ви не можете використовувати анонімний внутрішній клас у якості слухача, оскільки він стане ціллю збору сміття, як тільки ви вийдете з поточної сфери. Спочатку він запрацює, але згодом збирається сміття, вивозиться із WeakHashMap та припиняє роботу.

Зберігайте посилання на слухача в полі вашого класу, і ви будете добре, якщо примірник класу не буде знищений.

тобто замість:

prefs.registerOnSharedPreferenceChangeListener(
  new SharedPreferences.OnSharedPreferenceChangeListener() {
  public void onSharedPreferenceChanged(SharedPreferences prefs, String key) {
    // Implementation
  }
});

зробити це:

// Use instance field for listener
// It will not be gc'd as long as this instance is kept referenced
listener = new SharedPreferences.OnSharedPreferenceChangeListener() {
  public void onSharedPreferenceChanged(SharedPreferences prefs, String key) {
    // Implementation
  }
};

prefs.registerOnSharedPreferenceChangeListener(listener);

Причина відмови від реєстрації у методі onDestroy виправляє проблему в тому, що для цього вам довелося зберегти слухача в полі, тому запобігаючи проблему. Це збереження слухача в полі, яке виправляє проблему, а не відреєстрація в програмі onDestroy.

UPDATE : У Android документи були оновлені з попередженнями про цю поведінку. Отже, дивна поведінка залишається. Але зараз це документально підтверджено.


20
Це мене вбивало, я думав, що втрачаю розум. Дякуємо, що опублікували це рішення!
Бред Хайн

10
Ця публікація ВЕЛИЧЕЗНАЙ, спасибі, це могло коштувати мені годин неможливої ​​налагодження!
Кевін Гаудін

Дивовижно, це саме те, що мені було потрібно. Гарне пояснення теж!
stealthcopter

Це мене вже покусало, і я не знав, що було до прочитання цієї публікації. Дякую! Бу, Android!
Голий

5
Чудова відповідь, дякую. Однозначно слід згадати в документах. code.google.com/p/android/isissue/detail?id=48589
Андрій Черних

16

ця прийнята відповідь нормальна, оскільки для мене це створює новий екземпляр щоразу, коли активність поновлюється

то як щодо збереження посилання на слухача в межах діяльності

OnSharedPreferenceChangeListener myPrefListner = new OnSharedPreferenceChangeListener(){
      public void onSharedPreferenceChanged(SharedPreferences prefs, String key) {
         // your stuff
      }
};

і увімкнути Резолюцію та Паузу

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



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

}

це буде дуже схоже на те, що ви робите, за винятком того, що ми підтримуємо чітку орієнтир.


Чому ви використовували super.onResume()раніше getPreferenceScreen()...?
Yousha Aleayoub

@YoushaAleayoub, читайте про android.app.supernotcalledexception, це вимагається при застосуванні Android.
Самуїл

Що ви маєте на увазі? використання super.onResume()потрібне АБО його використання ДО ПЕРЕД ПОТРІБНО getPreferenceScreen()? тому що я говорю про ПРАВЕ місце. cs.dartmouth.edu/~campbell/cs65/lecture05/lecture05.html
Yousha Aleayoub

Пам’ятаю, читав це тут developer.android.com/training/basics/activity-lifecycle/… , дивіться коментар у коді. але покласти це в хвіст логічно. але до цього часу я не стикався з жодними проблемами в цьому.
Самуїл

Велике спасибі, всюди , що я знайшов onResume () і OnPause () метод , який вони зареєстровані thisі не listener, це викликало помилку і я міг би вирішити мою проблему. До цих пір ці два способи є загальнодоступними, але не захищені
Ніколас

16

Оскільки це найдокладніша сторінка до теми, я хочу додати 50ct.

У мене виникла проблема, що OnSharedPreferenceChangeListener не викликався. Мої спільні параметри отримуються на початку основної діяльності:

prefs = PreferenceManager.getDefaultSharedPreferences(this);

Мій код PreferenceActivity короткий і не робить нічого, крім показу налаштувань:

public class Preferences extends PreferenceActivity {
    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        // load the XML preferences file
        addPreferencesFromResource(R.xml.preferences);
    }
}

Кожен раз, коли натискається кнопка меню, я створюю PreferenceActivity з основної діяльності:

@Override
public boolean onPrepareOptionsMenu(Menu menu) {
    super.onCreateOptionsMenu(menu);
    //start Preference activity to show preferences on screen
    startActivity(new Intent(this, Preferences.class));
    //hook into sharedPreferences. THIS NEEDS TO BE DONE AFTER CREATING THE ACTIVITY!!!
    prefs.registerOnSharedPreferenceChangeListener(this);
    return false;
}

Зауважте, що реєстрацію OnSharedPreferenceChangeListener потрібно здійснити ПІСЛЯ створення цього параметра PreferenceActivity, інакше обробник в основній діяльності не буде викликаний !!! Мені знадобилося трохи солодкого часу, щоб зрозуміти, що ...


9

Прийнята відповідь створюється SharedPreferenceChangeListenerщоразу, коли onResumeвикликається. @Samuel вирішує це, входячиSharedPreferenceListener в клас активності. Але є третє і простіше рішення, яке Google також використовує в цій кодовій лабораторії . Зробіть свій клас активності реалізацією OnSharedPreferenceChangeListenerінтерфейсу та перекриттям onSharedPreferenceChangedв діяльності, ефективно роблячи саму діяльність a SharedPreferenceListener.

public class MainActivity extends Activity implements SharedPreferences.OnSharedPreferenceChangeListener {

    @Override
    public void onSharedPreferenceChanged(SharedPreferences sharedPreferences, String s) {

    }

    @Override
    protected void onStart() {
        super.onStart();
        PreferenceManager.getDefaultSharedPreferences(this)
                .registerOnSharedPreferenceChangeListener(this);
    }

    @Override
    protected void onStop() {
        super.onStop();
        PreferenceManager.getDefaultSharedPreferences(this)
                .unregisterOnSharedPreferenceChangeListener(this);
    }
}

1
саме так і повинно бути. реалізувати інтерфейс, зареєструватися в onStart та скасувати реєстрацію в onStop.
Junaed

2

Код Котліна для реєстрації SharedPreferenceChangeListener він виявляє, коли зміни будуть відбуватися на збереженому ключі:

  PreferenceManager.getDefaultSharedPreferences(this)
        .registerOnSharedPreferenceChangeListener { sharedPreferences, key ->
            if(key=="language") {
                //Do Something 
            }
        }

ви можете помістити цей код у OnStart () або десь інше .. * Вважайте, що ви повинні використовувати

 if(key=="YourKey")

або ваші коди всередині блоку "// Зроби щось" будуть запускатися неправильно для кожної зміни, яка відбуватиметься в будь-якому іншому ключі в sharedPreferences


1

Тож я не знаю, чи це справді комусь би допомогло, але це вирішило мою проблему. Навіть незважаючи на те, що я реалізував те, OnSharedPreferenceChangeListenerяк сказано у прийнятій відповіді . І все-таки у мене була невідповідність тому, що слухався викликаний.

Я прийшов сюди, щоб зрозуміти, що Android через деякий час просто відправляє його на вивезення сміття. Отже, я переглянув свій код. На жаль, я не оголосив слухача Глобально, а натомість всередині onCreateView. І це було тому, що я слухав Android Studio, який говорив мені перетворити слухача на локальну змінну.


0

Має сенс, що слухачів зберігають у WeakHashMap. Тому що більшість часу розробники вважають за краще писати такий код.

PreferenceManager.getDefaultSharedPreferences(getApplicationContext()).registerOnSharedPreferenceChangeListener(
    new OnSharedPreferenceChangeListener() {
    @Override
    public void onSharedPreferenceChanged(
        SharedPreferences sharedPreferences, String key) {
        Log.i(LOGTAG, "testOnSharedPreferenceChangedWrong key =" + key);
    }
});

Це може здатися непоганим. Але якщо контейнер OnSharedPreferenceChangeListeners не був WeakHashMap, це було б дуже погано. Якщо б вищевказаний код був записаний у Activity. Оскільки ви використовуєте нестатичний (анонімний) внутрішній клас, який неявно містить посилання на екземпляр, що додається. Це призведе до витоку пам'яті.

Більше того, якщо ви зберігаєте слухача як поле, ви можете використати registerOnSharedPreferenceChangeListener на початку та викликати ungisterOnSharedPreferenceChangeListener в кінці. Але ви не можете отримати доступ до локальної змінної у способі поза її рамкою. Тож у вас просто є можливість зареєструватися, але немає шансу відреєстрацію слухача. Таким чином, використання WeakHashMap вирішить проблему. Це я рекомендую.

Якщо зробити екземпляр слухача статичним полем, це дозволить уникнути витоку пам'яті, викликаного нестатичним внутрішнім класом. Але оскільки слухачів може бути декілька, це повинно бути пов'язано з примірниками. Це зменшить витрати на обробку зворотного дзвінка onSharedPreferenceChanged .


-3

Під час читання даних, доступних для читання в Word, якими ділиться перший додаток, ми повинні

Замініть

getSharedPreferences("PREF_NAME", Context.MODE_PRIVATE);

з

getSharedPreferences("PREF_NAME", Context.MODE_MULTI_PROCESS);

у другому додатку, щоб отримати оновлене значення у другому додатку.

Але все одно це не працює ...


Android не підтримує доступ до SharedPreferences з декількох процесів. Це спричиняє проблеми одночасності, що може призвести до втрати всіх переваг. Також MODE_MULTI_PROCESS більше не підтримується.
Сем

@Sam цій відповіді 3 роки, будь ласка, тому не голосуйте за неї, якщо вона не працює для вас в останніх версіях Android. Час відповіді було написано, що це був найкращий підхід.
shridutt kothari

1
Ні, цей підхід ніколи не був безпечним для багатьох процесів, навіть коли ви писали цю відповідь.
Сем

Як стверджує @Sam, правильно, спільні префікси ніколи не були безпечними. Крім того, щоб shridutt kothari - якщо вам не сподобалось, скажімо, видаліть неправильну відповідь (це все одно не відповідає на питання ОП). Однак якщо ви все-таки хочете використовувати спільні префікси безпечним способом, вам потрібно створити над ним безпечну абстракцію, тобто ContentProvider, який є безпечним для процесів, і все ще дозволяє використовувати спільні налаштування як збережений механізм зберігання. , Я робив це раніше, і для невеликих наборів даних / налаштувань виконує sqlite з справедливою маржею.
Марк Кін

1
@MarkKeen До речі, я насправді намагався використовувати провайдери контенту для цього, і постачальники контенту, як правило, випадали випадковим чином у виробництві через помилки Android, тому в ці дні я просто використовую трансляції для синхронізації конфігурації з вторинними процесами за потребою.
Сем
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.