SharedPreferences垃圾吗?对比MMKV和DataStore经验之谈

2024-04-30 20:12

本文主要是介绍SharedPreferences垃圾吗?对比MMKV和DataStore经验之谈,希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!

SharedPreferences 很垃圾吗? 嗯,他会阻塞主线程。他可能会崩溃,他可能无法提供大内容的存储,性能比较差,ANR等等。
但是是它的错吗?他的设计本意是提供极少的一些变量存储。结果臃肿的代码和封装写法,过度使用导致了很多问题。

如果不想看全篇,看粗体内容看看你是否有共鸣和不了解的地方,查漏补缺。

目前流行的存储有如下几个我这边自行给出个人使用感受:

  1. MMKV
    通过内存映射,写入文件就是写入内存,读取和存储都极快。稳定性和速度都能经得起考验。它是腾讯出品用于日志存储的,由于是奔溃瞬间也能记录,那么也就只有mmap的特性,进程死了也能存储。但是会有较低概率,彻底损坏,而且是整个文件损坏。所以存储无关紧要的东西最好。如果真实发生损坏的话,丢失了也没什么关系。但是,很多公司和代码都习惯使用mmkv来作为缓存第一方案。比较概率还是很低很低的。
    我有做过上架app的程序,也有在系统平台定制rom开发的程序,mmkv丢失没有遇到过的,但是可能并没有及时同步到文件中(比如改变一个配置,立刻关机,下次开机该配置是老的,修改办法是每次有修改都delay10秒去mmkv.async()一下)。因为它要的就是快,如果每次你写入,立刻保存它就退化成了SharedPref了。

    那么,极度依赖数据不丢失的程序,建议开启自行编写一个策略去备份,比如10天半个月备份一次,引自官方文档:

    String backupRootDir = getFilesDir().getAbsolutePath() + "/mmkv_backup_3";
    // backup one instance
    boolean ret = MMKV.backupOneToDirectory(mmapID, backupRootDir, null);
    // backup all instances
    long count = MMKV.backupAllToDirectory(backupRootDir);// restore one instance
    ret = MMKV.restoreOneMMKVFromDirectory(mmapID, backupRootDir, otherDir);
    // restore all instances
    count = MMKV.restoreAllFromDirectory(backupRootDir);
    

    那么, 大部分情况,都是比较推荐使用MMKV的。如果你的产品并不在意大部分丢失。比如我们的一个app本身日活可能是每周2次,哪怕是登录信息丢失了。也没什么,用户也会认为过期等再次登录即可。当然是需要你们产品综合考虑的。更何况在一般手机上并不会突然断电,断电也轮不到你的app在工作。

  2. SharedPreferences
    android有的时候就有它了,基本上代码15年机制没有变化。他的缺点前面已经提到。引用别人的帖子讲的很全:

    1、不要存放大的key和value在SharedPreferences中,否则会一直存诸在内存中得不到释放,内存使用过高会频发引发GC,导致界面丢帧甚至ANR。
    2、不相关的配置选项最好不要放在一起,单个文件越大读取速速度则越慢。
    3、读取频繁的key和不频繁的key尽量不要放在一起(如果整个个文件本身就较小则忽略,为了这点性能添加维护得不偿失)。
    4、不要每次都edit,因为每次都会创建一个新的Editorlmpl对象象,最好是批量处理统一提交。
    否则edit().commit每次创建一个Editorlmpl对象并且进行一次IO操作,严重影响性能。
    5、commit发生在Ul线程中,apply发生在工作线程中,对于数据的提交最好是批量操作统一提交。虽然apply发生在工作线程(不会因为IO阻塞UI线程)但是如果添加任
    务较多也有可能带来其他严重后果(参照ActivityThread源码中handleStopActivity方法实现)。
    6、尽量不要存放大json,大html,这种可以直接文件缓存。 7、不要指望这货能够跨进程通信Context.PROCESS。
    8、最好提前初始化SharedPreferences, 避免SharedPreferences第一次创建时读取文件线程未结束而出现等待情况。

    那么我再补充一点,很多人封装了这种static函数:

    public static void putXXX(Context ctx, String key, boolean value) {// name存储文件名称ctx.getSharedPreferences(NAME, Context.MODE_PRIVATE).edit().putXXX(key, value).apply();
    }
    public static boolean getXXX(Context ctx, String key, boolean defValue) {// name存储文件名称return ctx.getSharedPreferences(SPXMLNAME, Context.MODE_PRIVATE).getXXX(key, defValue);
    }
    

    对吧,但是看了源码你就知道,如果你频繁需要用到这个函数的时候,你每次去创建内部有大量的同步锁,文件读取,创建等待。性能备受挑战。综合这些SP的问题,其实大部分是使用者给他强加了太多的压力了,他本来就是一个小巧的东西。
    经验总结:
    1. 一定要存的条目少,value要短;条目多了拆文件。
    2. 大量读取少量写入的case,一定要结合static变量,避免每次都新建sp(后面我会贴出参考代码)。
    3. Application的attachBaseContext里面如果有读取需要,建议考虑使用SP。MMKV可能初始化没好,DataStore又是异步。

  3. DataStore
    文件存储跟SP一样,会自行备份,安全不丢失
    也解决了SP的各种卡顿,隐形奔溃。
    缺点是,项目没有kotlin不行。读取必须是suspend,异步。如果非要同步可以使用runBlocking{}包裹。
    稍微复杂点,我已经封装好类。
    同样的,如果你比如在application或者首屏有加载需要,考虑拆分文件。

SharedPrefUtils静态类: 前面提到过,少读少取就用这种封装类:
AppDataStore单例类:kotlin项目DataStore的单例帮助类:
MemorySharedPreference类:多读少取就使用这种封装类:

class MyPefUtil {private static String perf; //通过这种方式来提速读取效率。因为这个perf在代码中需要到处读取public static void putPerf(Context ctx, String key, String value) {this.perf = value;// name存储文件名称ctx.getSharedPreferences(NAME, Context.MODE_PRIVATE).edit().putString(key, value).apply();}public static String getXXX(Context ctx, String key) {if (perf != null) {return perf;}// name存储文件名称perf = ctx.getSharedPreferences(NAME, Context.MODE_PRIVATE).getString(key, defValue);return perf;}
}

MemorySharedPreference:

class MemorySharedPreference(private val spName:String, private val applicationContext:Context) {private val sp by lazy { applicationContext.getSharedPreferences(spName, Context.MODE_PRIVATE) }//暂时内存结果。避免过度readprivate val map = ConcurrentHashMap<String?, Any?>(4)fun getString(key: String, defValue: String?): String? {if (map.containsKey(key)) {return map[key].asOrNull()}return sp.getString(key, defValue).also { map[key] = it }}fun getStringSet(key: String, defValues: MutableSet<String>?): MutableSet<String>? {if (map.containsKey(key)) {return map[key].asOrNull()}return sp.getStringSet(key, defValues).also { map[key] = it }}fun getInt(key: String, defValue: Int): Int? {if (map.containsKey(key)) {return map[key].asOrNull()}return sp.getInt(key, defValue).also { map[key] = it }}fun getLong(key: String, defValue: Long): Long {if (map.containsKey(key)) {return map[key].asOrNull() ?: 0}return sp.getLong(key, defValue).also { map[key] = it }}fun getFloat(key: String, defValue: Float): Float {if (map.containsKey(key)) {return map[key].asOrNull() ?: 0f}return sp.getFloat(key, defValue).also { map[key] = it }}fun getBoolean(key: String, defValue: Boolean): Boolean {if (map.containsKey(key)) {return map[key].asOrNull() ?: false}return sp.getBoolean(key, defValue).also { map[key] = it }}fun contains(key: String): Boolean {if (map.containsKey(key)) {return true}return sp.contains(key)}fun putString(key: String, value: String?) {map[key] = valuesp.edit().putString(key, value).apply()}fun putStringSet(key: String, values: MutableSet<String>?) {map[key] = valuessp.edit().putStringSet(key, values).apply()}fun putInt(key: String, value: Int) {map[key] = valuesp.edit().putInt(key, value).apply()}fun putLong(key: String, value: Long) {map[key] = valuesp.edit().putLong(key, value).apply()}fun putFloat(key: String, value: Float) {map[key] = valuesp.edit().putFloat(key, value).apply()}fun putBoolean(key: String, value: Boolean) {map[key] = valuesp.edit().putBoolean(key, value).apply()}fun remove(key: String) {map.remove(key)sp.edit().remove(key).apply()}fun clear() {map.clear()sp.edit().clear().apply()}
}
public class SharedPrefUtil {private static String SPXMLNAME = "sp_config";public static void removeKey(Context ctx, String key) {ctx.getSharedPreferences(SPXMLNAME, Context.MODE_PRIVATE).edit().remove(key).commit();}// 1,存储boolean变量方法public static void putBoolean(Context ctx, String key, boolean value) {// name存储文件名称ctx.getSharedPreferences(SPXMLNAME, Context.MODE_PRIVATE).edit().putBoolean(key, value).apply();}// 2,读取boolean变量方法public static boolean getBoolean(Context ctx, String key, boolean defValue) {// name存储文件名称return ctx.getSharedPreferences(SPXMLNAME, Context.MODE_PRIVATE).getBoolean(key, defValue);}public static void putString(Context ctx, String key, String value) {// name存储文件名称ctx.getSharedPreferences(SPXMLNAME, Context.MODE_PRIVATE).edit().putString(key, value).apply();}public static String getString(Context ctx, String key, String defValue) {// name存储文件名称return ctx.getSharedPreferences(SPXMLNAME, Context.MODE_PRIVATE).getString(key, defValue);}//public static void putInt(Context ctx, String key, int value) {// name存储文件名称ctx.getSharedPreferences(SPXMLNAME, Context.MODE_PRIVATE).edit().putInt(key, value).apply();}public static int getInt(Context ctx, String key, int defValue) {// name存储文件名称return ctx.getSharedPreferences(SPXMLNAME, Context.MODE_PRIVATE).getInt(key, defValue);}
}
object AppDataStore {private const val DATA_STORE_NAME = "dataStore" //对应最终件:/data/data/xxxx/files/datastore/dataStore.preferences_pbprivate later var app:Applicationval Context.dataStore by preferencesDataStore(name = DATA_STORE_NAME,//指定名称
//    produceMigrations = {context ->  //指定要恢复的sp文件,无需恢复可不写
//        listOf(SharedPreferencesMigration(context, SP_PREFERENCES_NAME))
//    })suspend inline fun <reified T> containsKey(key:String) : Boolean{val prefKey = when (T::class.java) {Int::class.java -> intPreferencesKey(key)Long::class.java -> longPreferencesKey(key)Double::class.java -> doublePreferencesKey(key)Float::class.java -> floatPreferencesKey(key)Boolean::class.java -> booleanPreferencesKey(key)String::class.java -> stringPreferencesKey(key)Set::class.java -> stringSetPreferencesKey(key)else -> {throw IllegalArgumentException("This type can be removed from DataStore")}}val t = app.dataStore.data.map {it.asMap().forEach { (t, u) ->ALog.t("allData: $t -> $u")}it.contains(prefKey)}.first()return t}fun clear() {runBlocking {app.dataStore.edit {ALog.t("clear!")it.clear()} }}fun save(key:String, value: Any) {runBlocking {ALog.t("save $key <to> $value")saveSuspend(key, value)}}fun save(vararg pair:Pair<String, Any>) {runBlocking {pair.forEach {saveSuspend(it.first, it.second)}}}inline fun <reified T> remove(key:String) {runBlocking {removeSuspend<T>(key)}}suspend inline fun <reified T> removeSuspend(key:String) : T?{var ret : T? = nullapp.dataStore.edit { setting ->ret = when (T::class.java) {Int::class.java -> setting.remove(intPreferencesKey(key)).asOrNull()Long::class.java -> setting.remove(longPreferencesKey(key)).asOrNull()Double::class.java -> setting.remove(doublePreferencesKey(key)).asOrNull()Float::class.java -> setting.remove(floatPreferencesKey(key)).asOrNull()Boolean::class.java -> setting.remove(booleanPreferencesKey(key)).asOrNull()String::class.java -> setting.remove(stringPreferencesKey(key)).asOrNull()Set::class.java -> setting.remove(stringSetPreferencesKey(key)).asOrNull() //later: 这里不做二次检查了。默认就认为是stringSetelse -> {throw IllegalArgumentException("This type can be removed from DataStore")}}}return ret}/*** 因为我们用于保存,不应该使用lifeCycleScope来发起。有可能无法保存成功。应该使用全局scope。*/@Deprecated("不建议直接使用,因为可能协程被取消,除非你明白你的scope一定保存成功")suspend fun saveSuspend(key:String, value:Any) {app.dataStore.edit { setting ->when (value) {is Int -> setting[intPreferencesKey(key)] = valueis Long -> setting[longPreferencesKey(key)] = valueis Double -> setting[doublePreferencesKey(key)] = valueis Float -> setting[floatPreferencesKey(key)] = valueis Boolean -> setting[booleanPreferencesKey(key)] = valueis String -> setting[stringPreferencesKey(key)] = valueis Set<*> -> {val componentType = value::class.java.componentType!!@Suppress("UNCHECKED_CAST") // Checked by reflection.when {String::class.java.isAssignableFrom(componentType) -> {app.dataStore.edit { preferences ->preferences[stringSetPreferencesKey(key)] = value as Set<String>}}}}else -> {throw IllegalArgumentException("This type can be saved into DataStore")}}}}/*** 获取数据* */suspend inline fun < reified T : Any> read(key: String, defaultValue:T): T {return  when (T::class) {Int::class -> {app.dataStore.data.map { setting ->setting[intPreferencesKey(key)] ?: defaultValue}.first() as T}Long::class -> {app.dataStore.data.map { setting ->setting[longPreferencesKey(key)] ?: defaultValue}.first() as T}Double::class -> {app.dataStore.data.map { setting ->setting[doublePreferencesKey(key)] ?:defaultValue}.first() as T}Float::class -> {app.dataStore.data.map { setting ->setting[floatPreferencesKey(key)] ?:defaultValue}.first() as T}Boolean::class -> {app.dataStore.data.map { setting ->setting[booleanPreferencesKey(key)]?:defaultValue}.first() as T}String::class -> {app.dataStore.data.map { setting ->setting[stringPreferencesKey(key)] ?: defaultValue}.first() as T}else -> {throw IllegalArgumentException("This type can be get into DataStore")}}}
}

这篇关于SharedPreferences垃圾吗?对比MMKV和DataStore经验之谈的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/949809

相关文章

锐捷和腾达哪个好? 两个品牌路由器对比分析

《锐捷和腾达哪个好?两个品牌路由器对比分析》在选择路由器时,Tenda和锐捷都是备受关注的品牌,各自有独特的产品特点和市场定位,选择哪个品牌的路由器更合适,实际上取决于你的具体需求和使用场景,我们从... 在选购路由器时,锐捷和腾达都是市场上备受关注的品牌,但它们的定位和特点却有所不同。锐捷更偏向企业级和专

什么是 Ubuntu LTS?Ubuntu LTS和普通版本区别对比

《什么是UbuntuLTS?UbuntuLTS和普通版本区别对比》UbuntuLTS是Ubuntu操作系统的一个特殊版本,旨在提供更长时间的支持和稳定性,与常规的Ubuntu版本相比,LTS版... 如果你正打算安装 Ubuntu 系统,可能会被「LTS 版本」和「普通版本」给搞得一头雾水吧?尤其是对于刚入

TP-LINK/水星和hasivo交换机怎么选? 三款网管交换机系统功能对比

《TP-LINK/水星和hasivo交换机怎么选?三款网管交换机系统功能对比》今天选了三款都是”8+1″的2.5G网管交换机,分别是TP-LINK水星和hasivo交换机,该怎么选呢?这些交换机功... TP-LINK、水星和hasivo这三台交换机都是”8+1″的2.5G网管交换机,我手里的China编程has

【编程底层思考】垃圾收集机制,GC算法,垃圾收集器类型概述

Java的垃圾收集(Garbage Collection,GC)机制是Java语言的一大特色,它负责自动管理内存的回收,释放不再使用的对象所占用的内存。以下是对Java垃圾收集机制的详细介绍: 一、垃圾收集机制概述: 对象存活判断:垃圾收集器定期检查堆内存中的对象,判断哪些对象是“垃圾”,即不再被任何引用链直接或间接引用的对象。内存回收:将判断为垃圾的对象占用的内存进行回收,以便重新使用。

免费也能高质量!2024年免费录屏软件深度对比评测

我公司因为客户覆盖面广的原因经常会开远程会议,有时候说的内容比较广需要引用多份的数据,我记录起来有一定难度,所以一般都用录屏工具来记录会议内容。这次我们来一起探索有什么免费录屏工具可以提高我们的工作效率吧。 1.福晰录屏大师 链接直达:https://www.foxitsoftware.cn/REC/  录屏软件录屏功能就是本职,这款录屏工具在录屏模式上提供了多种选项,可以选择屏幕录制、窗口

HotSpot虚拟机的经典垃圾收集器

读《深入理解Java虚拟机》第三版笔记。 关系 Serial、ParNew、Parallel Scavenge、Parallel Old、Serial Old(MSC)、Concurrent Mark Sweep (CMS)、Garbage First(G1)收集器。 如图: 1、Serial 和 Serial Old 收集器 2、ParNew 收集器 3、Parallel Sc

浅谈PHP5中垃圾回收算法(Garbage Collection)的演化

前言 PHP是一门托管型语言,在PHP编程中程序员不需要手工处理内存资源的分配与释放(使用C编写PHP或Zend扩展除外),这就意味着PHP本身实现了垃圾回收机制(Garbage Collection)。现在如果去PHP官方网站(php.net)可以看到,目前PHP5的两个分支版本PHP5.2和PHP5.3是分别更新的,这是因为许多项目仍然使用5.2版本的PHP,而5.3版本对5.2并不是完

类的load方法和initialize方法对比

1. load方法在main()之前被调用,而initialize方法在main()之后调用 load方法实际是在load_images过程中被调用的。load_images会将当前应用依赖的所有镜像(动态库)加载到内存,在在加载中首先是对镜像进行扫描,将所有包含 load 方法的类加入列表 loadable_classes ,然后从这个列表中逐一调用其所包含的 load 方法。 +[XXCl

JavaScript正则表达式六大利器:`test`、`exec`、`match`、`matchAll`、`search`与`replace`详解及对比

在JavaScript中,正则表达式(Regular Expression)是一种用于文本搜索、替换、匹配和验证的强大工具。本文将深入解析与正则表达式相关的几个主要执行方法:test、exec、match、matchAll、search和replace,并对它们进行对比,帮助开发者更好地理解这些方法的使用场景和差异。 正则表达式基础 在深入解析方法之前,先简要回顾一下正则表达式的基础知识。正则

【HarmonyOS】-TaskPool和Worker的对比实践

ArkTS提供了TaskPool与Worker两种多线程并发方案,下面我们将从其工作原理、使用效果对比两种方案的差异,进而选择适用于ArkTS图片编辑场景的并发方案。 TaskPool与Worker工作原理 TaskPool与Worker两种多线程并发能力均是基于 Actor并发模型实现的。Worker主、子线程通过收发消息进行通信;TaskPool基于Worker做了更多场景化的功能封装,例