SharedPreferences.onSharedPreferenceChangeListener nie jest wywoływany konsekwentnie

267

Rejestruję odbiornik zmiany preferencji w ten sposób (w onCreate()mojej głównej działalności):

SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(this);

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

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

Problem w tym, że słuchacz nie zawsze jest wywoływany. Działa po raz pierwszy, gdy preferencje są zmieniane, a następnie nie są wywoływane, dopóki nie odinstaluję i ponownie nie zainstaluję aplikacji. Wydaje się, że żadna ilość ponownego uruchomienia aplikacji nie naprawi tego.

Znalazłem wątek na liście mailingowej zgłaszający ten sam problem, ale tak naprawdę nikt mu nie odpowiedział. Co ja robię źle?

synic
źródło

Odpowiedzi:

612

To jest podstępne. SharedPreferences utrzymuje nasłuchujących w WeakHashMap. Oznacza to, że nie można używać anonimowej klasy wewnętrznej jako detektora, ponieważ stanie się ona celem odśmiecania, gdy tylko opuścisz obecny zakres. Będzie to działało na początku, ale ostatecznie zostanie zebrane śmieci, usunięte z WeakHashMap i przestanie działać.

Zachowaj odniesienie do słuchacza w polu swojej klasy, a będziesz w porządku, pod warunkiem, że instancja klasy nie zostanie zniszczona.

tj. zamiast:

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

Zrób to:

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

Przyczyną wyrejestrowania się w metodzie onDestroy jest naprawienie problemu, ponieważ w tym celu trzeba było zapisać słuchacza w polu, co zapobiegło problemowi. To zapisanie nasłuchiwania w polu, które rozwiązuje problem, a nie wyrejestrowanie się z onDestroy.

AKTUALIZACJA : Dokumenty Androida zostały zaktualizowane o ostrzeżenia o tym zachowaniu. Tak więc dziwne zachowanie pozostaje. Ale teraz jest to udokumentowane.

Blanka
źródło
20
To mnie zabijało, myślałem, że tracę rozum. Dziękujemy za opublikowanie tego rozwiązania!
Brad Hein
10
Ten post jest OGROMNY, wielkie dzięki, może mnie to kosztować wiele godzin niemożliwego debugowania!
Kevin Gaudin
Świetnie, właśnie tego potrzebowałem. Dobre wyjaśnienie też!
stealthcopter 15.01.11
Już mnie to ugryzło i nie wiedziałem, co się dzieje, dopóki nie przeczytałem tego postu. Dziękuję Ci! Boo, Android!
Nakedible
5
Świetna odpowiedź, dziękuję. Zdecydowanie należy wspomnieć w dokumentacji. code.google.com/p/android/issues/detail?id=48589
Andrey Chernih
16

ta zaakceptowana odpowiedź jest w porządku, ponieważ dla mnie tworzy ona nową instancję za każdym razem, gdy aktywność zostanie wznowiona

co powiesz na zachowanie odniesienia do słuchacza w ramach działania

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

oraz w onResume i onPause

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



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

}

będzie to bardzo podobne do tego, co robisz, z wyjątkiem tego, że utrzymujemy twarde referencje.

Samuel
źródło
Dlaczego używałeś super.onResume()wcześniej getPreferenceScreen()...?
Yousha Aleayoub
@YoushaAleayoub, poczytaj o android.app.supernotcalledexception, który jest wymagany przez implementację Androida.
Samuel
co masz na myśli? korzystanie super.onResume()jest wymagane LUB korzystanie z niego PRZED getPreferenceScreen()jest wymagane? ponieważ mówię o właściwym miejscu. cs.dartmouth.edu/~campbell/cs65/lecture05/lecture05.html
Yousha Aleayoub
Pamiętam, że czytałem go tutaj developer.android.com/training/basics/activity-lifecycle/... , zobacz komentarz w kodzie. ale umieszczenie go w ogonie jest logiczne. ale do tej pory nie miałem z tym żadnych problemów.
Samuel,
Bardzo dziękuję, wszędzie tam, gdzie znalazłem metodę onResume () i onPause (), którą zarejestrowali, thisi nie listener, spowodowało to błąd i mogłem rozwiązać mój problem. Btw te dwie metody są teraz publiczne, nie chronione
Nicolas
16

Ponieważ jest to najbardziej szczegółowa strona tematu, chcę dodać 50ct.

Miałem problem, że nie wywołano OnSharedPreferenceChangeListener. Moje współdzielone preferencje są pobierane na początku głównej aktywności przez:

prefs = PreferenceManager.getDefaultSharedPreferences(this);

Mój kod preferencji jest krótki i nie robi nic poza pokazaniem preferencji:

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

Za każdym naciśnięciem przycisku menu tworzę PreferenceActivity z głównego działania:

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

Należy pamiętać, że rejestracja OnSharedPreferenceChangeListener musi zostać wykonana PO utworzeniu PreferenceActivity w tym przypadku, w przeciwnym razie moduł obsługi w głównym działaniu nie zostanie wywołany !!! Uświadomienie sobie, że ...

Bim
źródło
9

Zaakceptowana odpowiedź tworzy za SharedPreferenceChangeListenerkażdym razem, gdy onResumejest wywoływana. @Samuel rozwiązuje to, czyniąc SharedPreferenceListenerczłonka klasy Activity. Ale istnieje trzecie i prostsze rozwiązanie, którego Google używa również w tym kodzie kodowym . Spraw, aby klasa działań implementowała OnSharedPreferenceChangeListenerinterfejs i zastępowała onSharedPreferenceChangeddziałanie, skutecznie czyniąc samo działanie 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);
    }
}
PrashanD
źródło
1
dokładnie tak powinno być. zaimplementuj interfejs, zarejestruj się w onStart i wyrejestruj w onStop.
Junaed
2

Kod Kotlin dla rejestru SharedPreferenceChangeListener wykrywa, kiedy nastąpi zmiana na zapisanym kluczu:

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

możesz umieścić ten kod w onStart () lub w innym miejscu .. * Zastanów się, że musisz użyć

 if(key=="YourKey")

lub kody wewnątrz bloku „// Zrób coś” będą uruchamiane niepoprawnie dla każdej zmiany, która nastąpi w dowolnym innym kluczu w sharePreferences

Hamed Jaliliani
źródło
1

Więc nie wiem, czy to naprawdę komukolwiek by pomogło, rozwiązało to mój problem. Mimo że OnSharedPreferenceChangeListenerwdrożyłem zgodnie z przyjętą odpowiedzią . Mimo to miałem niespójność z wezwaniem słuchacza.

Przyszedłem tutaj, aby zrozumieć, że Android po prostu wysyła go do śmieci po pewnym czasie. Spojrzałem na mój kod. Ku mojemu wstydowi nie zadeklarowałem GLOBALNIE słuchacza, ale zamiast tego w onCreateView. A to dlatego, że słuchałem Android Studio z poleceniem konwersji odbiornika na zmienną lokalną.

Asghar Musani
źródło
0

Ma to sens, że nasłuchiwania są przechowywane w WeakHashMap, ponieważ przez większość czasu programiści wolą pisać taki kod.

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

To może wydawać się niezłe. Ale jeśli kontener OnSharedPreferenceChangeListeners nie był WeakHashMap, byłoby bardzo źle. Jeśli powyższy kod został napisany w działaniu. Ponieważ używasz niestatycznej (anonimowej) klasy wewnętrznej, która niejawnie przechowuje odwołanie do otaczającej instancji. Spowoduje to wyciek pamięci.

Co więcej, jeśli trzymasz detektor jako pole, możesz użyć registerOnSharedPreferenceChangeListener na początku i wywołać unregisterOnSharedPreferenceChangeListener na końcu. Ale nie można uzyskać dostępu do zmiennej lokalnej w metodzie spoza jej zakresu. Masz więc tylko możliwość zarejestrowania się, ale nie ma możliwości wyrejestrowania słuchacza. Zatem użycie WeakHashMap rozwiąże problem. Tak właśnie polecam.

Jeśli utworzysz instancję detektora jako pole statyczne, unikniesz wycieku pamięci spowodowanego przez niestatyczną klasę wewnętrzną. Ale ponieważ nasłuchiwanie może być wielokrotne, powinno być związane z instancją. Zmniejszy to koszty obsługi wywołania zwrotnego onSharedPreferenceChanged .

androidyue
źródło
-3

Podczas czytania danych czytelnych dla programu Word udostępnianych przez pierwszą aplikację, powinniśmy

Zastąpić

getSharedPreferences("PREF_NAME", Context.MODE_PRIVATE);

z

getSharedPreferences("PREF_NAME", Context.MODE_MULTI_PROCESS);

w drugiej aplikacji, aby uzyskać zaktualizowaną wartość w drugiej aplikacji.

Ale nadal nie działa ...

shridutt kothari
źródło
Android nie obsługuje dostępu do SharedPreferences z wielu procesów. Takie postępowanie powoduje problemy z współbieżnością, które mogą spowodować utratę wszystkich preferencji. Ponadto MODE_MULTI_PROCESS nie jest już obsługiwany.
Sam
@Sam Ta odpowiedź ma 3 lata, więc nie głosuj w dół, jeśli nie działa dla Ciebie w najnowszych wersjach Androida. kiedy napisano odpowiedź, było to najlepsze podejście.
shridutt kothari,
1
Nie, takie podejście nigdy nie było bezpieczne dla wielu procesów, nawet gdy napisałeś tę odpowiedź.
Sam
Jak stwierdza @Sam, poprawnie współdzielone prefiksy nigdy nie były przetwarzane bezpiecznie. Również do shridutt kothari - jeśli nie podoba ci się głosowanie negatywne, usuń niepoprawną odpowiedź (i tak nie odpowiada na pytanie PO). Jeśli jednak nadal chcesz używać współużytkowanych prefiksów w sposób bezpieczny dla procesu, musisz utworzyć nad nim bezpieczną abstrakcję, tj. ContentProvider, który jest bezpieczny dla procesu i nadal pozwala ci używać współużytkowanych preferencji jako trwałego mechanizmu przechowywania. , Robiłem to już wcześniej, a dla małych zestawów danych / preferencji out wykonuje sqlite z dużym marginesem.
Mark Keen
1
@MarkKeen Nawiasem mówiąc, faktycznie próbowałem użyć dostawców treści do tego, a dostawcy treści mieli tendencję do losowego niepowodzenia w produkcji z powodu błędów Androida, więc teraz używam transmisji do synchronizacji konfiguracji z procesami wtórnymi w razie potrzeby.
Sam