当前位置: 首页>后端>正文

一种好用的KV存储封装方案

一、 概述

众所周知,用kotlin委托属性去封装KV存储库,可以优化数据的访问。
封装方法有多种,各有优劣。
通过反复实践,笔者摸索出一套比较好用的方案,借此文做个简单的分享。

代码已上传Github: https://github.com/BillyWei01/KVWrapper
其中包含了基础类型,Set<String>, byte[], 对象,枚举,Map等类型的封装方法。

二、 封装方法

封装过程包含 基类定义委托实现 两部分。
项目源码中已经实现了各种常用类型的定义,使用时复制粘贴即可。
这里我们贴一下 基类定义 的代码。

2.1 方法封装

abstract class KVData {  
    // 定义KV接口,由子类提供一个包含基本put/get方法的KV实现。
    abstract val kv: SpKV  

    // 基础类型  
    protected fun boolean(key: String, defValue: Boolean = false) = BooleanProperty(key, defValue)  
    protected fun int(key: String, defValue: Int = 0) = IntProperty(key, defValue)  
    protected fun float(key: String, defValue: Float = 0f) = FloatProperty(key, defValue)  
    protected fun long(key: String, defValue: Long = 0L) = LongProperty(key, defValue)  
    protected fun double(key: String, defValue: Double = 0.0) = DoubleProperty(key, defValue)  
    protected fun string(key: String, defValue: String = "") = StringProperty(key, defValue)  
    protected fun array(key: String, defValue: ByteArray = EMPTY_ARRAY) = ObjectProperty(key, ArrayEncoder, defValue)  

    // 内置的对象类型  
    protected fun stringSet(key: String, defValue: Set<String>= null) = StringSetProperty(key, defValue)  

    // 自定义对象类型  
    protected fun <T> obj(key: String, encoder: ObjectEncoder<T>, defValue: T= null) = ObjectProperty(key, encoder, defValue)  

    // 枚举类型  
    protected fun <T> stringEnum(key: String, converter: StringEnumConverter<T>) = StringEnumProperty(key, converter)  
    protected fun <T> intEnum(key: String, converter: IntEnumConverter<T>) = IntEnumProperty(key, converter)  

    // Map类型  
    protected fun combineKey(key: String) = CombineKeyProperty(key)  
    protected fun string2String(key: String) = StringToStringProperty(key)  
    protected fun string2Set(key: String) = StringToSetProperty(key)  
    protected fun string2Int(key: String) = StringToIntProperty(key)  
    protected fun string2Boolean(key: String) = StringToBooleanProperty(key)  
    protected fun int2Boolean(key: String) = IntToBooleanProperty(key)  

    // 可以按需扩展更多的类型  
}  

各种委托实现类类名比较长,我们在基类封装一些名称简短的方法,以方便使用。

2.2 数据隔离

不同环境(开发环境/测试环境),不同用户,最好数据实例是分开的,相互不干扰。
比方说有 uid='001' 和 uid='002' 两个用户的数据,如果需要隔离两者的数据,有多种方法,例如:

  1. 拼接uid到key
    如果是在原始的SharePreferences的基础上,是比较好实现的,直接put(key+uid, value)即可;
    但是如果用委托属性定义,则相对麻烦一些,因为通常用委托属性定义时,key是常量。
    对于这种需要复合 常量 + 变量 的情况,可以用上面定义的Map类型的委托(底层实现也是拼接key)。
    但不同用户的数据糅合到一个文件中,对性能多少有些影响:
    • 在多用户的情况下,实例的数据膨胀;
    • 每次访问value, 都需要拼接uid到key上。

因此,可以将不同用户的数据保存到不同的实例中。

  1. 拼接uid到文件名
    具体的做法,就是拼接uid到路径或者文件名上。
    对于SharePreferences来说,显然只能拼接uid到名字上了。

基于此分析,我们定义两种类型的基类:

  • GlobalKV: 全局数据,切换环境和用户,不影响GlobalKV所访问的数据实例。
  • UserKV: 用户数据,需要同时区分 “服务器环境“ 和 ”用户ID“。
// 全局数据  
open class GlobalKV(name: String) : KVData() {  
    override val kv: SpKV by lazy {  
        SpKV(name)  
    }  
}  
// 用户数据
abstract class UserKV(
    private val name: String,
    private val userId: Long
) : KVData() {
    override val kv: SpKV by lazy {
        val fileName = "${name}_${userId}_${AppContext.env.tag}"
        if (AppContext.debug) {
            SpKV(fileName)
        } else {
            // 如果是release包,可以对文件名做个md5,以便隐藏uid等信息
            SpKV(Utils.getMD5(fileName.toByteArray()))
        }
    }
}

UserKV通过将用户ID和环境等信息拼接到文件名中,可以使得不同用户/不同环境的数据写到不同的文件。

三、 使用方法

  • 数据类的定义
    根据数据的作用域,决定继承自 GlobalKV 还是 UserKV

  • 变量的声明

    • 基本数据类型,传入key即可;
    • 枚举类型或者对象类型,需要同时传入key和转换接口的实现(将非基本类型序列化为基本类型)。

3.1 GlobalKV实例

// APP信息    
object AppState : GlobalKV("app_state") {  
    // 服务器环境  
    var environment by stringEnum("environment", Env.CONVERTER)  

    // 用户ID  
    var userId by long("user_id")  

    // 设备ID  
    var deviceId by string("device_id")  
}  
  

保存数据:

AppState.userId = uid  

读取数据:

val uid = AppState.userId  

3.2 UserKV实例

//  用户信息    
class UserInfo(uid: Long) : UserKV("user_info", uid) {
    companion object {
        private val map = ArrayMap<Long, UserInfo>()

        @Synchronized
        fun get(): UserInfo {
            return get(AppContext.uid)
        }

        @Synchronized
        fun get(uid: Long): UserInfo {
            return map.getOrPut(uid) {
                UserInfo(uid)
            }
        }
    }

    var userAccount by obj("user_account", AccountInfo.ENCODER)
    var gender by intEnum("gender", Gender.CONVERTER)
    var isVip by boolean("is_vip")
    var fansCount by int("fans_count")
    var score by float("score")
    var loginTime by long("login_time")
    var balance by double("balance")
    var sign by string("sing")
    var lock by array("lock")
    var tags by stringSet("tags")
    val favorites by string2Set("favorites")
    val config by combineKey("config")
}
  

UserKV的实例不能是单例(不同的uid对应不同的实例)。
因此,可以定义companion对象,用来缓存实例,以及提供获取实例的API。

然后声明变量的部分,和GlobalKV无异。
需要注意的是:

  • 基础类型,枚举类型,对象类型等,用var声明;
  • Map类型,用val声明。

Map类型保存和读取方法如下:

UserInfo.get(uid).run {  
    favorites["Android"] = setOf("A", "B", "C")  
    favorites["iOS"] = setOf("D", "E", "F", "G")  
}  
UserInfo.get(uid).run {  
    val androidFavorites = favorites["Android"]  
    val iosFavorites = favorites["iOS"]  
}  

以上代码,使用上类似于Map访问value的语法,但底层其实是通过拼接key来实现的。
比如favorites["Android"],其传入底层的key是"favorites__Android"。

3.3 环境相关的实例

有一类数据,需要区分环境,但是和用户无关。
这种情况,可以用UserKV, 然后uid传0(或者其他的uid用不到的数值)。

// 远程设置  
object RemoteSetting : UserKV("remote_setting", 0L) {  
    // 某项功能的AB测试分组
    val fun1ABTestGroup by int("fun1_ab_test_group")  
  
    // 服务端下发的配置项  
    val setting by combineKey("setting")  
}  

四、小结

文章开头给出的代码是基于SharePreferences封装的模板,但这套方案也适用于其他类型的KV存储框架
例如 FastKV 的 KVData 也是按照这套方案封装的。

通过属性委托封装KV存储的API,不仅可以代理其原本支持的保存类型,还可以通过一些技巧支持诸如组数,枚举,对象,Map等类型。
这套方案也提供了保存不同用户数据到不同实例(文件/对象)的演示。

方案内容不多,但其中包含一些比较实用的技巧,希望对各位读者有所帮助。


https://www.xamrdz.com/backend/3te1926234.html

相关文章: