SharedPreferences.onSharedPreferenceChangeListener不一致地未被调用

295

我像这样在我的主活动的onCreate()中注册了一个偏好更改监听器:

SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(this);

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

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

问题在于监听器并不总是被调用。它在首次更改首选项时有效,然后在卸载和重新安装应用之前不再被调用。无论重启应用程序多少次都无法解决。

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

8个回答

680
这是一个棘手的问题。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 文档已经 更新,关于这种行为发出了警告。因此,异常行为仍然存在。但现在已经有文档记录了。

35
这让我备受折磨,我以为自己要疯了。谢谢你发布这个解决方案! - Brad Hein
我已经按照您回答@Blanka的方式做了,但是在关闭应用程序后,监听器没有起作用。我没有在onDestroy()方法中注销监听器。 - Piyush Agarwal
没有任何数量的重新启动应用程序似乎可以修复它:您能否评论一下问题的这部分?重新启动应用程序不会调用主活动的onCreate(),因此再次向地图添加侦听器吗? - Mr_and_Mrs_D
8
非常好的答案,谢谢。这个问题肯定应该在文档中提到。https://code.google.com/p/android/issues/detail?id=48589 - Andrey Chernih
非常感谢!!!我已经坐了几个小时,一直在琢磨这个问题!你真是我的救星!! - Payerl
显示剩余2条评论

25

这个被接受的答案可以,对我来说它每次恢复活动时都会创建新实例

那么在活动中保留对监听器的引用如何?

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

还有在你的onResume和onPause中

@Override     
public void onResume() {
    super.onResume();          
    getPreferenceScreen().getSharedPreferences().registerOnSharedPreferenceChangeListener(listener);     
}

@Override     
public void onPause() {         
    super.onPause();          
    getPreferenceScreen().getSharedPreferences().unregisterOnSharedPreferenceChangeListener(listener);

}

这将非常类似于您正在做的内容,除了我们保持一个硬引用。


为什么在调用 getPreferenceScreen()... 之前使用了 super.onResume() - Yousha Aleayoub
@YoushaAleayoub,请阅读有关 android.app.supernotcalledexception 的内容,它是 Android 实现所必需的。 - Samuel
你的意思是什么?使用super.onResume()是必需的还是在getPreferenceScreen()之前使用它是必需的?因为我正在谈论正确的位置。http://cs.dartmouth.edu/~campbell/cs65/lecture05/lecture05.html - Yousha Aleayoub
我记得在这里阅读过:http://developer.android.com/training/basics/activity-lifecycle/pausing.html#Resume,看看代码中的注释。但把它放在尾部是合理的。但到目前为止,我还没有遇到任何问题。 - Samuel
非常感谢,我在其他地方找到的onResume()和onPause()方法都是注册了this而不是listener,这导致了错误,但我已经解决了我的问题。顺便说一下,这两个方法现在是公共的,而不是受保护的。 - Nicolas

23

被接受的答案每次调用onResume时都会创建一个SharedPreferenceChangeListener. @Samuel通过将SharedPreferenceListener设为Activity类的成员来解决这个问题。但是还有第三个更简单的解决方案,Google也在这个代码实验室中使用了该方案。使您的活动类实现OnSharedPreferenceChangeListener接口,并在活动中覆盖onSharedPreferenceChanged, 从而使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 中取消注册。 - Junaed
1
如果您正在启动新活动,则此官方方法将无法使用,因为它将停止当前活动并注销监听器,例如,从另一个应用程序选择文件的意图操作。在这种情况下,使用 onDestroy 而不是 onStop/onPause 将起作用。 - convers39

16

作为该主题最详细的页面,我想要补充我的看法。

我的问题是OnSharedPreferenceChangeListener没有被调用。我的SharedPreferences是通过主Activity启动时获取的:

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

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


3

Kotlin代码用于注册SharedPreferenceChangeListener,它可以检测保存的键上即将发生的更改:

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

你可以将此代码放在onStart()中,或者其他任何地方。 *请注意,您必须使用

 if(key=="YourKey")

否则,如果sharedPreferences中的任何其他键发生更改,您在“//Do Something”块内的代码将错误地运行。


2

所以,我不知道这是否真的会帮助任何人,但它解决了我的问题。尽管我已经按照被接受的答案中所述实现了OnSharedPreferenceChangeListener,但我仍然存在监听器调用不一致的问题。

我来到这里想要理解Android在一段时间后将其发送给垃圾收集器。因此,我查看了我的代码。可耻的是,我没有全局声明该监听器,而是在onCreateView内部声明。这是因为我听从了Android Studio告诉我将监听器转换为本地变量。


如果您在OnCreate/OnStart/OnResume中注册并在OnDestroy/OnStop/OnPause中注销,则不会提示您将其转换为本地变量,因此可以在两个不同的方法中使用它。我可能对此有所错误,但是您注册/注销的方法取决于您是否希望侦听器在打开另一个活动时保持活动状态。例如,如果您有某种SettingsActivity供用户更改其用户名,并且您希望MainActivity立即识别更改,而不是将值返回给MainActivity。 - Literate Corvette

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
@Sam 这个答案已经三年了,请不要在最新版本的安卓中对其进行贬低,如果它对你没有用。当时编写答案时,这是最好的方法。 - Shridutt Kothari
1
不,即使在您撰写此回答时,该方法也从未具有多进程安全性。 - Sam
正如@Sam所说,shared prefs从来都不是进程安全的。另外,对于shridutt kothari-如果你不喜欢被downvotes,那就删除你的错误答案(它根本没有回答OP的问题)。然而,如果你仍然想以进程安全的方式使用shared prefs,你需要创建一个进程安全的抽象层,即ContentProvider,它是进程安全的,并且仍然允许你使用shared preferences作为持久化存储机制。我以前做过这个,对于小数据集/首选项,它比sqlite表现得更好。 - Mark
1
顺便说一下,我实际上尝试过使用内容提供者来解决这个问题,但是由于Android的bug,这些内容提供者在生产环境中往往会随机失败,所以现在我只是使用广播将配置同步到需要的辅助进程。 - Sam
@Sam 很高兴知道 - 我确实在内容提供程序周围进行了一些仪器测试,并且似乎工作正常,但正如你所说 - 当你进入长期使用的生产环境时,奇怪的事情可能会发生! - Mark

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接