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")
            }
        }
    }
}
相关推荐
千里马学框架5 天前
一起学 Android 14:ShellTransition 屏幕旋转过程深度剖析
android·智能手机·性能优化·framework·性能·屏幕旋转·rotation
美狐美颜SDK开放平台5 天前
开发直播APP时如何接入视频美颜SDK?开发流程与注意事项
android·人工智能·计算机视觉·音视频·直播美颜sdk
AFinalStone5 天前
Android7 SystemUI源码解析(七)Keyguard锁屏模块深度解析
android·systemui
致远ccc5 天前
Google Play 上架前如何测试 App?多国家 Android 环境测试
android·app测试·googleplay·多国家应用测试
ttyyttemo5 天前
Kotlin 协程中的 Job 结构化并发与取消
android
sun0077005 天前
tbox 4g/5g切换,导致wan ip 改变,导致车机旧网络不可用。需要重启车机才行
android
其实防守也摸鱼5 天前
内网穿透与反向代理:原理、工具与实战指南
android·大数据·运维·安全·网络安全·自动化·渗透
AFinalStone5 天前
Android7 SystemUI 源码解析(四)NavigationBar 导航栏与 SystemBars
android·systemui
JMchen5 天前
属性动画原理与高级动画实现
android·kotlin·canvas
AFinalStone5 天前
Android7 SystemUI 源码解析(二)启动流程深度解析
android·systemui