前言:
自 DataStore
被推荐用来取代 SharedPreferences
后,我便将其应用于项目之中。然而在实际使用过程中,却遭遇了严重的问题:一旦发生立即断电重启的情况,数据不仅无法保存,甚至还会出现损坏且无法恢复的状况!这简直如同一场灾难。
通过 Google IssueTracker
进行查询后得知,这个问题自被发现之初至今,已然过去两年有余。从 DataStore
的 1.0.0
版本直至 1.1.0
版本,该问题始终未得到解决,而且官网文档也未公布下一个版本的发布时间,这似乎意味着在短期内此问题都难以得到修复。不过,不得不说 DataStore
与 Flow
和协程配合使用时,所展现出的便利性是极具吸引力的,因此在当前阶段,我也不太愿意对其进行大规模的改动。
问题描述
在项目里,我只是采用了 DataStore
简单的 <Key,Value>
模式来存储用户首选项数据,本想着这样能方便又高效地完成数据存储任务。可谁能料到,在遇到立即断电这种情况时,却出现了严重的问题。不但新的数据没办法存储下来,更糟糕的是,还会致使其他原本正常的数据一并遭到损坏,并且这些损坏的数据根本没办法恢复,实在是让人头疼不已呀。
而且呢,下面相关的使用方法都是原原本本照着官网来操作的,按道理来说不应该出现问题才对,可偏偏就在立即断电重启这样的场景下,还是出现了故障,这着实让人有些无奈和困扰啊。
val Context.userSettingsDataStore by preferencesDataStore("user_settings")
data class UserSettings(val isDark: Boolean)
class UserSettingsRepository(context: Context) {
private val dataStore = context.userSettingsDataStore
private object PreferencesKeys {
val KEY_DARK = booleanPreferencesKey("is_dark")
}
val userSettingsFlow = dataStore.data.catch { ex ->
if (ex is IOException) {
emit(emptyPreferences())
} else {
throw ex
}
}.map {
it[PreferencesKeys.KEY_DARK] ?: false
}
suspend fun setThemeDark(dark: Boolean) {
dataStore.edit {
it[PreferencesKeys.KEY_DARK] = dark
}
}
}
原因分析:
目前对于出现这种问题的原因,我暂时还没能想明白呀。总感觉导致这个问题出现的因素不止一处,可能涉及到多个方面的情况交织在一起了。而且我也向官方反馈了这个情况,可到现在官方都还没有给出任何回复呢,就只能这么干等着,心里实在没底,也不知道什么时候才能把这个棘手的问题给解决掉啊。
解决(临时解决)方案:
经过一番测试后发现,在面对立即断电重启这样的情况时,SharedPreferences
的表现相当稳定,数据既不会丢失,更不会出现损坏的情况。基于这个测试结果,我琢磨出了一个思路,那就是在使用 DataStore
进行数据存储的同时,也另外存储一份相同的数据到 SharedPreferences
当中。如此一来,等到下次启动应用的时候,就可以先从 SharedPreferences
里读取数据,然后再把这些数据重新写入到 DataStore
里面去。
可能有人会问了,既然都已经回过头去用 SharedPreferences
了,那干嘛还非要执着于使用 DataStore
呢?其实啊,重点就在于 DataStore
配合 Flow
来对流式监听数据变化这一功能真的是太好用了,仅凭这一点,就让我对 DataStore
依旧抱有一丝希望,盼着官方能够尽快修复它存在的这个问题呀。
以下就是我目前想到的临时解决办法:
我新建了一个名为 DataStoreBackup
的类,用它来替换掉原来 DataStore
的 edit
和 updateData
方法。在创建 DataStore
单例的时候呢,会从 SharedPreferences
中重新读取数据,通过这样的方式来尽量保证数据的完整性以及应用在应对断电重启等情况时的稳定性,虽然只是个临时举措,但也算是目前能想到的比较可行的办法了。
import android.content.Context
import androidx.annotation.GuardedBy
import androidx.datastore.core.DataMigration
import androidx.datastore.core.DataStore
import androidx.datastore.core.handlers.ReplaceFileCorruptionHandler
import androidx.datastore.preferences.core.MutablePreferences
import androidx.datastore.preferences.core.PreferenceDataStoreFactory
import androidx.datastore.preferences.core.Preferences
import androidx.datastore.preferences.core.booleanPreferencesKey
import androidx.datastore.preferences.core.edit
import androidx.datastore.preferences.core.emptyPreferences
import androidx.datastore.preferences.core.floatPreferencesKey
import androidx.datastore.preferences.core.intPreferencesKey
import androidx.datastore.preferences.core.longPreferencesKey
import androidx.datastore.preferences.core.mutablePreferencesOf
import androidx.datastore.preferences.core.stringPreferencesKey
import androidx.datastore.preferences.core.stringSetPreferencesKey
import androidx.datastore.preferences.preferencesDataStoreFile
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import kotlin.properties.ReadOnlyProperty
import kotlin.reflect.KProperty
fun preferencesDataStoreAndBackup(
name: String,
corruptionHandler: ReplaceFileCorruptionHandler<Preferences>? =
ReplaceFileCorruptionHandler {
it.printStackTrace()
emptyPreferences()
},
produceMigrations: (Context) -> List<DataMigration<Preferences>> = { listOf() },
scope: CoroutineScope = CoroutineScope(Dispatchers.IO + SupervisorJob())
): ReadOnlyProperty<Context, DataStoreBackup> {
return PreferenceDataStoreBackupSingletonDelegate(
name,
corruptionHandler,
produceMigrations,
scope
)
}
class DataStoreBackup(
context: Context,
name: String,
private val dataStore: DataStore<Preferences>
) {
private val sp by lazy {
context.getSharedPreferences(name, Context.MODE_PRIVATE)
}
val data get() = dataStore.data
suspend fun edit(
transform: suspend (MutablePreferences) -> Unit
) {
this.updateData(transform)
}
suspend fun updateData(transform: suspend (MutablePreferences) -> Unit) {
dataStore.updateData {
editBackup(transform)
it.toMutablePreferences().apply {
transform.invoke(this)
}
}
}
private suspend fun editBackup(transform: suspend (MutablePreferences) -> Unit) {
val newData = mutablePreferencesOf()
transform.invoke(newData)
withContext(Dispatchers.IO) {
val editor = sp.edit()
newData.asMap().keys.forEach {
val key = it.name
when (val value = newData[it]) {
is Boolean -> {
editor.putBoolean(key, value)
}
is Long -> {
editor.putLong(key, value)
}
is Int -> {
editor.putInt(key, value)
}
is Float -> {
editor.putFloat(key, value)
}
is String -> {
editor.putString(key, value)
}
is Set<*> -> {
@Suppress("UNCHECKED_CAST")
editor.putStringSet(key, value as? Set<String> ?: emptySet())
}
}
}
editor.commit()
}
}
}
internal class PreferenceDataStoreBackupSingletonDelegate internal constructor(
private val name: String,
private val corruptionHandler: ReplaceFileCorruptionHandler<Preferences>? = null,
private val produceMigrations: (Context) -> List<DataMigration<Preferences>>,
private val scope: CoroutineScope
) : ReadOnlyProperty<Context, DataStoreBackup> {
private val lock = Any()
@GuardedBy("lock")
@Volatile
private var INSTANCE: DataStoreBackup? = null
/**
* Gets the instance of the DataStore.
*
* @param thisRef must be an instance of [Context]
* @param property not used
*/
override fun getValue(thisRef: Context, property: KProperty<*>): DataStoreBackup {
return INSTANCE ?: synchronized(lock) {
if (INSTANCE == null) {
val applicationContext = thisRef.applicationContext
val backupFileName = name + "_backup"
val dataStore = PreferenceDataStoreFactory.create(
corruptionHandler = corruptionHandler,
migrations = produceMigrations(applicationContext),
scope = scope
) {
applicationContext.preferencesDataStoreFile(name)
}
scope.launch(Dispatchers.IO) {
val map = readBackupSharedPreferences(applicationContext, backupFileName)
dataStore.edit {
restorePreferencesFromBackup(map, it)
}
}
INSTANCE = DataStoreBackup(applicationContext, backupFileName, dataStore)
}
INSTANCE!!
}
}
private suspend fun readBackupSharedPreferences(
appContext: Context,
name: String
): Map<String, *> {
return withContext(Dispatchers.IO) {
try {
val sp = appContext.getSharedPreferences(
name,
Context.MODE_PRIVATE
)
sp.all
} catch (e: Throwable) {
emptyMap()
}
}
}
private fun restorePreferencesFromBackup(
map: Map<String, *>,
mutablePreferences: MutablePreferences
) {
map.keys.forEach { key ->
when (val value = map[key]) {
is Boolean -> mutablePreferences[
booleanPreferencesKey(key)
] = value
is Float -> mutablePreferences[
floatPreferencesKey(key)
] = value
is Int -> mutablePreferences[
intPreferencesKey(key)
] = value
is Long -> mutablePreferences[
longPreferencesKey(key)
] = value
is String -> mutablePreferences[
stringPreferencesKey(key)
] = value
is Set<*> -> {
@Suppress("UNCHECKED_CAST")
mutablePreferences[
stringSetPreferencesKey(key)
] = value as Set<String>
}
}
}
}
}
使用示例:
val Context.userSettingsDataStore by preferencesDataStoreAndBackup("user_settings")
data class UserSettings(val isDark: Boolean)
class UserSettingsRepository(context: Context) {
private val dataStore = context.userSettingsDataStore
private object PreferencesKeys {
val KEY_DARK = booleanPreferencesKey("is_dark")
}
val userSettingsFlow = dataStore.data.catch { ex ->
if (ex is IOException) {
emit(emptyPreferences())
} else {
throw ex
}
}.map {
it[PreferencesKeys.KEY_DARK] ?: false
}
suspend fun setThemeDark(dark: Boolean) {
dataStore.edit {
it[PreferencesKeys.KEY_DARK] = dark
}
}
}
没错,代码方面的改动并不大呢。仅仅是把原本使用的 preferencesDataStore
替换成 preferencesDataStoreAndBackup
就行了,操作起来还挺简单的。快去测试一下,看看在经历断电重启这种情况后,数据到底能不能够成功存储,希望这个临时的解决办法能够帮你到你呢。
总结:
这种解决办法呢,确实存在一些缺点。
先说缺点的方面吧,它会导致双倍的存储时间,毕竟要同时往 DataStore
和 SharedPreferences
里存储数据呀,这无疑增加了数据存储所耗费的时长。不过好在它不会阻塞 UI
,无论是读取数据还是写入数据,都是在协程中完成的,所以在操作过程中,用户界面不会出现卡顿之类的糟糕体验,这一点还是比较让人欣慰的。
而说到优点嘛,暂时还真没怎么发现呢,也不确定它到底有没有其他突出的优势,目前来看,它最大的作用就是解决了在立即断电重启的场景下数据无法存储的问题,从这个角度讲,也算是达到了我想要的最基本的效果了。
真心希望官方能够早点推出优化后的版本呀,这样就不用再采用这种临时的、略显笨拙的解决办法了。要是路过的大神们察觉到这个办法存在什么问题,还请不吝赐教呀,我就是个小白,很多地方还不太懂,要是能得到大家的指点,那可就太幸运了。