SharedPreferences.onSharedPreferenceChangeListener tidak dipanggil secara konsisten

267

Saya mendaftarkan pendengar perubahan preferensi seperti ini (di onCreate()aktivitas utama saya):

SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(this);

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

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

Masalahnya adalah, pendengar tidak selalu dipanggil. Ini berfungsi untuk beberapa kali pertama preferensi diubah, dan kemudian tidak lagi dipanggil sampai saya menghapus dan menginstal ulang aplikasi. Tampaknya tidak ada jumlah memulai ulang aplikasi untuk memperbaikinya.

Saya menemukan utas milis melaporkan masalah yang sama, tetapi tidak ada yang benar-benar menjawabnya. Apa yang saya lakukan salah?

synic
sumber

Jawaban:

612

Ini yang licik. SharedPreferences menyimpan pendengar di WeakHashMap. Ini berarti bahwa Anda tidak dapat menggunakan kelas dalam anonim sebagai pendengar, karena itu akan menjadi target pengumpulan sampah segera setelah Anda meninggalkan ruang lingkup saat ini. Ini akan bekerja pada awalnya, tetapi pada akhirnya, akan mengumpulkan sampah, dihapus dari WeakHashMap dan berhenti bekerja.

Simpan referensi ke pendengar di bidang kelas Anda dan Anda akan baik-baik saja, asalkan instance kelas Anda tidak dihancurkan.

yaitu bukannya:

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

melakukan hal ini:

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

Alasan tidak mendaftar di metode onDestroy memperbaiki masalah adalah karena untuk melakukan itu Anda harus menyimpan pendengar di lapangan, karena itu mencegah masalah. Ini menyimpan pendengar di bidang yang memperbaiki masalah, bukan membatalkan pendaftaran di onDestroy.

PEMBARUAN : Dokumen Android telah diperbarui dengan peringatan tentang perilaku ini. Jadi, perilaku aneh tetap ada. Tapi sekarang sudah didokumentasikan.

Blanka
sumber
20
Ini membunuh saya, saya pikir saya kehilangan akal. Terima kasih telah mengirimkan solusi ini!
Brad Hein
10
Posting ini SANGAT BESAR, terima kasih banyak, ini bisa membuat saya berjam-jam mustahil melakukan debugging!
Kevin Gaudin
Luar biasa, inilah yang saya butuhkan. Penjelasan yang bagus juga!
stealthcopter
Ini sudah menggigit saya, dan saya tidak tahu apa yang terjadi sampai membaca posting ini. Terima kasih! Boo, Android!
Nakedible
5
Jawaban yang bagus, terima kasih. Jelas harus disebutkan dalam dokumen. code.google.com/p/android/issues/detail?id=48589
Andrey Chernih
16

jawaban yang diterima ini ok, karena bagi saya itu membuat contoh baru setiap kali aktivitas dilanjutkan

jadi bagaimana menyimpan referensi ke pendengar dalam aktivitas

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

dan di onResume dan onPause Anda

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



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

}

ini akan sangat mirip dengan apa yang Anda lakukan kecuali kami mempertahankan referensi keras.

Samuel
sumber
Mengapa Anda menggunakannya super.onResume()sebelumnya getPreferenceScreen()...?
Yousha Aleayoub
@YoushaAleayoub, baca tentang android.app.supernotcalledexception diperlukan oleh implementasi android.
Samuel
maksud kamu apa? menggunakan super.onResume()diperlukan ATAU menggunakannya SEBELUM getPreferenceScreen()diharuskan? karena saya sedang berbicara tentang tempat yang BENAR. cs.dartmouth.edu/~campbell/cs65/lecture05/lecture05.html
Yousha Aleayoub
Saya ingat membaca di sini developer.android.com/training/basics/activity-lifecycle/… , lihat komentar dalam kode. tapi membuntutinya logis. tetapi sampai sekarang saya belum menghadapi masalah dalam hal ini.
Samuel
Terima kasih banyak, di mana pun saya menemukan metode onResume () dan onPause () yang mereka daftarkan thisdan tidak listener, itu menyebabkan kesalahan dan saya bisa menyelesaikan masalah saya. Btw kedua metode ini sekarang sudah umum, tidak dilindungi
Nicolas
16

Karena ini adalah halaman paling detail untuk topik yang ingin saya tambahkan 50ct saya.

Saya punya masalah yang tidak dipanggil OnSharedPreferenceChangeListener. Preferensi Berbagi Saya diambil pada awal Kegiatan utama dengan:

prefs = PreferenceManager.getDefaultSharedPreferences(this);

Kode PreferenceActivity saya pendek dan tidak melakukan apa pun kecuali menunjukkan preferensi:

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

Setiap kali tombol menu ditekan, saya membuat PreferenceActivity dari Kegiatan utama:

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

Perhatikan bahwa mendaftarkan OnSharedPreferenceChangeListener perlu dilakukan SETELAH membuat PreferenceActivity dalam kasus ini, kalau tidak Handler dalam Kegiatan utama tidak akan dipanggil !!! Aku butuh waktu yang manis untuk menyadari bahwa ...

Bim
sumber
9

Jawaban yang diterima menciptakan SharedPreferenceChangeListenersetiap kali onResumedipanggil. @Samuel menyelesaikannya dengan membuat SharedPreferenceListeneranggota kelas Activity. Tapi ada solusi ketiga dan lebih langsung yang digunakan Google dalam codelab ini . Buat kelas aktivitas Anda mengimplementasikan OnSharedPreferenceChangeListenerantarmuka dan menimpa onSharedPreferenceChangeddalam Aktivitas, secara efektif menjadikan Aktivitas itu sendiri 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);
    }
}
PrashanD
sumber
1
tepatnya, ini seharusnya. mengimplementasikan antarmuka, mendaftar di onStart, dan batalkan pendaftaran di onStop.
Junaed
2

Kode Kotlin untuk register SharedPreferenceChangeListener mendeteksi ketika perubahan akan terjadi pada kunci yang disimpan:

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

Anda dapat meletakkan kode ini di onStart (), atau di tempat lain .. * Pertimbangkan yang harus Anda gunakan

 if(key=="YourKey")

atau kode Anda di dalam blok "// Do Something" akan dijalankan secara salah untuk setiap perubahan yang akan terjadi pada kunci lain di sharedPreferences

Hamed Jaliliani
sumber
1

Jadi, saya tidak tahu apakah ini benar-benar akan membantu siapa pun, itu menyelesaikan masalah saya. Meskipun saya telah menerapkan OnSharedPreferenceChangeListenerseperti yang dinyatakan oleh jawaban yang diterima . Namun, saya masih memiliki ketidakkonsistenan dengan pendengar yang dipanggil.

Saya datang ke sini untuk memahami bahwa Android baru saja mengirimkannya untuk pengumpulan sampah setelah beberapa waktu. Jadi, saya melihat kode saya. Sayang saya, saya belum menyatakan pendengar secara global melainkan di dalam onCreateView. Dan itu karena saya mendengarkan Android Studio memberitahu saya untuk mengubah pendengar menjadi variabel lokal.

Asghar Musani
sumber
0

Masuk akal bahwa pendengar disimpan dalam WeakHashMap.Karena sebagian besar waktu, pengembang lebih suka menulis kode seperti ini.

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

Ini mungkin tampak tidak buruk. Tetapi jika wadah OnSharedPreferenceChangeListeners 'bukan WeakHashMap, itu akan sangat buruk. Jika kode di atas ditulis dalam sebuah Aktivitas. Karena Anda menggunakan kelas dalam non-statis (anonim) yang secara implisit akan memegang referensi instance terlampir. Ini akan menyebabkan kebocoran memori.

Terlebih lagi, jika Anda menyimpan pendengar sebagai bidang, Anda bisa menggunakan registerOnSharedPreferenceChangeListener di awal dan panggil unregisterOnSharedPreferenceChangeListener pada akhirnya. Tetapi Anda tidak dapat mengakses variabel lokal dengan metode di luar cakupannya. Jadi Anda hanya memiliki kesempatan untuk mendaftar tetapi tidak ada kesempatan untuk membatalkan registrasi pendengar. Dengan demikian menggunakan WeakHashMap akan menyelesaikan masalah. Ini adalah cara yang saya rekomendasikan.

Jika Anda menjadikan instance pendengar sebagai bidang statis, ini akan menghindari kebocoran memori yang disebabkan oleh kelas dalam yang tidak statis. Tetapi karena pendengarnya bisa banyak, itu harus terkait dengan contoh. Ini akan mengurangi biaya penanganan panggilan balik onSharedPreferenceChanged .

androidyue
sumber
-3

Saat membaca data yang dapat dibaca Word dibagikan oleh aplikasi pertama, kita harus

Menggantikan

getSharedPreferences("PREF_NAME", Context.MODE_PRIVATE);

dengan

getSharedPreferences("PREF_NAME", Context.MODE_MULTI_PROCESS);

di aplikasi kedua untuk mendapatkan nilai yang diperbarui di aplikasi kedua.

Tapi tetap saja tidak berfungsi ...

shridutt kothari
sumber
Android tidak mendukung mengakses SharedPreferences dari berbagai proses. Melakukan hal itu menyebabkan masalah konkurensi, yang dapat menyebabkan semua preferensi hilang. Juga, MODE_MULTI_PROCESS tidak lagi didukung.
Sam
@Sam, jawaban ini adalah 3 tahun, jadi jangan pilih-pilih, jika tidak bekerja untuk Anda di versi terbaru Android. jawaban waktu ditulis itu adalah pendekatan terbaik untuk melakukannya.
shridutt kothari
1
Tidak, pendekatan itu tidak pernah multi-proses aman, bahkan ketika Anda menulis jawaban ini.
Sam
Sebagaimana @Sam menyatakan, dengan benar, prefs yang dibagikan tidak pernah diproses dengan aman. Juga untuk shridutt kothari - jika Anda tidak suka downvotes hapus jawaban salah Anda (toh itu tidak menjawab pertanyaan OP). Namun, Jika Anda masih ingin menggunakan prefs yang dibagikan dalam proses yang aman, Anda perlu membuat abstraksi yang aman di atasnya yaitu ContentProvider, yang merupakan proses yang aman, dan masih memungkinkan Anda untuk menggunakan preferensi bersama sebagai mekanisme penyimpanan tetap. , Saya sudah melakukan ini sebelumnya, dan untuk dataset kecil / preferensi keluar melakukan sqlite dengan margin yang adil.
Mark Keen
1
@MarkKeen Ngomong-ngomong, saya benar-benar mencoba menggunakan penyedia konten untuk ini dan penyedia konten cenderung gagal secara acak dalam Produksi karena bug Android, jadi hari ini saya hanya menggunakan siaran untuk menyinkronkan konfigurasi ke proses sekunder sesuai kebutuhan.
Sam