SharedPreferences.onSharedPreferenceChangeListener no se llama constantemente


267

Estoy registrando un oyente de cambio de preferencia como este (en el onCreate()de mi actividad principal):

SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(this);

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

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

El problema es que no siempre se llama al oyente. Funciona las primeras veces que se cambia una preferencia, y luego ya no se llama hasta que desinstale y vuelva a instalar la aplicación. Ninguna cantidad de reinicio de la aplicación parece solucionarlo.

Encontré un hilo de la lista de correo informando el mismo problema, pero nadie realmente le respondió. ¿Qué estoy haciendo mal?

Respuestas:


612

Esta es una disimulada. SharedPreferences mantiene a los oyentes en un WeakHashMap. Esto significa que no puede usar una clase interna anónima como escucha, ya que se convertirá en el objetivo de la recolección de basura tan pronto como abandone el alcance actual. Al principio funcionará, pero eventualmente, se recolectará la basura, se eliminará del WeakHashMap y dejará de funcionar.

Mantenga una referencia al oyente en un campo de su clase y estará bien, siempre que su instancia de clase no se destruya.

es decir, en lugar de:

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

hacer esto:

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

La razón por la que la cancelación del registro en el método onDestroy soluciona el problema es porque para hacerlo tenía que guardar el oyente en un campo, evitando así el problema. Es salvar al oyente en un campo lo que soluciona el problema, no anular el registro en onDestroy.

ACTUALIZACIÓN : los documentos de Android se han actualizado con advertencias sobre este comportamiento. Entonces, el comportamiento extraño sigue siendo. Pero ahora está documentado.


20
Esto me estaba matando, pensé que me estaba volviendo loco. ¡Gracias por publicar esta solución!
Brad Hein

10
Esta publicación es ENORME, muchas gracias, ¡esto podría haberme costado horas de depuración imposible!
Kevin Gaudin

Impresionante, esto es justo lo que necesitaba. Buena explicación también!
stealthcopter

Esto ya me mordió, y no sabía qué pasaba hasta leer esta publicación. ¡Gracias! Boo, Android!
Desnudo

55
Gran respuesta, gracias. Definitivamente debería mencionarse en los documentos. code.google.com/p/android/issues/detail?id=48589
Andrey Chernih

16

esta respuesta aceptada está bien, ya que para mí está creando una nueva instancia cada vez que se reanuda la actividad

Entonces, ¿qué hay de mantener la referencia al oyente dentro de la actividad

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

y en tu onResume y onPause

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



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

}

Esto será muy similar a lo que está haciendo, excepto que mantenemos una referencia sólida.


¿Por qué lo usaste super.onResume()antes getPreferenceScreen()...?
Yousha Aleayoub

@YoushaAleayoub, lea sobre android.app.supernotcalledexception que requiere la implementación de Android.
Samuel

¿Qué quieres decir? el uso super.onResume()que se requiera o utilizando este medicamento antes getPreferenceScreen()se requiere? porque estoy hablando del lugar CORRECTO. cs.dartmouth.edu/~campbell/cs65/lecture05/lecture05.html
Yousha Aleayoub

Recuerdo haberlo leído aquí developer.android.com/training/basics/activity-lifecycle/… , vea el comentario en el código. pero ponerlo en la cola es lógico. pero hasta ahora no he tenido ningún problema en esto.
Samuel

Muchas gracias, en todas partes que encontré en el método Resume () y onPause () que registraron thisy no listener, causó un error y pude resolver mi problema. Por cierto, estos dos métodos son públicos ahora, no están protegidos
Nicolas

16

Como esta es la página más detallada para el tema, quiero agregar mi 50ct.

Tuve el problema de que no se llamaba a OnSharedPreferenceChangeListener. Mis preferencias compartidas se recuperan al comienzo de la actividad principal:

prefs = PreferenceManager.getDefaultSharedPreferences(this);

Mi código de PreferenceActivity es corto y no hace nada excepto mostrar las preferencias:

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

Cada vez que se presiona el botón de menú, creo la Actividad de Preferencia desde la Actividad principal:

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

Tenga en cuenta que el registro de OnSharedPreferenceChangeListener debe hacerse DESPUÉS de crear PreferenceActivity en este caso, de lo contrario, no se llamará al controlador en la actividad principal. Me tomó un tiempo dulce darme cuenta de que ...


9

La respuesta aceptada crea un SharedPreferenceChangeListenercada vez que onResumese llama. @Samuel lo resuelve al hacer SharedPreferenceListenerun miembro de la clase Actividad. Pero hay una tercera y más sencilla solución que Google también usa en este codelab . Haga que su clase de actividad implemente la OnSharedPreferenceChangeListenerinterfaz y anule onSharedPreferenceChangedla actividad, haciendo que la actividad en sí sea 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
exactamente, esto debería ser. implemente la interfaz, regístrese en onStart y anule el registro en onStop.
Junaed

2

Código de Kotlin para el registro SharedPreferenceChangeListener que detecta cuando se producirán cambios en la clave guardada:

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

puede poner este código en onStart (), o en otro lugar .. * Tenga en cuenta que debe usar

 if(key=="YourKey")

o sus códigos dentro del bloque "// Do Something" se ejecutarán incorrectamente para cada cambio que ocurra en cualquier otra clave en sharedPreferences


1

Entonces, no sé si esto realmente ayudaría a alguien, resolvió mi problema. A pesar de que había implementado lo OnSharedPreferenceChangeListenerque dice la respuesta aceptada . Aún así, tuve una inconsistencia con la llamada del oyente.

Vine aquí para entender que el Android simplemente lo envía para la recolección de basura después de algún tiempo. Entonces, miré mi código. Para mi vergüenza, no había declarado al oyente GLOBALMENTE, sino dentro del onCreateView. Y eso fue porque escuché a Android Studio diciéndome que convirtiera el oyente a una variable local.


0

Tiene sentido que los oyentes se mantengan en WeakHashMap. Debido a que la mayoría de las veces, los desarrolladores prefieren escribir el código de esta manera.

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

Esto puede parecer no está mal. Pero si el contenedor de OnSharedPreferenceChangeListeners no fuera WeakHashMap, sería muy malo si el código anterior se escribiera en una actividad. Como está utilizando una clase interna no estática (anónima) que implícitamente contiene la referencia de la instancia de cierre. Esto causará pérdida de memoria.

Además, si mantiene el oyente como un campo, puede usar registerOnSharedPreferenceChangeListener al principio y llamar a unregisterOnSharedPreferenceChangeListener al final. Pero no puede acceder a una variable local en un método fuera de su alcance. Por lo tanto, solo tiene la oportunidad de registrarse, pero no la posibilidad de cancelar el registro del oyente. Por lo tanto, usar WeakHashMap resolverá el problema. Esta es la forma que recomiendo.

Si realiza la instancia de escucha como un campo estático, evitará la pérdida de memoria causada por la clase interna no estática. Pero como los oyentes podrían ser múltiples, debería estar relacionado con la instancia. Esto reducirá el costo de manejar la devolución de llamada onSharedPreferenceChanged .


-3

Al leer los datos legibles de Word compartidos por la primera aplicación, deberíamos

Reemplazar

getSharedPreferences("PREF_NAME", Context.MODE_PRIVATE);

con

getSharedPreferences("PREF_NAME", Context.MODE_MULTI_PROCESS);

en la segunda aplicación para obtener un valor actualizado en la segunda aplicación.

Pero aún así no está funcionando ...


Android no admite el acceso a SharedPreferences desde múltiples procesos. Hacerlo provoca problemas de concurrencia, lo que puede provocar la pérdida de todas las preferencias. Además, MODE_MULTI_PROCESS ya no es compatible.
Sam

@Sam, esta respuesta tiene 3 años, así que no la desestimes, si no funciona para ti en las últimas versiones de Android. la hora en que se escribió la respuesta fue el mejor enfoque para hacerlo.
shridutt kothari

1
No, ese enfoque nunca fue seguro para múltiples procesos, incluso cuando escribió esta respuesta.
Sam

Como dice @Sam, correctamente, las preferencias compartidas nunca fueron seguras para el proceso. También para shridutt kothari: si no le gustan los votos negativos, elimine su respuesta incorrecta (de todos modos, no responde a la pregunta del OP). Sin embargo, si aún desea utilizar las preferencias compartidas de una manera segura para el proceso, debe crear una abstracción segura para el proceso por encima, es decir, un ContentProvider, que es seguro para el proceso y aún le permite usar las preferencias compartidas como mecanismo de almacenamiento persistente. , He hecho esto antes, y para pequeños conjuntos de datos / preferencias, realiza sqlite por un margen justo.
Mark Keen

1
@MarkKeen Por cierto, en realidad intenté usar proveedores de contenido para esto y los proveedores de contenido tendieron a fallar aleatoriamente en Producción debido a errores de Android, por lo que en estos días solo uso transmisiones para sincronizar la configuración con procesos secundarios según sea necesario.
Sam
Al usar nuestro sitio, usted reconoce que ha leído y comprende nuestra Política de Cookies y Política de Privacidad.
Licensed under cc by-sa 3.0 with attribution required.