likes
comments
collection
share

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

作者站长头像
站长
· 阅读数 4

一、 概述

众所周知,用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来访问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: ObjectConverter<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: ObjectConverter<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: ObjectConverter<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到文件名中。 将不同用户的数据保存到不同的实例中。 具体的做法,就是拼接uid到路径或者文件名上。

对于方案1,不同用户的数据会糅合到一个文件中,对性能有影响:

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

并且,方案2在使用上更加方便一些:在读写value时不需要主动传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 fun1ABTestGroup by int("fun1_ab_test_group")
    
    // 服务端下发的配置项
    val setting by combineKV("setting")
}

五、小结

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

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