一种好用的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存储框架。
例如 FastKVKVData 也是按照这套方案封装的。

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

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

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 159,569评论 4 363
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,499评论 1 294
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 109,271评论 0 244
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 44,087评论 0 209
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,474评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,670评论 1 222
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,911评论 2 313
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,636评论 0 202
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,397评论 1 246
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,607评论 2 246
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,093评论 1 261
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,418评论 2 254
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,074评论 3 237
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,092评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,865评论 0 196
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,726评论 2 276
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,627评论 2 270

推荐阅读更多精彩内容