SharedPreferences 存储难题:重启后数据丢失?
2025-01-17 17:12:54
SharedPreferences 存储问题排查:重启后无法获取预期值
在使用 SharedPreferences 进行应用配置或数据持久化时,开发者有时会遇到这样的困扰:应用重启后,无法读取到之前写入的某些键值。问题常见表现是,保存到 SharedPreferences 的状态变量在重启后恢复为默认值,而其它变量可能读取正常。本文将分析可能造成这种现象的原因,并给出对应的解决方法。
问题分析:存储行为的微妙之处
这种“间歇性”存储失败的问题通常不会立刻显现,并且在调试过程中不易发现,所以它才尤其需要重视。可能影响 SharedPreferences 写入操作的因素有几个:
- 不恰当的存储时机 : SharedPreferences 的
commit()
方法是同步执行的,会阻塞 UI 线程,apply()
是异步执行的。如果在UI线程频繁调用commit()
,特别是在UI界面需要大量更新时,可能会导致阻塞和性能问题,数据甚至丢失。同时异步的apply
执行的时间也不确定,在应用程序强制停止时,写入数据还没完成,就有可能丢失。 - 上下文不匹配 :
SharedPreferences
可以通过不同的 context 创建和访问, 如果使用不一致的context进行写入和读取,将会得到不同的结果。通常使用应用级别的 context 是更好的选择。 - 存储值未生效 : 有时候看似执行了存储操作,但值并未真正生效,常见原因如没有 commit 或 apply,或者 commit 之前值被覆盖。 另外一些比较细节的点也要考虑,例如多线程对共享资源的竞争等。
解决方法
针对上述问题,我们可以尝试以下几个解决方案。
方案一:使用 apply()
代替 commit()
apply()
是异步操作,在执行完数据存储后返回,避免了阻塞 UI 线程。apply()
方法会将数据提交到内存,并在后台完成写入磁盘的操作。这样既可以保证用户体验,也能相对降低数据丢失的风险。但务必知晓其异步特性,依赖立即写入完成的业务需要使用commit。
// 使用 apply() 异步保存数据
pref.edit()
.putBoolean(pref.biometric_enable, isOn)
.apply();
操作步骤:
- 找到所有调用
commit()
的地方。 - 将其替换为
apply()
。 - 测试修改后的代码,查看应用重启后数据是否能正确保存。
代码示例 (Kotlin)
toggle_biometric.setOnToggledListener { toggleableView, isOn ->
pref.setPrefBoolean(applicationContext, pref.biometric_enable, isOn) //内部实现为 edit().putBoolean().apply()
biometricLogin = isOn
Toast.makeText(this@HomeActivity, "Biometric Login: $biometricLogin", Toast.LENGTH_SHORT).show()
}
override fun onNavigationItemSelected(item: MenuItem): Boolean {
when (item.itemId) {
R.id.nav_biometric -> {
val isChecked = !toggle_biometric.isOn
pref.edit().putBoolean(pref.biometric_enable,isChecked).apply() // 修正代码, 使用apply,且不在视图未修改前就使用之前的数据进行设置
toggle_biometric.isOn = isChecked
if(isChecked) {
biometricLogin = isChecked
}
else {
biometricLogin = isChecked
}
}
}
drawerLayout.closeDrawer(GravityCompat.START)
return true
}
override fun onResume() {
super.onResume()
toggle_biometric.isOn = pref.getPrefBoolean(applicationContext , pref.biometric_enable)
}
在这个示例中,使用了 apply()
来保存状态。请注意,对 toggle_biometric.isOn
的更改要写在SharedPreferences修改逻辑的后面。另外在 onNavigationItemSelected
方法中,之前是在 toggle_biometric
的 setOnToggledListener
回调方法中处理 SharedPreferences 的修改逻辑,然后在 onNavigationItemSelected
中又用之前的值设置 toggle_biometric
, 导致修改被覆盖,所以修正后将 toggle_biometric
的状态更新放到了写入 SharedPreferences 后。 onResume()
中加载状态时也会避免覆盖更新的数据,保证了存储和读取的逻辑统一。
方案二:确保使用正确的 Context
为了确保读取和写入操作使用的是同一份 SharedPreferences 文件,要使用应用程序级别的 context
对象。这能够避免由于 context 对象差异导致数据读取和写入不到相同位置。
代码示例 (Kotlin)
val appContext = applicationContext //获取应用程序context
toggle_biometric.setOnToggledListener { toggleableView, isOn ->
pref.setPrefBoolean(appContext, pref.biometric_enable, isOn)
biometricLogin = isOn
Toast.makeText(this@HomeActivity, "Biometric Login: $biometricLogin", Toast.LENGTH_SHORT).show()
}
override fun onNavigationItemSelected(item: MenuItem): Boolean {
when (item.itemId) {
R.id.nav_biometric -> {
val isChecked = !toggle_biometric.isOn
pref.edit().putBoolean(pref.biometric_enable,isChecked).apply()
toggle_biometric.isOn = isChecked
if(isChecked) {
biometricLogin = isChecked
}
else {
biometricLogin = isChecked
}
}
}
drawerLayout.closeDrawer(GravityCompat.START)
return true
}
override fun onResume() {
super.onResume()
toggle_biometric.isOn = pref.getPrefBoolean(appContext, pref.biometric_enable)
}
操作步骤:
- 创建并维护一个全局
Application
对象或者全局context对象。 - 在读取和写入 SharedPreferences 时,都使用该全局对象。
原理:
这样能够保证整个应用的SharedPreferences操作都是作用在同一个文件。使用 applicationContext
保证无论 activity 是否存在,存储都可以发生。
方案三:立即更新视图与数据
在上面的代码中,UI 元素的更新要和数据同步更新。 如果UI上未变化前,使用之前的数据去更新数据,可能会覆盖上次的修改,尤其在界面变化过程中。
操作步骤:
- 数据更改,视图更新之后再调用 SharedPreferences 操作
- 读取 SharedPreferences 值设置给 view
额外建议
- 错误处理 : 在数据保存与加载的任何阶段添加适当的异常处理。这有助于在出现意外问题时及时捕获并分析原因,提升应用程序的稳定性。
- 同步操作需谨慎 : 如果需要使用同步的
commit()
操作,请尽可能降低其执行频率,避免UI阻塞,可以在专门的工作线程中执行同步写操作。 - 测试覆盖 : 为了覆盖数据存储方面的错误,应该进行较为全面的测试。比如:重复的应用程序强制停止和启动操作来验证数据的持久性。
- 数据加密 : 如果
SharedPreferences
存储敏感信息,比如用户登录的凭证,要使用数据加密来加强安全性。Android 提供一些API方便开发者加密 SharedPreferences 数据。
通过这些细致的检查和代码修改,大部分因SharedPreferences导致的数据存储问题可以迎刃而解,保证了应用的稳定性,和数据的可靠性。