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

麻烦的是,监听器并不总是被调用。它会在前几次更改首选项时起作用,然后在我卸载并重新安装该应用程序之前不再调用它。重新启动应用程序似乎无济于事。

我发现一个邮件列表线程报告了同样的问题,但是没有人真正回答他。我究竟做错了什么?

Answers:


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 2010年

10
这篇文章非常庞大,非常感谢,这可能使我花费了数小时无法调试!
凯文·高丁

太棒了,这就是我所需要的。也很好的解释!
隐身直升机2011年

这已经让我感到痛苦,在阅读这篇文章之前,我不知道发生了什么。谢谢!嘘,Android!
赤裸裸的

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.supernotcallexception的信息,这是android实现所必需的。
塞缪尔

你什么意思?super.onResume()需要使用还是需要使用getPreferenceScreen()?因为我在说正确的地方。cs.dartmouth.edu/~campbell/cs65/lecture05/lecture05.html
Yousha Aleayoub 2016年

我记得在这里阅读过developer.android.com/training/basics/activity-lifecycle/…,请参阅代码中的注释。但顺理成章。但到目前为止,我还没有遇到任何问题。
塞缪尔

非常感谢,在其他地方,我发现的onResume(),并在onPause(),他们注册的方法this,而不是listener,它造成的错误和我能解决我的问题。顺便说一下,这两种方法现在是公开的,不受保护
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);
    }
}

每次按下菜单按钮时,我都会从主Activity中创建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;
}

请注意,在这种情况下,在创建PreferenceActivity之后需要完成OnSharedPreferenceChangeListener的注册,否则不会调用主Activity中的Handler !!!我花了一些甜蜜的时间才意识到...


9

接受的答案将创建一个SharedPreferenceChangeListener每次onResume调用。@Samuel通过使其成为SharedPreferenceListenerActivity类的成员来解决该问题。但是Google此代码实验室中也使用了第三个更直接的解决方案。使您的活动类实现OnSharedPreferenceChangeListener接口并onSharedPreferenceChanged在Activity中进行覆盖,从而有效地使Activity本身成为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中取消注册。
朱纳德

2

寄存器SharedPreferenceChangeListener的Kotlin代码,它检测何时对保存的密钥进行更改:

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

您可以将此代码放在onStart()或其他地方。*考虑必须使用的代码

 if(key=="YourKey")

否则,对于sharedPreferences中任何其他键中发生的每次更改,“ //做某事”块中的代码都会错误地运行


1

所以,我不知道这是否真的可以帮助任何人,它解决了我的问题。即使我已经OnSharedPreferenceChangeListener按照公认的答案进行了实施。不过,我与被调用的听众之间仍然存在矛盾。

我来到这里是为了了解Android会在一段时间后将其发送给垃圾回收。因此,我查看了我的代码。令我感到羞耻的是,我没有将GLOBAL声明为监听器,而是将其声明为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);

在第二个应用程序中获取更新的价值。

但是仍然无法正常工作...


Android不支持从多个进程访问SharedPreferences。这样做会导致并发问题,这可能会导致所有首选项丢失。此外,不再支持MODE_MULTI_PROCESS。
山姆

@Sam这个答案已有3年历史了,如果它在最新版本的android中不起作用,请不要投票。写下答案的时间是这样做的最好方法。
shridutt kothari

1
不,即使您编写了此答案,该方法也永远不会是多进程安全的。
山姆

正如@Sam所指出的,正确的是,共享首选项从不安全。也要对shridutt kothari-如果您不喜欢下降表决,请删除错误的答案(无论如何,它不会回答OP的问题)。但是,如果您仍然希望以过程安全的方式使用共享首选项,则需要在其上方创建一个过程安全的抽象,即ContentProvider,它是过程安全的,并且仍然允许您将共享首选项用作持久性存储机制。 ,我之前已经做过,对于较小的数据集/首选项,sqlite的执行效果还不错。
马克·基恩

1
@MarkKeen顺便说一句,我实际上尝试为此使用内容提供程序,由于Android错误,这些内容提供程序在生产中往往会随机失败,因此这些天我仅使用广播根据需要将配置同步到辅助进程。
山姆
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.