SharedPreferences.onSharedPreferenceChangeListenerが一貫して呼び出されない


267

私はこのような設定変更リスナーを(onCreate()私のメインアクティビティの)に登録しています:

SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(this);

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

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

問題は、リスナーが常に呼び出されるとは限らないことです。設定が変更された最初の数回は機能し、アプリをアンインストールして再インストールするまで呼び出されなくなります。アプリケーションを再起動しても、問題は解決しないようです。

私は同じ問題を報告するメーリングリストのスレッドを見つけましたが、誰も実際には彼に答えていません。何が悪いのですか?

回答:


612

これは卑劣なものです。SharedPreferencesは、リスナーをWeakHashMapに保持します。つまり、匿名の内部クラスは、現在のスコープを離れるとすぐにガベージコレクションのターゲットになるため、リスナーとして使用することはできません。最初は機能しますが、最終的にはガベージコレクションが行われ、WeakHashMapから削除されて機能しなくなります。

クラスのフィールドにリスナーへの参照を保持してください。クラスインスタンスが破棄されていなければ問題ありません。

つまり、代わりに:

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

これを行う:

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

onDestroyメソッドで登録を解除することで問題が解決する理由は、そのためにはリスナーをフィールドに保存する必要があり、そのため問題が発生しないためです。これは、問題を修正するフィールドにリスナーを保存することであり、onDestroyでの登録解除ではありません。

更新:Androidのドキュメントが更新され、この動作に関する警告が出されました。したがって、奇妙な振る舞いは残ります。しかし今、それは文書化されています。


20
これは私を殺していた、私は私の心を失っていると思った。このソリューションを投稿していただきありがとうございます。
Brad Hein

10
この投稿は巨大です。ありがとうございました。これにより、デバッグに何時間もかかる可能性があります。
Kevin Gaudin、2010年

すごい、これは私が必要とするものだけです。良い説明も!
ステルスコプター、2011年

これはすでに私に噛み付き、この投稿を読むまでは何が起きているのかわかりませんでした。ありがとうございました!ブー、アンドロイド!
裸の

5
すばらしい回答、ありがとうございます。間違いなくドキュメントで言及する必要があります。code.google.com/p/android/issues/detail?id=48589
Andrey Chernih 2013年

16

私はそれが活動が再開するたびに新しいインスタンスを作成しているので、この受け入れられた答えは大丈夫です

だから、アクティビティ内でリスナーへの参照を維持するのはどうですか

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

そしてあなたのonResumeとonPauseで

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



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

}

これは、ハード参照を維持していることを除いて、あなたがしていることと非常に似ています。


super.onResume()以前使用した理由はgetPreferenceScreen()...
Yousha Aleayoub 16

@ YoushaAleayoub、android.app.supernotcalledexceptionについて読んでください。これは、androidの実装に必要です。
Samuel

どういう意味ですか?使用super.onResume()が必要getPreferenceScreen()ですか、それとも使用前に使用する必要がありますか?私は正しい場所について話しているので。cs.dartmouth.edu/~campbell/cs65/lecture05/lecture05.html
Yousha Aleayoub

ここでdeveloper.android.com/training/basics/activity-lifecycle/…を読んだことを覚えています。コード内のコメントを参照してください。しかし、それを尾に置くことは論理的です。今まで私はこれで何の問題にも直面していません。
Samuel

おかげさまで、登録thisしていないonResume()およびonPause()メソッドを見つけた他のすべての場所でlistenerエラーが発生し、問題を解決できました。ところで、これらの2つの方法は現在公開されており、保護されていません
Nicolas

16

これがトピックの最も詳細なページなので、50ctを追加します。

OnSharedPreferenceChangeListenerが呼び出されないという問題がありました。SharedPreferencesは、メインアクティビティの開始時に次の方法で取得されます。

prefs = PreferenceManager.getDefaultSharedPreferences(this);

私のPreferenceActivityコードは短く、設定を表示する以外は何もしません:

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

メニューボタンを押すたびに、メインアクティビティからPreferenceActivityを作成します。

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

OnSharedPreferenceChangeListenerを登録すると、この場合にはPreferenceActivityを作成した後に行われる必要があり、他の主要な活動のハンドラが呼び出されることはありませんことを!それを実感するのに少し甘い時間がかかりました...


9

受け入れられた回答は、呼び出されるSharedPreferenceChangeListenerたびに作成さonResumeれます。@SamuelSharedPreferenceListenerは、Activityクラスのメンバーにすることで解決します。しかし、Googleこのコードラボでも使用する3番目の簡単なソリューションがあります。アクティビティクラスにOnSharedPreferenceChangeListenerインターフェイスを実装させonSharedPreferenceChanged、アクティビティでオーバーライドして、アクティビティ自体を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
まさに、これはそれであるはずです。インターフェースを実装し、onStartに登録し、onStopにunRegisterします。
Junaed、

2

保存されたキーで変更が発生するタイミングを検出する登録SharedPreferenceChangeListenerのKotlinコード:

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

このコードをonStart()または他のどこかに置くことができます。*使用する必要があることを考慮してください

 if(key=="YourKey")

または、「// Do Something」ブロック内のコードは、sharedPreferencesの他のキーで発生するすべての変更に対して誤って実行されます


1

ですから、これが本当に役立つかどうかはわかりませんが、問題は解決しました。受け入れられた回答でOnSharedPreferenceChangeListener述べられているとおりに私は実装しましたが。それでも、呼び出されるリスナーとの不一致がありました。

ここに来て、Androidはしばらくしてガベージコレクションのためにそれを送信するだけであることを理解しました。だから、私は自分のコードを見ました。残念なことに、私はリスナーをグローバルに宣言していませんでしたonCreateView。これは、Android Studioがリスナーをローカル変数に変換するように指示するのを聞いたためです。


0

リスナーがWeakHashMapに保持されることは理にかなっています。ほとんどの場合、開発者はこのようなコードを記述することを好みます。

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

これは悪くないように見えるかもしれません。しかし、OnSharedPreferenceChangeListenersのコンテナーがWeakHashMapでない場合、それは非常に悪いことになります。上記のコードがActivityで記述されている場合。包含インスタンスの参照を暗黙的に保持する非静的(匿名)内部クラスを使用しているため。これにより、メモリリークが発生します。

さらに、リスナーをフィールドとして保持する場合は、最初にregisterOnSharedPreferenceChangeListenerを使用し、最後にunregisterOnSharedPreferenceChangeListenerを呼び出すことができます。しかし、スコープ外のメソッドでローカル変数にアクセスすることはできません。したがって、登録する機会はありますが、リスナーの登録を解除する機会はありません。したがって、WeakHashMapを使用すると問題が解決します。これが私がお勧めする方法です。

リスナーインスタンスを静的フィールドとして作成すると、非静的内部クラスによって引き起こされるメモリリークが回避されます。ただし、リスナーは複数になる可能性があるため、インスタンスに関連する必要があります。これにより、onSharedPreferenceChangedコールバックの処理コストが削減されます。


-3

最初のアプリで共有されているWordで読み取り可能なデータを読み取りながら、

交換する

getSharedPreferences("PREF_NAME", Context.MODE_PRIVATE);

getSharedPreferences("PREF_NAME", Context.MODE_MULTI_PROCESS);

2番目のアプリで更新された値を取得します。

しかし、それでも動作しません...


Androidは、複数のプロセスからSharedPreferencesへのアクセスをサポートしていません。これを行うと同時実行性の問題が発生し、すべての設定が失われる可能性があります。また、MODE_MULTI_PROCESSはサポートされなくなりました。
サム

@Samこの回答は3年前のものです。最新バージョンのAndroidで機能しない場合は、投票しないでください。答えが書かれた時間はそれを行うための最良のアプローチでした。
shridutt kothari 2017年

1
いいえ、あなたがこの答えを書いても、そのアプローチは決してマルチプロセス安全ではありませんでした。
サム

@Samが述べているように、正しく、共有設定はプロセスセーフではありませんでした。また、kothariをshriduttする場合-反対票が気に入らない場合は、誤った回答を削除してください(とにかくOPの質問には回答しません)。ただし、プロセスセーフな方法で共有設定を引き続き使用する場合は、その上にプロセスセーフな抽象化を作成する必要があります。つまり、プロセスセーフであり、永続化されたストレージメカニズムとして共有設定を使用できるContentProviderです。 、私は以前にこれを行ったことがあり、小さなデータセット/設定では、かなりのマージンでsqliteを実行します。
マークキーン

1
@MarkKeenちなみに、実際にこれにコンテンツプロバイダーを使用してみましたが、コンテンツプロバイダーはAndroidのバグのためにプロダクションでランダムに失敗する傾向があったので、最近は必要に応じてブロードキャストを使用して設定をセカンダリプロセスに同期しています。
サム
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.