SharedPreferences.onSharedPreferenceChangeListener wird nicht konsistent aufgerufen

267

Ich registriere einen Listener für Präferenzänderungen wie folgt (in onCreate()meiner Hauptaktivität):

SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(this);

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

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

Das Problem ist, dass der Hörer nicht immer angerufen wird. Es funktioniert für die ersten Male, wenn eine Einstellung geändert wird, und wird dann nicht mehr aufgerufen, bis ich die App deinstalliere und neu installiere. Kein Neustart der Anwendung scheint das Problem zu beheben.

Ich habe einen Mailinglisten- Thread gefunden , der das gleiche Problem meldet, aber niemand hat ihm wirklich geantwortet. Was mache ich falsch?

synic
quelle

Antworten:

612

Dies ist eine hinterhältige. SharedPreferences hält Listener in einer WeakHashMap. Dies bedeutet, dass Sie keine anonyme innere Klasse als Listener verwenden können, da diese zum Ziel der Speicherbereinigung wird, sobald Sie den aktuellen Bereich verlassen. Es wird zuerst funktionieren, aber irgendwann wird Müll gesammelt, aus der WeakHashMap entfernt und funktioniert nicht mehr.

Behalten Sie einen Verweis auf den Listener in einem Feld Ihrer Klasse bei, und Sie sind in Ordnung, vorausgesetzt, Ihre Klasseninstanz wird nicht zerstört.

dh anstelle von:

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

mach das:

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

Der Grund für die Aufhebung der Registrierung in der onDestroy-Methode ist, dass Sie den Listener in einem Feld speichern mussten, um das Problem zu vermeiden. Es ist das Speichern des Listeners in einem Feld, das das Problem behebt, nicht das Aufheben der Registrierung in onDestroy.

UPDATE : Die Android-Dokumente wurden mit Warnungen zu diesem Verhalten aktualisiert . So bleibt seltsames Verhalten. Aber jetzt ist es dokumentiert.

Blanka
quelle
20
Das brachte mich um, ich dachte, ich würde den Verstand verlieren. Vielen Dank, dass Sie diese Lösung veröffentlicht haben!
Brad Hein
10
Dieser Beitrag ist RIESIG, vielen Dank, das hätte mich Stunden unmögliches Debuggen kosten können!
Kevin Gaudin
Genial, das ist genau das, was ich brauchte. Gute Erklärung auch!
Stealthcopter
Das hat mich schon gebissen und ich wusste nicht, was los war, bis ich diesen Beitrag gelesen habe. Danke dir! Boo, Android!
Nakedible
5
Tolle Antwort, danke. Sollte auf jeden Fall in den Dokumenten erwähnt werden. code.google.com/p/android/issues/detail?id=48589
Andrey Chernih
16

Diese akzeptierte Antwort ist in Ordnung, da für mich jedes Mal, wenn die Aktivität fortgesetzt wird, eine neue Instanz erstellt wird

Wie wäre es also, wenn Sie den Verweis auf den Hörer innerhalb der Aktivität behalten?

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

und in Ihrem onResume und onPause

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



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

}

Dies wird sehr ähnlich zu dem sein, was Sie tun, außer dass wir eine harte Referenz beibehalten.

Samuel
quelle
Warum hast du super.onResume()vorher benutzt getPreferenceScreen()...?
Yousha Aleayoub
@YoushaAleayoub, lesen Sie über android.app.supernotcalledexception , die für die Android-Implementierung erforderlich ist.
Samuel
Was meinst du? Verwendung super.onResume()ist erforderlich ODER Verwendung, BEVOR getPreferenceScreen()erforderlich ist? weil ich über den richtigen Ort spreche. cs.dartmouth.edu/~campbell/cs65/lecture05/lecture05.html
Yousha Aleayoub
Ich erinnere mich, dass ich es hier gelesen habe. Developer.android.com/training/basics/activity-lifecycle/… , siehe den Kommentar im Code. Aber es ist logisch, es in den Schwanz zu stecken. aber bis jetzt habe ich keine Probleme damit gehabt.
Samuel
Vielen Dank, überall sonst, wo ich die onResume () - und onPause () -Methode gefunden habe, die sie registriert haben thisund nicht listener, hat sie Fehler verursacht und ich konnte mein Problem lösen. Übrigens sind diese beiden Methoden jetzt öffentlich und nicht geschützt
Nicolas
16

Da dies die detaillierteste Seite für das Thema ist, möchte ich meine 50ct hinzufügen.

Ich hatte das Problem, dass OnSharedPreferenceChangeListener nicht aufgerufen wurde. Meine SharedPreferences werden zu Beginn der Hauptaktivität abgerufen von:

prefs = PreferenceManager.getDefaultSharedPreferences(this);

Mein PreferenceActivity-Code ist kurz und zeigt nur die Einstellungen an:

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

Jedes Mal, wenn die Menütaste gedrückt wird, erstelle ich die PreferenceActivity aus der Hauptaktivität:

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

Beachten Sie, dass die Registrierung des OnSharedPreferenceChangeListener in diesem Fall nach dem Erstellen der PreferenceActivity erfolgen muss, da sonst der Handler in der Hauptaktivität nicht aufgerufen wird !!! Ich brauchte einige süße Zeit, um zu erkennen, dass ...

Bim
quelle
9

Die akzeptierte Antwort erstellt SharedPreferenceChangeListenerjedes Mal einen onResumeAufruf. @Samuel löst das Problem , indem er SharedPreferenceListenerMitglied der Aktivitätsklasse wird. Es gibt jedoch eine dritte und einfachere Lösung, die Google auch in diesem Codelab verwendet . Lassen Sie Ihre Aktivitätsklasse die OnSharedPreferenceChangeListenerSchnittstelle implementieren und onSharedPreferenceChangedin der Aktivität überschreiben , sodass die Aktivität selbst effektiv zu einer Aktivität wird 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
quelle
1
genau das sollte es sein. Implementieren Sie die Schnittstelle, registrieren Sie sich in onStart und entfernen Sie die Registrierung in onStop.
Junaed
2

Kotlin-Code für das Register SharedPreferenceChangeListener erkennt, wann Änderungen am gespeicherten Schlüssel vorgenommen werden:

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

Sie können diesen Code in onStart () oder an einer anderen Stelle einfügen. * Beachten Sie, dass Sie ihn verwenden müssen

 if(key=="YourKey")

oder Ihre Codes im Block "// Do Something" werden bei jeder Änderung, die an einem anderen Schlüssel in sharedPreferences vorgenommen wird, falsch ausgeführt

Hamed Jaliliani
quelle
1

Ich weiß also nicht, ob dies wirklich jemandem helfen würde, es hat mein Problem gelöst. Obwohl ich das OnSharedPreferenceChangeListenerwie in der akzeptierten Antwort angegeben umgesetzt hatte . Trotzdem hatte ich eine Inkonsistenz mit dem angerufenen Hörer.

Ich bin hierher gekommen, um zu verstehen, dass Android es nach einiger Zeit nur zur Speicherbereinigung sendet. Also schaute ich auf meinen Code. Zu meiner Schande hatte ich den Hörer nicht GLOBAL deklariert, sondern im onCreateView. Und das lag daran, dass ich mir das Android Studio angehört habe, in dem ich aufgefordert wurde, den Listener in eine lokale Variable umzuwandeln.

Asghar Musani
quelle
0

Es ist sinnvoll, dass die Listener in WeakHashMap gespeichert bleiben. Da Entwickler es meistens vorziehen, den Code so zu schreiben.

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

Das mag nicht schlecht erscheinen. Wenn der Container von OnSharedPreferenceChangeListeners jedoch nicht WeakHashMap wäre, wäre dies sehr schlecht. Wenn der obige Code in einer Aktivität geschrieben wurde. Da Sie eine nicht statische (anonyme) innere Klasse verwenden, die implizit die Referenz der einschließenden Instanz enthält. Dies führt zu einem Speicherverlust.

Wenn Sie den Listener als Feld behalten, können Sie am Anfang registerOnSharedPreferenceChangeListener verwenden und am Ende unregisterOnSharedPreferenceChangeListener aufrufen . Sie können jedoch nicht auf eine lokale Variable in einer Methode außerhalb ihres Gültigkeitsbereichs zugreifen. Sie haben also nur die Möglichkeit, sich zu registrieren, aber keine Möglichkeit, die Registrierung des Hörers aufzuheben. Durch die Verwendung von WeakHashMap wird das Problem behoben. Dies ist der Weg, den ich empfehle.

Wenn Sie die Listener-Instanz als statisches Feld festlegen, wird der durch nicht statische innere Klasse verursachte Speicherverlust vermieden. Da die Listener jedoch mehrere sein können, sollte dies instanzbezogen sein. Dies reduziert die Kosten für die Bearbeitung des onSharedPreferenceChanged- Rückrufs.

androidyue
quelle
-3

Beim Lesen von Word-lesbaren Daten, die von der ersten App gemeinsam genutzt werden, sollten wir dies tun

Ersetzen

getSharedPreferences("PREF_NAME", Context.MODE_PRIVATE);

mit

getSharedPreferences("PREF_NAME", Context.MODE_MULTI_PROCESS);

in der zweiten App, um den aktualisierten Wert in der zweiten App zu erhalten.

Aber es funktioniert immer noch nicht ...

Shridutt Kothari
quelle
Android unterstützt den Zugriff auf SharedPreferences von mehreren Prozessen aus nicht. Dies führt zu Parallelitätsproblemen, die dazu führen können, dass alle Einstellungen verloren gehen. Außerdem wird MODE_MULTI_PROCESS nicht mehr unterstützt.
Sam
@ Sam diese Antwort ist 3 Jahre alt, bitte stimmen Sie sie nicht ab, wenn sie in den neuesten Android-Versionen nicht für Sie funktioniert. Die Antwort wurde geschrieben, es war der beste Ansatz, dies zu tun.
Shridutt Kothari
1
Nein, dieser Ansatz war niemals mehrprozesssicher, selbst wenn Sie diese Antwort geschrieben haben.
Sam
Wie @Sam richtig feststellt, waren gemeinsam genutzte Einstellungen niemals prozesssicher. Auch um Shridutt Kothari - wenn Ihnen die Downvotes nicht gefallen, entfernen Sie Ihre falsche Antwort (sie beantwortet die Frage des OP sowieso nicht). Wenn Sie jedoch gemeinsam genutzte Einstellungen weiterhin prozesssicher verwenden möchten, müssen Sie darüber eine prozesssichere Abstraktion erstellen, z. B. einen ContentProvider, der prozesssicher ist und es Ihnen weiterhin ermöglicht, gemeinsam genutzte Einstellungen als dauerhaften Speichermechanismus zu verwenden. Ich habe das schon einmal gemacht und für kleine Datensätze / Einstellungen führt SQLite mit einem fairen Vorsprung aus.
Mark Keen
1
@MarkKeen Übrigens habe ich tatsächlich versucht, Inhaltsanbieter dafür zu verwenden, und die Inhaltsanbieter scheiterten in der Produktion aufgrund von Android-Fehlern zufällig. Daher verwende ich heutzutage nur Broadcasts, um die Konfiguration nach Bedarf mit sekundären Prozessen zu synchronisieren.
Sam