MMKV缺陷:不支持getAll?

5,234 阅读5分钟

前言

本文主要包括以下内容
1.MMKV有什么缺陷?
2.使用kotlin委托优化MMKV调用的实用技巧

如果觉得本文对您有所帮助,请帮忙点赞,谢谢~

MMKV有什么缺陷

我们在之前介绍过,MMKV相比SharedPreferences有很多优点,比如
1.mmap防止数据丢失,提高读写效率;
2.精简数据,以最少的数据量表示最多的信息,减少数据大小;
3.增量更新,避免每次进行相对增量来说大数据量的全量写入。
详情可见:SharedPreferences替换:MMKV集成与原理

那么MMKV有没有什么缺点?
MMKV的主要缺点就在于它不支持getAll

public Map<String, ?> getAll() {
    throw new UnsupportedOperationException("use allKeys() instead, getAll() not implement because type-erasure inside mmkv");
}

MMKV都是按字节进行存储的,实际写入文件把类型擦除了,这也是MMKV不支持getAll的原因

虽然说getAll用的不多问题不大,但是MMKV因此就不具备导出和迁移的能力了。
比如说,以后出了更优秀的存储框架,例如DataStore正式发布后,是没有办法直接从MMKV批量迁移到新框架的,除非代码里面写死一个个key迁移,这样是很麻烦的
所以在我们引入MMKV时,就应该考虑到将来可能的数据迁出

如何让MMKV支持getAll?

既然MMKV不支持getAll的原因是因为类型被擦除了,那最简单的思路就是加上类型
我们可以在key上添加一个类型后缀

但是如果我们强制规定写key时后面都添加上后缀是难以维护的,并且也很麻烦
一个更加优雅地方式是添加一个代理层,所有读写操作都代理给这个代理类,并在这里为key添加类型

class SpProxy(private val mmkv: MMKV?) : SharedPreferences, SharedPreferences.Editor {
    override fun getAll(): MutableMap<String, *> {
        val keys = mmkv?.allKeys()
        val map = mutableMapOf<String, Any>()
        keys?.forEach {
            if (it.contains("@")) {
                val typeList = it.split("@")
                when (typeList[typeList.size - 1]) {
                    String::class.simpleName -> map[it] = getString(it, "") ?: ""
                    Int::class.simpleName -> map[it] = getInt(it, 0)
                    Long::class.simpleName -> map[it] = getLong(it, 0L)
                    Float::class.simpleName -> map[it] = getFloat(it, 0f)
                    Boolean::class.simpleName -> map[it] = getBoolean(it, false)
                }
            }
        }
        return map
    }

    override fun getString(key: String?, defValue: String?): String? {
        val typeKey = getTypeKey<String>(key)
        return mmkv?.getString(typeKey, defValue)
    }

    override fun getBoolean(key: String?, defValue: Boolean): Boolean {
        val typeKey = getTypeKey<Boolean>(key)
        return mmkv?.getBoolean(typeKey, defValue) ?: defValue
    }

    ...

    override fun contains(key: String?): Boolean {
        val realKey = getRealKey(key)
        return realKey.isNotEmpty()
    }

    override fun edit(): SharedPreferences.Editor? {
        return mmkv?.edit()
    }

    override fun putString(key: String?, value: String?): SharedPreferences.Editor? {
        val typeKey = getTypeKey<String>(key)
        return mmkv?.putString(typeKey, value)
    }

    override fun putBoolean(key: String?, value: Boolean): SharedPreferences.Editor? {
        val typeKey = getTypeKey<Boolean>(key)
        return mmkv?.putBoolean(typeKey, value)
    }

    override fun remove(key: String?): SharedPreferences.Editor? {
        val realKey = getRealKey(key)
        if (realKey.isNotEmpty()){
            return mmkv?.remove(realKey)
        }
        return null
    }

    override fun clear(): SharedPreferences.Editor? {
        return mmkv?.clear()
    }

    inline fun <reified T> getTypeKey(key: String?): String {
        val type = "@" + T::class.simpleName
        return if (key?.contains(type) == true) {
            type
        } else {
            key + type
        }
    }

    private fun getRealKey(key: String?):String{
        val typeKys = listOf(getTypeKey<String>(key),getTypeKey<Long>(key),getTypeKey<Float>(key),getTypeKey<Int>(key),getTypeKey<Boolean>(key))
        typeKys.forEach {
            if (mmkv?.containsKey(it)==true){
                return it
            }
        }
        return ""
    }
}

如上所示:
1.所有读写操作都基于SpProxy来做
2.key尾部添加类型字段通过getTypeKey方法实现
3.支持getAll方法,方便后续迁移
4.mmkv的操作全都封装在SpProxy类里了,后续如果要迁移到其他类,修改SpProxy即可,外部可完全不用修改

计算机软件中所有问题都可以通过添加一个中间层来解决,为SharedPreferences封装一个代理层,可以有效地拓展我们项目的扩展性

如何迁移老数据

MMKV为我们提供了importFromSharedPreferences方法来从SharedPreferences迁移到MMKV
但如果直接调用这个方法迁移,那么数据类型就丢失了,下面提供一个新的迁移方法

fun migrate(migrateSp:SpProxy,preferences: SharedPreferences){
    val kvs = preferences.all
   if (kvs != null && kvs.isNotEmpty()) {
       val iterator = kvs.entries.iterator()
        while (iterator.hasNext()) {
            val entry = iterator.next()
            val key = entry.key
            val value = entry.value
            if (key != null && value != null) {
                migrateSp.run {
                    when (value) {
                        is Boolean -> this.putBoolean(key, value)
                        is Int ->  this.putInt(key,value)
                        is Long -> this.putLong(key,value)
                        is Float -> this.putFloat(key, value)
                        is String -> this.putString(key,value)
                        else -> {}
                    }
                }
            }
        }
        kvs.size
    }
}

如上所示:支持从SharedPreferences迁移数据到MMKV并保留类型

利用委托优化存取操作

我们一般利用SharedPreferences存取数据是这样写的

private val KEY_DEMO_STR = "key_demo_STR"
fun setDemoStr(str: String) {
    edit.putString(KEY_DEMO_STR, str).apply()
}

fun getDemoStr(): String? {
    return preferences.getString(KEY_DEMO_STR, "")
}

首先我们需要定义一个key,然后再定义setget方法
这样一个简单的操作就需要8行代码了,但如果我们利用委托机制可以实现一行搞定

一行搞定数据存取

优化后的代码实现数据存取可以一行搞定

object TestSP : PreferenceHolder() {
    var value: Long by bindToPreferenceField(0L)
}

//读取sp
val value = TestSP.value
println(value) // 0 or 100
//存入sp
TestSP.value = 100

如上所示,通过变量value的读取与赋值即可实现数据的存取
这背后的原理是根据变量名生成key,然后将数据地存取操作委托给了ReadWriteProperty
然后在其中的getValuesetValue中调用mmkvSharedPreferences相关方法来实现真正的存储与读取

这样做的优点在于
1.避免定义大量字符串key和出现重复key
2.简洁的委托模式不用再书写get(..) set(..)
3.后续如果需要修改代码,修改委托类即可,项目中的其他代码不需要变动,增强了扩展性

详情可见:Preferences委托优化

总结

本文主要讲述了MMKV的缺陷:即不支持getAll导致后续迁移困难及解决方案
同时介绍了利用委托优化数据存取API调用的经验

Show Me The Code

本文代码可见:SpProxy

参考资料

SharedPreferences用Kotlin应该这样写
来聊一聊MMKV的不足以及线下调试工具