注册

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

一、 概述


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

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

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


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

项目中是基于SharePreferences封装的,但这套方案也适用于其他类型的KV存储框架。


二、 封装方法


此方案封装了两类委托:



  1. 基础类型

    基础类型包括 [boolean, int, float, long, double, String, Set<String>, Object] 等类型。

    其中,Set<String> 本可以通过 Object 类型囊括,

    但因为Set<String>是 SharePreferences 内置支持的类型,这里我们就直接内置支持了。
  2. 扩展key的基础类型

    基础类型的委托,定义属性时需传入常量的key,通过委托所访问到的是key对应的value

    而开发中有时候需要【常量+变量】的key,基础类型的委托无法实现。

    为此,方案中实现了一个 CombineKV 类。

    CombineKV通过组合[key+extKey]实现通过两级key来访问value的效果。

    此外,方案基于CombineKV封装了各种基础类型的委托,用于简化API,以及约束所访问的value的类型。

2.1 委托实现


基础类型BasicDelegate.kt

扩展key的基础类型: ExtDelegate.kt


这里举例一下基础类型中的Boolean类型的委托实现:


class BooleanProperty(private val key: String, private val defValue: Boolean) :
ReadWriteProperty<KVData, Boolean> {
override fun getValue(thisRef: KVData, property: KProperty<*>): Boolean {
return thisRef.kv.getBoolean(key, defValue)
}

override fun setValue(thisRef: KVData, property: KProperty<*>, value: Boolean) {
thisRef.kv.putBoolean(key, value)
}
}

class NullableBooleanProperty(private val key: String) :
ReadWriteProperty<KVData, Boolean?> {
override fun getValue(thisRef: KVData, property: KProperty<*>): Boolean? {
return thisRef.kv.getBoolean(key)
}

override fun setValue(thisRef: KVData, property: KProperty<*>, value: Boolean?) {
thisRef.kv.putBoolean(key, value)
}
}

经典的 ReadWriteProperty 实现:

分别重写 getValue 和 setValue 方法,方法中调用KV存储的读写API。

由于kotlin区分了可空类型和非空类型,方案中也分别封装了可空和非空两种委托。


2.2 基类定义


实现了委托之后,我们将各种委托API封装到一个基类中:KVData


abstract class KVData {
// 存储接口
abstract val kv: KVStore

// 基础类型
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 stringSet(key: String, defValue: Set<String> = emptySet()) = StringSetProperty(key, defValue)
protected fun <T> obj(key: String, encoder: ObjectEncoder<T>, defValue: T) = ObjectProperty(key, encoder, defValue)

// 可空的基础类型
protected fun nullableBoolean(key: String) = NullableBooleanProperty(key)
protected fun nullableInt(key: String) = NullableIntProperty(key)
protected fun nullableFloat(key: String) = NullableFloatProperty(key)
protected fun nullableLong(key: String) = NullableLongProperty(key)
protected fun nullableDouble(key: String) = NullableDoubleProperty(key)
protected fun nullableString(key: String) = NullableStringProperty(key)
protected fun nullableStringSet(key: String) = NullableStringSetProperty(key)
protected fun <T> nullableObj(key: String, encoder: NullableObjectEncoder<T>) = NullableObjectProperty(key, encoder)

// 扩展key的基础类型
protected fun extBoolean(key: String, defValue: Boolean = false) = ExtBooleanProperty(key, defValue)
protected fun extInt(key: String, defValue: Int = 0) = ExtIntProperty(key, defValue)
protected fun extFloat(key: String, defValue: Float = 0f) = ExtFloatProperty(key, defValue)
protected fun extLong(key: String, defValue: Long = 0L) = ExtLongProperty(key, defValue)
protected fun extDouble(key: String, defValue: Double = 0.0) = ExtDoubleProperty(key, defValue)
protected fun extString(key: String, defValue: String = "") = ExtStringProperty(key, defValue)
protected fun extStringSet(key: String, defValue: Set<String> = emptySet()) = ExtStringSetProperty(key, defValue)
protected fun <T> extObj(key: String, encoder: ObjectEncoder<T>, defValue: T) = ExtObjectProperty(key, encoder, defValue)

// 扩展key的可空的基础类型
protected fun extNullableBoolean(key: String) = ExtNullableBooleanProperty(key)
protected fun extNullableInt(key: String) = ExtNullableIntProperty(key)
protected fun extNullableFloat(key: String) = ExtNullableFloatProperty(key)
protected fun extNullableLong(key: String) = ExtNullableLongProperty(key)
protected fun extNullableDouble(key: String) = ExtNullableDoubleProperty(key)
protected fun extNullableString(key: String) = ExtNullableStringProperty(key)
protected fun extNullableStringSet(key: String) = ExtNullableStringSetProperty(key)
protected fun <T> extNullableObj(key: String, encoder: NullableObjectEncoder<T>) = ExtNullableObjectProperty(key, encoder)

// CombineKV
protected fun combineKV(key: String) = CombineKVProperty(key)
}

使用时,继承KVData,然后实现kv, 返回一个KVStore的实现类即可。


举例,如果用SharedPreferences实现KVStore,可如下实现:


class SpKV(name: String): KVStore {
private val sp: SharedPreferences =
AppContext.context.getSharedPreferences(name, Context.MODE_PRIVATE)
private val editor: SharedPreferences.Editor = sp.edit()

override fun putBoolean(key: String, value: Boolean?) {
if (value == null) {
editor.remove(key).apply()
} else {
editor.putBoolean(key, value).apply()
}
}

override fun getBoolean(key: String): Boolean? {
return if (sp.contains(key)) sp.getBoolean(key, false) else null
}

// ...... 其他类型
}


更多实现可参考: SpKV


三、 使用方法


object LocalSetting : KVData("local_setting") {
override val kv: KVStore by lazy {
SpKV(name)
}
// 是否开启开发者入口
var enableDeveloper by boolean("enable_developer")

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

// id -> name 的映射。
val idToName by extNullableString("id_to_name")

// 收藏
val favorites by extStringSet("favorites")

var gender by obj("gender", Gender.CONVERTER, Gender.UNKNOWN)
}


定义委托属性的方法很简单:



  • 和定义变量类似,需要声明变量名类型
  • 和变量声明不同,需要传入key
  • 如果要定义自定义类型,需要传入转换器(实现字符串和对象类型的转换),以及默认值

基本类型的读写,和变量的读写一样。

例如:


fun test1(){
// 写入
LocalSetting.userId = 10001L
LocalSetting.gender = Gender.FEMALE

// 读取
val uid = LocalSetting.userId
val gender = LocalSetting.gender
}

读写扩展key的基本类型,则和Map的语法类似:


fun test2() {
if (LocalSetting.idToName[1] == null || LocalSetting.idToName[2] == null) {
Log.d("TAG", "Put values to idToName")
LocalSetting.idToName[1] = "Jonn"
LocalSetting.idToName[2] = "Mary"
} else {
Log.d("TAG", "There are values in idToName")
}
Log.d("TAG", "idToName values: " +
"1 -> ${LocalSetting.idToName[1]}, " +
"2 -> ${LocalSetting.idToName[2]}"
)
}

扩展key的基本类型,extKey是Any类型,也就是说,以上代码的[],可以传入任意类型的参数。


四、数据隔离


4.1 用户隔离


不同环境(开发环境/测试环境),不同用户,最好数据实例是分开的,相互不干扰。

比方说有 uid='001' 和 uid='002' 两个用户的数据,如果需要隔离两者的数据,有多种方法,例如:



  1. 拼接uid到key中。

    如果是在原始的SharePreferences的基础上,是比较好实现的,直接put(key+uid, value)即可;

    但是如果用委托属性定义,可以用上面定义的扩展key的类型。


  2. 拼接uid到文件名中。

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



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

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

    具体的做法,就是拼接uid到路径或者文件名上。



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



  • GlobalKV: 全局数据,切换环境和用户,不影响GlobalKV所访问的数据实例。
  • UserKV: 用户数据,需要同时区分 “服务器环境“ 和 ”用户ID“。

open class GlobalKV(name: String) : KVData() {
override val kv: KVStore by lazy {
SpKV(name)
}
}

abstract class UserKV(
private val name: String,
private val userId: Long
) : KVData() {
override val kv: SpKV by lazy {
// 拼接UID作为文件名
val fileName = "${name}_${userId}_${AppContext.env.tag}"
if (AppContext.debug) {
SpKV(fileName)
} else {
// 如果是release包,可以对文件名做个md5,以便匿藏uid等信息
SpKV(Utils.getMD5(fileName.toByteArray()))
}
}
}

UserKV实例:


/**
* 用户信息
*/

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

// 返回当前用户的实例
fun get(): UserInfo {
return get(AppContext.uid)
}

// 根据uid返回对应的实例
@Synchronized
fun get(uid: Long): UserInfo {
return map.getOrPut(uid) {
UserInfo(uid)
}
}
}

var gender by intEnum("gender", Gender.CONVERTER)
var isVip by boolean("is_vip")

// ... 其他变量
}

UserKV的实例不能是单例(不同的uid对应不同的实例)。

因此,可以定义companion对象,用来缓存实例,以及提供获取实例的API。


保存和读取方法如下:

先调用get()方法获取,然后其他用法就和前面描述的用法一样了。


UserInfo.get().gender = Gender.FEMALE

val gender = UserInfo.get().gender

4.2 环境隔离


有一类数据,需要区分环境,但是和用户无关。

这种情况,可以用UserKV, 然后uid传0(或者其他的uid用不到的数值)。


/**
* 远程设置
*/

object RemoteSetting : UserKV("remote_setting", 0L) {
// 某项功能的AB测试分组
val fun1ABTestGr0up by int("fun1_ab_test_group")

// 服务端下发的配置项
val setting by combineKV("setting")
}

五、小结


通过属性委托封装KV存储的API,可使原来“类名 + 操作 + key”的方式,变更为“类名 + 属性”的方式,从而简化KV存储的使用。
另外,这套方案也提到了保存不同用户数据到不同实例的演示。


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


作者:呼啸长风
来源:juejin.cn/post/7323449163420303370

0 个评论

要回复文章请先登录注册