Android使用DataStore保存数据之后断电重启设备数据丢失临时解决办法

news2025/1/3 14:18:18

前言:

DataStore 被推荐用来取代 SharedPreferences 后,我便将其应用于项目之中。然而在实际使用过程中,却遭遇了严重的问题:一旦发生立即断电重启的情况,数据不仅无法保存,甚至还会出现损坏且无法恢复的状况!这简直如同一场灾难。

通过 Google IssueTracker 进行查询后得知,这个问题自被发现之初至今,已然过去两年有余。从 DataStore1.0.0 版本直至 1.1.0 版本,该问题始终未得到解决,而且官网文档也未公布下一个版本的发布时间,这似乎意味着在短期内此问题都难以得到修复。不过,不得不说 DataStoreFlow 和协程配合使用时,所展现出的便利性是极具吸引力的,因此在当前阶段,我也不太愿意对其进行大规模的改动。


问题描述

在项目里,我只是采用了 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 的类,用它来替换掉原来 DataStoreeditupdateData 方法。在创建 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 就行了,操作起来还挺简单的。快去测试一下,看看在经历断电重启这种情况后,数据到底能不能够成功存储,希望这个临时的解决办法能够帮你到你呢。


总结:

这种解决办法呢,确实存在一些缺点。
先说缺点的方面吧,它会导致双倍的存储时间,毕竟要同时往 DataStoreSharedPreferences 里存储数据呀,这无疑增加了数据存储所耗费的时长。不过好在它不会阻塞 UI,无论是读取数据还是写入数据,都是在协程中完成的,所以在操作过程中,用户界面不会出现卡顿之类的糟糕体验,这一点还是比较让人欣慰的。
而说到优点嘛,暂时还真没怎么发现呢,也不确定它到底有没有其他突出的优势,目前来看,它最大的作用就是解决了在立即断电重启的场景下数据无法存储的问题,从这个角度讲,也算是达到了我想要的最基本的效果了。
真心希望官方能够早点推出优化后的版本呀,这样就不用再采用这种临时的、略显笨拙的解决办法了。要是路过的大神们察觉到这个办法存在什么问题,还请不吝赐教呀,我就是个小白,很多地方还不太懂,要是能得到大家的指点,那可就太幸运了。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/2268627.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

VectorCAST入门指导

文章目录 1. VectorCAST 概述2. 启动 VectorCAST(Windows用户)2. 故障排除3. VectorCAST 界面4. 创建一个 VectorCAST 项目5. 设置工作目录6. 创建一个新项目7. 添加一个单元测试环境8. 添加测试用例9. 执行所有测试1. VectorCAST 概述 VectorCAST是一套测试自动化工具: Vecto…

12.30 linux 文件操作,磁盘分区挂载

ubuntu 在linux 对文件的相关操作【压缩&#xff0c;打包&#xff0c;软链接&#xff0c;文件权限】【head&#xff0c;tail&#xff0c;管道符&#xff0c;通配符&#xff0c;find&#xff0c;grep&#xff0c;cut等】脑图-CSDN博客 1.文件操作 在家目录下创建目录文件&#…

Zabbix 监控平台 添加监控目标主机

Zabbix监控平台是一个企业级开源解决方案&#xff0c;用于分布式系统监视和网络监视。它由Zabbix Server和可选组件Zabbix Agent组成&#xff0c;通过C/S模式&#xff08;客户端-服务器模型&#xff09;采集数据&#xff0c;并通过B/S模式&#xff08;浏览器-服务器模型&#x…

第10章 初等数论

2024年12月27日一稿&#xff08;P341&#xff09; 2024年12月28日二稿 2024年12月29日三稿 当命运这扇大门向你打开的时候&#xff0c;不要犹豫和害怕&#xff0c;一直往前跑就是了&#xff01; 10.1 素数 这里写错了&#xff0c;不能整除应该表示为 10.2 最大公约数与最小公…

R语言6种将字符转成数字的方法,写在新年来临之际

咱们临床研究中&#xff0c;拿到数据后首先要对数据进行清洗&#xff0c;把数据变成咱们想要的格式&#xff0c;才能进行下一步分析&#xff0c;其中数据中的字符转成数字是个重要的内容&#xff0c;因为字符中常含有特殊符号&#xff0c;不利于分析&#xff0c;转成数字后才能…

微信流量主挑战:三天25用户!功能未完善?(新纪元4)

&#x1f389;【小程序上线第三天&#xff01;突破25用户大关&#xff01;】&#x1f389; 嘿&#xff0c;大家好&#xff01;今天是我们小程序上线的第三天&#xff0c;我们的用户量已经突破了25个&#xff01;昨天还是16个&#xff0c;今天一觉醒来竟然有25个&#xff01;这涨…

Ps:将数据组作为文件导出

Ps菜单&#xff1a;文件/导出/数据组作为文件 Export/Data Sets as Files “将数据组作为文件导出” Export Data Sets as Files命令是 Photoshop 数据驱动设计功能的一部分&#xff0c;用于结合可变数据和模板&#xff0c;生成多个文件。 1、自动化批量生成 适用于名片、证书、…

Java基本操作笔记

命令行快速进入指定文件夹 快速切换进入指定文件 文件夹快速切换 idea开发步骤 快捷键 修改模块 选中模块右键依次选择 选择第三个修改模块和文件夹的名称 输入修改后的名字回车 导入模块 找到要导入的模块&#xff0c;ctrc复制该文件夹 打开idea找到工程文件夹ctrv粘贴 点击o…

OpenCV-Python实战(8)——图像变换

一、缩放 cv2.resize() img cv2.resize(src*,dsize*,fx*,fy*,interpolation*) img&#xff1a;目标图像。 src&#xff1a;原始图像。 dsize&#xff1a;&#xff08;width&#xff0c;height&#xff09;图像大小。 fx、fy&#xff1a;可选参数&#xff0c;水平/垂直方向…

Spring thymeleaf 的快速默认搭建使用

Spring thymeleaf 的快速默认搭建使用 thymeleaf 的搭建Pom 文件 thymeleaf 的使用Controller返回参数String资源文件路径访问端点显示HTML页面 thymeleaf 的搭建 Pom 文件 Pom 文件引入 spring-boot-starter-thymeleaf 依赖 <dependency><groupId>org.springfra…

Linux | 零基础Ubuntu搭建JDK

目录 软件简介 在线文档 压缩包安装 下载地址 补:传输软件 传输等待 目录结构 解压安装 配置环境 更新环境 测试JDK结果 APT安装 软件简介 Java Development Kit (JDK) 是 Sun 公司&#xff08;已被 Oracle 收购&#xff09;针对 Java 开发员的软件开发工具包。自…

揭秘 Fluss 架构组件

这是 Fluss 系列的第四篇文章了&#xff0c;我们先回顾一下前面三篇文章主要说了哪些内容。 Fluss 部署&#xff0c;带领大家部署Fluss 环境&#xff0c;体验一下 Fluss 的功能Fluss 整合数据湖的操作&#xff0c;体验Fluss 与数据湖的结合讲解了 Fluss、Kafka、Paimon 之间的…

PyQt的介绍

举例 解释 一 PyQt是什么 PyQt 是 Python 编程语言的一个库&#xff0c;它是 Qt 库的 Python 绑定。Qt 是一个跨平台的图形用户界面&#xff08;GUI&#xff09;开发框架&#xff0c;广泛应用于开发桌面应用程序。PyQt 使得 Python 开发者能够利用 Qt 框架的强大功能来创建图…

数据库高安全—openGauss安全整体架构安全认证

openGauss作为新一代自治安全数据库&#xff0c;提供了丰富的数据库基础安全能力&#xff0c;并逐步完善各类高阶安全能力。这些安全能力涵盖了访问登录认证、用户权限管理、审计与追溯及数据安全隐私保护等。本章节将围绕openGauss安全机制进行源码解读&#xff0c;以帮助数据…

计算机网络 (16)数字链路层的几个共同问题

一、封装成帧 封装成帧是数据链路层的一个基本问题。数据链路层把网络层交下来的数据构成帧发送到链路上&#xff0c;以及把接收到的帧中的数据取出并上交给网络层。封装成帧就是在一段数据的前后分别添加首部和尾部&#xff0c;构成了一个帧。接收端在收到物理层上交的比特流后…

SAP SD信贷管理信用管理手册(下)

1、项目类别的信贷激活 图1-12-1.项目类别的信贷设置路径 图1-12-2.项目类别的信贷参数激活 说明&#xff1a;项目类别是否进行信贷管理设置。 2、定义信贷组 图1-13-1.定义信贷组路径 图1-13-2.信贷组定义 说明&#xff1a;信贷组参与后续信贷控制的组合分配。 3、销售凭证及…

【linux学习指南】可重入函数与volatile

文章目录 &#x1f4dd;可重⼊函数&#x1f320; volatile&#x1f6a9;总结 &#x1f4dd;可重⼊函数 main函数调⽤insert函数向⼀个链表head中插⼊节点node1,插⼊操作分为两步,刚做完第⼀步的时候,因为硬件中断使进程切换到内核,再次回⽤⼾态之前检查到有信号待处理,于是切换…

Web安全 - “Referrer Policy“ Security 头值不安全

文章目录 概述原因分析风险说明Referrer-Policy 头配置选项1. 不安全的策略no-referrer-when-downgradeunsafe-url 2. 安全的策略no-referreroriginorigin-when-cross-originsame-originstrict-originstrict-origin-when-cross-origin 推荐配置Nginx 配置示例 在 Nginx 中配置 …

FFmpeg:详细安装教程与环境配置指南

FFmpeg 部署完整教程 在本篇博客中&#xff0c;我们将详细介绍如何下载并安装 FFmpeg&#xff0c;并将其添加到系统的环境变量中&#xff0c;以便在终端或命令行工具中直接调用。无论你是新手还是有一定基础的用户&#xff0c;这篇教程都能帮助你轻松完成 FFmpeg 的部署。 一、…

AcWing练习题:平均数1

读取两个浮点数 AA 和 BB 的值&#xff0c;对应于两个学生的成绩。 请你计算学生的平均分&#xff0c;其中 AA 的成绩的权重为 3.53.5&#xff0c;BB 的成绩的权重为 7.57.5。 成绩的取值范围在 00 到 1010 之间&#xff0c;且均保留一位小数。 输入格式 输入占两行&#x…