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

一、 概述

众所周知,用kotlin委托属性去封装KV存储库,可以优化数据的访问。

封装方法有多种,各有优劣。

通过反复实践,笔者摸索出一套比较好用的方案,借此文做个简单的分享。

代码已上传Github: github.com/BillyWei01/...

其中包含了基础类型,Set, byte[], 对象,枚举,Map等类型的封装方法。

二、 封装方法

封装过程包含 基类定义委托实现 两部分。

项目源码中已经实现了各种常用类型的定义,使用时复制粘贴即可。

这里我们贴一下 基类定义 的代码。

2.1 方法封装

kotlin 复制代码
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)。

  1. 拼接uid到文件名中。

但是不同用户的数据糅合到一个文件中,对性能多少有些影响:

  • 在多用户的情况下,实例的数据膨胀;
  • 每次访问value, 都需要拼接uid到key上。

因此,可以将不同用户的数据保存到不同的实例中。 具体的做法,就是拼接uid到路径或者文件名上。

对于SharePreferences来说,显然只能拼接uid到名字上了。

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

  • GlobalKV: 全局数据,切换环境和用户,不影响GlobalKV所访问的数据实例。
  • UserKV: 用户数据,需要同时区分 "服务器环境" 和 "用户ID"。
kotlin 复制代码
// 全局数据  
open class GlobalKV(name: String) : KVData() {  
    override val kv: SpKV by lazy {  
        SpKV(name)  
    }  
}  
kotlin 复制代码
// 用户数据
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()))
        }
    }
}

三、 使用方法

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

然后就是声明变量:

  • 基本数据类型,传入key即可;
  • 如果是枚举类型或者对象类型,需要传入转换接口的实现,毕竟底层的KV库只认得基本类型。

3.1 GlobalKV实例

kotlin 复制代码
// 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")  
}  
  

保存数据:

kotlin 复制代码
AppState.userId = uid  

读取数据:

kotlin 复制代码
val uid = AppState.userId  

3.2 UserKV实例

kotlin 复制代码
//  用户信息    
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类型保存和读取方法如下:

kotlin 复制代码
UserInfo.get(uid).run {  
    favorites["Android"] = setOf("A", "B", "C")  
    favorites["iOS"] = setOf("D", "E", "F", "G")  
}  
kotlin 复制代码
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用不到的数值)。

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

四、小结

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

例如 FastKVKVData 也是按照这套方案封装的。

通过属性委托封装KV存储的API,不仅可以代理其原本支持的保存类型,还可以通过一些技巧支持诸如组数,枚举,对象,Map等类型。

这套方案也提供了保存不同用户数据到不同实例(文件/对象)的演示。

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

相关推荐
Cosmoshhhyyy23 分钟前
Jackson库中JsonInclude的使用
java·开发语言
007php0071 小时前
GoZero对接GPT接口的设计与实现:问题分析与解决
java·开发语言·python·gpt·golang·github·企业微信
sinat_384241092 小时前
带有悬浮窗功能的Android应用
android·windows·visualstudio·kotlin
V+zmm101342 小时前
外卖商城平台的微信小程序ssm+论文源码调试讲解
java·小程序·毕业设计·mvc·springboot
Jason-河山2 小时前
利用Java爬虫获得店铺详情:技术解析
java·开发语言·爬虫
yava_free2 小时前
介绍一下mysql binlog查看指定数据库的方法
java·数据库·mysql
疯一样的码农2 小时前
Maven Surefire 插件简介
java·maven
疯一样的码农2 小时前
Maven 仓库
java·maven
ᝰꫝꪉꪯꫀ3612 小时前
JavaWeb——Maven高级
java·后端·maven·springboot
kiddkid3 小时前
RabbitMQ高级
java·rabbitmq·java-rabbitmq