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

SharedPreferences 很垃圾吗? 嗯,他会阻塞主线程。他可能会崩溃,他可能无法提供大内容的存储,性能比较差,ANR等等。

但是是它的错吗?他的设计本意是提供极少的一些变量存储。结果臃肿的代码和封装写法,过度使用导致了很多问题。

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

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

  1. MMKV

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

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

    java 复制代码
    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函数:

    java 复制代码
    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类:多读少取就使用这种封装类:

java 复制代码
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:

kotlin 复制代码
class MemorySharedPreference(private val spName:String, private val applicationContext:Context) {
    private val sp by lazy { applicationContext.getSharedPreferences(spName, Context.MODE_PRIVATE) }

    //暂时内存结果。避免过度read
    private 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] = value
        sp.edit().putString(key, value).apply()
    }

    fun putStringSet(key: String, values: MutableSet<String>?) {
        map[key] = values
        sp.edit().putStringSet(key, values).apply()
    }

    fun putInt(key: String, value: Int) {
        map[key] = value
        sp.edit().putInt(key, value).apply()
    }

    fun putLong(key: String, value: Long) {
        map[key] = value
        sp.edit().putLong(key, value).apply()
    }

    fun putFloat(key: String, value: Float) {
        map[key] = value
        sp.edit().putFloat(key, value).apply()
    }

    fun putBoolean(key: String, value: Boolean) {
        map[key] = value
        sp.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()
    }
}
java 复制代码
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);
    }
}
kotlin 复制代码
object AppDataStore {
    private const val DATA_STORE_NAME = "dataStore" //对应最终件:/data/data/xxxx/files/datastore/dataStore.preferences_pb
	private later var app:Application

    val 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? = null
        app.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: 这里不做二次检查了。默认就认为是stringSet
                else -> {
                    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)] = value
                is Long -> setting[longPreferencesKey(key)] = value
                is Double -> setting[doublePreferencesKey(key)] = value
                is Float -> setting[floatPreferencesKey(key)] = value
                is Boolean -> setting[booleanPreferencesKey(key)] = value
                is String -> setting[stringPreferencesKey(key)] = value
                is 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")
            }
        }
    }
}
相关推荐
众拾达人7 分钟前
Android自动化测试实战 Java篇 主流工具 框架 脚本
android·java·开发语言
吃着火锅x唱着歌1 小时前
PHP7内核剖析 学习笔记 第四章 内存管理(1)
android·笔记·学习
_Shirley2 小时前
鸿蒙设置app更新跳转华为市场
android·华为·kotlin·harmonyos·鸿蒙
hedalei4 小时前
RK3576 Android14编译OTA包提示java.lang.UnsupportedClassVersionError问题
android·android14·rk3576
锋风Fengfeng4 小时前
安卓多渠道apk配置不同签名
android
枫_feng5 小时前
AOSP开发环境配置
android·安卓
叶羽西5 小时前
Android Studio打开一个外部的Android app程序
android·ide·android studio
qq_171538856 小时前
利用Spring Cloud Gateway Predicate优化微服务路由策略
android·javascript·微服务
Vincent(朱志强)8 小时前
设计模式详解(十二):单例模式——Singleton
android·单例模式·设计模式
mmsx8 小时前
android 登录界面编写
android·登录界面