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.

ОБНОВЛЕНИЕ : Документы Android были обновлены с предупреждениями об этом поведении. Таким образом, странное поведение остается. Но сейчас это задокументировано.


20
Это убивало меня, я думал, что схожу с ума. Спасибо за размещение этого решения!
Брэд Хейн

10
Этот пост ОГРОМНЫЙ, большое спасибо, это могло стоить мне часов невозможной отладки!
Кевин Гаудин

Круто, это как раз то, что мне было нужно. Хорошее объяснение тоже!
стелскоптер

Это уже укусило меня, и я не знал, что случилось, пока не прочитал этот пост. Спасибо! Бу, Android!
Nakedible

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

16

этот принятый ответ в порядке, так как для меня это создание нового экземпляра каждый раз, когда действие возобновляется

так как насчет сохранения ссылки на слушателя в деятельности

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

и в вашем onResume и onPause

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



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

}

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


Почему вы использовали super.onResume()раньше getPreferenceScreen()...?
Юша Алеауб

@YoushaAleayoub, прочитайте об android.app.supernotcalledexception, что требуется для реализации Android.
Самуил

что вы имеете в виду? использование super.onResume()требуется или использовать его, прежде чем getPreferenceScreen()требуется? потому что я говорю о правильном месте. cs.dartmouth.edu/~campbell/cs65/lecture05/lecture05.html
Юша Алеауб

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

Большое спасибо, везде, где я нашел методы onResume () и onPause (), которые они зарегистрировали thisи нет listener, это вызвало ошибку, и я смог решить мою проблему. Кстати, эти два метода сейчас публичны, а не защищены
Николас

16

Поскольку это самая подробная страница по теме, я хочу добавить свои 50 кар.

У меня была проблема, что OnSharedPreferenceChangeListener не был вызван. Мои SharedPreferences извлекаются в начале основного действия следующим образом:

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 из основного Activity:

@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членом класса Activity. Но есть третье и более простое решение, которое Google также использует в этой кодовой метке . Сделайте так, чтобы ваш класс активности реализовал OnSharedPreferenceChangeListenerинтерфейс и переопределил onSharedPreferenceChangedв Activity, эффективно делая само Activity 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

Код Kotlin для регистра 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 в начале и вызывать unregisterOnSharedPreferenceChangeListener в конце. Но вы не можете получить доступ к локальной переменной в методе, находящемся за ее пределами. Таким образом, у вас есть возможность зарегистрироваться, но нет возможности отменить регистрацию слушателя. Таким образом, использование WeakHashMap решит проблему. Это способ, который я рекомендую.

Если вы сделаете экземпляр слушателя как статическое поле, это позволит избежать утечки памяти, вызванной нестатическим внутренним классом. Но так как слушателей может быть несколько, это должно быть связано с экземпляром. Это уменьшит стоимость обработки обратного вызова onSharedPreferenceChanged .


-3

При чтении удобочитаемых данных Word, используемых первым приложением, мы должны

замещать

getSharedPreferences("PREF_NAME", Context.MODE_PRIVATE);

с участием

getSharedPreferences("PREF_NAME", Context.MODE_MULTI_PROCESS);

во втором приложении, чтобы получить обновленное значение во втором приложении.

Но все равно это не работает ...


Android не поддерживает доступ к SharedPreferences из нескольких процессов. Это вызывает проблемы параллелизма, которые могут привести к потере всех предпочтений. Кроме того, MODE_MULTI_PROCESS больше не поддерживается.
Сэм

@ Самому ответу уже 3 года, так что не голосуйте, если он не работает на вас в последних версиях Android. ответ времени был написан, это был лучший подход для этого.
Шридутт Котари

1
Нет, этот подход никогда не был безопасным для многих процессов, даже когда вы написали этот ответ.
Сэм

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

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