SharedPreferences.onSharedPreferenceChangeListener non chiamato in modo coerente


267

Sto registrando un listener di modifica preferenze come questo (nella onCreate()mia attività principale):

SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(this);

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

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

Il problema è che l'ascoltatore non viene sempre chiamato. Funziona per le prime volte in cui una preferenza viene modificata, quindi non viene più chiamata finché non disinstallo e reinstallo l'app. Nessuna quantità di riavvio dell'applicazione sembra risolverlo.

Ho trovato un thread della mailing list che riportava lo stesso problema, ma nessuno gli ha veramente risposto. Che cosa sto facendo di sbagliato?

Risposte:


612

Questo è subdolo. SharedPreferences mantiene gli ascoltatori in una WeakHashMap. Ciò significa che non è possibile utilizzare una classe interna anonima come listener, poiché diventerà la destinazione della Garbage Collection non appena si esce dall'ambito corrente. Funzionerà inizialmente, ma alla fine verrà raccolta dei rifiuti, rimossa da WeakHashMap e smetterà di funzionare.

Conserva un riferimento all'ascoltatore in un campo della tua classe e sarai OK, a condizione che l'istanza della tua classe non venga distrutta.

cioè invece di:

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

Fai questo:

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

Il motivo dell'annullamento della registrazione nel metodo onDestroy risolve il problema è perché per farlo è stato necessario salvare l'ascoltatore in un campo, evitando così il problema. È il salvataggio dell'ascoltatore in un campo che risolve il problema, non l'annullamento della registrazione in onDestroy.

AGGIORNAMENTO : i documenti Android sono stati aggiornati con avvisi su questo comportamento. Quindi, rimane un comportamento strano. Ma ora è documentato.


20
Questo mi stava uccidendo, pensavo di perdere la testa. Grazie per aver pubblicato questa soluzione!
Brad Hein,

10
Questo post è ENORME, grazie mille, questo mi avrebbe potuto costare ore di impossibile debug!
Kevin Gaudin,

Fantastico, questo è proprio quello di cui avevo bisogno. Buona spiegazione anche!
Stealthcopter,

Questo mi ha già morso, e non sapevo cosa succedesse fino a leggere questo post. Grazie! Boo, Android!
Nakedible,

5
Ottima risposta, grazie. Sicuramente dovrebbe essere menzionato nei documenti. code.google.com/p/android/issues/detail?id=48589
Andrey Chernih

16

questa risposta accettata è ok, poiché per me sta creando una nuova istanza ogni volta che l'attività riprende

che ne dite di mantenere il riferimento all'ascoltatore all'interno dell'attività

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

e nel tuo onResume e onPause

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



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

}

questo sarà molto simile a quello che stai facendo, tranne per il fatto che stiamo mantenendo un duro riferimento.


Perché l'hai usato super.onResume()prima getPreferenceScreen()...?
Yousha Aleayoub,

@YoushaAleayoub, leggi android.app.supernotcalledexception è richiesto dall'implementazione Android.
Samuel,

cosa intendi? l'utilizzo super.onResume()è richiesto O l'utilizzo PRIMA getPreferenceScreen()è richiesto? perché sto parlando del posto giusto. cs.dartmouth.edu/~campbell/cs65/lecture05/lecture05.html
Yousha Aleayoub

Ricordo di averlo letto qui developer.android.com/training/basics/activity-lifecycle/… , vedere il commento nel codice. ma metterlo in coda è logico. ma fino ad ora non ho riscontrato alcun problema in questo.
Samuel,

Grazie mille, ovunque ho trovato il metodo onResume () e onPause () che hanno registrato thise non listener, ha causato errori e ho potuto risolvere il mio problema. Tra questi due metodi sono ora pubblici, non protetti
Nicolas,

16

Poiché questa è la pagina più dettagliata per l'argomento, voglio aggiungere il mio 50ct.

Ho avuto il problema che OnSharedPreferenceChangeListener non è stato chiamato. Le mie condivisioni condivise vengono recuperate all'inizio dell'attività principale da:

prefs = PreferenceManager.getDefaultSharedPreferences(this);

Il mio codice Preferenza attiva è breve e non fa altro che mostrare le preferenze:

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

Ogni volta che si preme il pulsante menu creo PreferenceActivity dall'attività principale:

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

Nota che la registrazione di OnSharedPreferenceChangeListener deve essere effettuata DOPO la creazione di PreferenceActivity in questo caso, altrimenti il ​​gestore nell'attività principale non verrà chiamato !!! Mi ci è voluto un po 'di tempo dolce per rendermi conto che ...


9

La risposta accettata crea un SharedPreferenceChangeListenerogni volta che onResumeviene chiamato. @Samuel lo risolve creando SharedPreferenceListenerun membro della classe Activity. Ma c'è una terza e una soluzione più semplice che Google utilizza anche in questo codice . Rendi la tua classe di attività implementare l' OnSharedPreferenceChangeListenerinterfaccia e sovrascrivere onSharedPreferenceChangedl'attività, rendendola effettivamente un'attività 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
esattamente, dovrebbe essere così. implementare l'interfaccia, registrarsi su onStart e annullare la registrazione su onStop.
Junaed,

2

Codice di Kotlin per il registro SharedPreferenceChangeListener rileva quando si verificherà una modifica sulla chiave salvata:

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

puoi inserire questo codice in onStart () o altrove. * Considera che devi usare

 if(key=="YourKey")

oppure i tuoi codici all'interno del blocco "// Do Something" verranno eseguiti in modo errato per ogni modifica che si verificherà in qualsiasi altra chiave in sharedPreferences


1

Quindi, non so se questo aiuterebbe davvero qualcuno, ha risolto il mio problema. Anche se avevo implementato OnSharedPreferenceChangeListenercome indicato dalla risposta accettata . Tuttavia, avevo un'incongruenza con l'ascoltatore chiamato.

Sono venuto qui per capire che Android lo ha appena inviato per la raccolta dei rifiuti dopo qualche tempo. Quindi ho dato un'occhiata al mio codice. Con mia vergogna, non avevo dichiarato l'ascoltatore GLOBALMENTE ma invece all'interno onCreateView. E questo perché ho ascoltato Android Studio che mi diceva di convertire l'ascoltatore in una variabile locale.


0

È logico che gli ascoltatori siano mantenuti in WeakHashMap, poiché la maggior parte delle volte gli sviluppatori preferiscono scrivere il codice in questo modo.

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

Questo potrebbe non sembrare negativo. Ma se il contenitore OnSharedPreferenceChangeListeners non fosse WeakHashMap, sarebbe molto male. Se il codice sopra fosse scritto in un'attività. Dato che stai usando una classe interna non statica (anonima) che conterrà implicitamente il riferimento dell'istanza che la racchiude. Ciò causerà una perdita di memoria.

Inoltre, se si mantiene il listener come campo, è possibile utilizzare registerOnSharedPreferenceChangeListener all'inizio e chiamare unregisterOnSharedPreferenceChangeLhangeener alla fine. Ma non è possibile accedere a una variabile locale in un metodo al di fuori del suo ambito. Quindi hai solo l'opportunità di registrarti ma non hai la possibilità di annullare la registrazione dell'ascoltatore. Pertanto, l'utilizzo di WeakHashMap risolverà il problema. Questo è il modo in cui raccomando.

Se si crea l'istanza del listener come campo statico, si eviterà la perdita di memoria causata dalla classe interna non statica. Ma poiché gli ascoltatori potrebbero essere multipli, dovrebbe essere correlato all'istanza. Ciò ridurrà il costo di gestione del callback onSharedPreferenceChanged .


-3

Mentre leggiamo i dati leggibili in Word condivisi dalla prima app, dovremmo

Sostituire

getSharedPreferences("PREF_NAME", Context.MODE_PRIVATE);

con

getSharedPreferences("PREF_NAME", Context.MODE_MULTI_PROCESS);

nella seconda app per ottenere un valore aggiornato nella seconda app.

Ma ancora non funziona ...


Android non supporta l'accesso a SharedPreferences da più processi. Ciò causa problemi di concorrenza, che possono comportare la perdita di tutte le preferenze. Inoltre, MODE_MULTI_PROCESS non è più supportato.
Sam,

@Sam questa risposta ha 3 anni, quindi per favore non votarla, se non funziona per te nelle ultime versioni di Android. quando è stata scritta la risposta è stato l'approccio migliore per farlo.
Shridutt Kothari,

1
No, quell'approccio non è mai stato sicuro per più processi, anche quando hai scritto questa risposta.
Sam,

Come afferma @Sam, correttamente, le preferenze condivise non sono mai state sicure. Anche per shridutt kothari - se non ti piacciono i downvote rimuovi la tua risposta errata (non risponde comunque alla domanda del PO). Tuttavia, se desideri comunque utilizzare le preferenze condivise in modo sicuro per il processo, devi creare un'astrazione sicura per il processo al di sopra di essa, ad esempio un ContentProvider, che è sicuro per il processo e ti consente comunque di utilizzare le preferenze condivise come meccanismo di archiviazione persistente. , L'ho già fatto prima, e per piccoli set di dati / preferenze esegue sqlite con un discreto margine.
Mark Keen,

1
@MarkKeen A proposito, in realtà ho provato a utilizzare i fornitori di contenuti per questo e i fornitori di contenuti tendevano a fallire casualmente nella produzione a causa di bug Android, quindi in questi giorni utilizzo solo le trasmissioni per sincronizzare la configurazione con i processi secondari secondo necessità.
Sam,
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.