Protocol Buffer数据对象本地缓存方案——Proto DataStore

1,972 阅读5分钟

DataStore是Jetpack中新增的数据存储解决方案,旨在取代 SharedPreferences。DataStore 基于 Kotlin 协程和 Flow 构建,以异步、一致的事务方式存储数据,并提供了以下两种不同的实现:

  • Preferences DataStore,用于存储键值对
  • Proto DataStore,用于存储类型化对象(Protocol Buffer)

DataStore和MMKV、数据库的选择

如果要处理键值对存储,已经在用MMKV的推荐继续使用,如果没有接入MMKV,那么也推荐接入MMKV,毕竟性能好效率高。

或者使用Preferences DataStore,作为谷歌官方推出的SharedPreferences键值对存储替代方案,不过本文不多做描述。

对于PB协议数据本地化存储,MMKV除了常规基础类型,只支持实现了Parcelable的对象存储,Protocol Buffer的对象要进行存储并没有现成方案,而Proto DataStore正好可以解决这个问题。

对于性能,键值对存储DataStore的效率一定是比MMKV差的,但Proto DataStore存储PB对象的场景和MMKV没有什么可比性,所以不需过于关注此问题,优先考虑实现方便程度(比如类型检查问题,Proto DataStore可以在编译期显示错误提示,相比MMKV、SP等运行时崩溃,编译时错误提示更好让开发人员发现问题)。

所以在项目中,根据不同场景选用不同存储方式:

  1. 常规键值对存储——MMKV
  2. 非大量数据的PB对象——Proto DataStore
  3. 大量复杂关系数据——数据库

Protobuf 协议前后版本兼容问题

协议缓存版本为旧版,最新代码+服务器版本为新版,存在以下情况:

  • 新版协议增加参数,使用缓存读取新参数,将会返回该参数类型对应的默认值
  • 新版协议删除已有参数,则新版代码也应该移除了对该参数的使用,否则报错
  • 新版协议修改已有参数

    • 仅修改参数名,则新版代码也应该修改了该参数的获取方法名,否则报错找不到该属性 从缓存数据中仍能被读取出来(因为protobuf协议中,每个参数都对应一个编号protoDataStore实质上是根据序号进行存储的,即修改序号对应的参数名,该项的数据仍然能读取到)

    • 修改参数类型,则新版代码也应该修改了该参数的使用类型,否则可能报错类型转换错误 从缓存中读取到的为该类型的默认值

可以看出,Proto DataStore遵循了Protobuf协议向前向后兼容的特性:对于新增或修改类型的参数(服务端一般不会修改已有参数的类型),从缓存中读取并不会引起错误崩溃,只需要我们处理好返回默认值的情况;而对于仅修改了名称的参数,甚至还能从缓存中读取到原先名字对应保存的数据。

这些特性都是专为存储Protobuf Buffer而存在的Proto DataStore所独有的,如果使用其他方案,比如将对象转为Byte存入MMVK,在取出并反序列化为对象后,还需要自行处理新旧对象间的差异,将是一件非常麻烦的事情。

接入和封装使用

引入项目gradle:

api('androidx.datastore:datastore:1.0.0-beta01') {
    exclude group: 'org.jetbrains.kotlinx', module: 'kotlinx-coroutines-core'
    exclude group: 'org.jetbrains.kotlinx', module: 'kotlinx-coroutines-core-jvm'
}

api('androidx.datastore:datastore-core:1.0.0-beta01') {
    exclude group: 'org.jetbrains.kotlinx', module: 'kotlinx-coroutines-core-jvm'
}

最新版本为1.0.0正式版,这里我选择了适用于自己项目内无需升级其它组件的版本为1.0.0-beta01,核心api都在 datastore-core库中,datastore主要增加了一些扩展方法,比如快速创建全局单一实例的dataStore对象,避免多个对象操作同一文件的问题。

常规使用方法:

  1. 创建全局单一实例DataStore对象
val Context.userInfoStore: DataStore<UserProto> by dataStore(
    fileName = "userInfo.pb",
    serializer = UserProtoSerializer
)

//创建序列化器
object UserProtoSerializer : Serializer<UserProto> {
    override suspend fun readFrom(input: InputStream): UserProto {
        try {
            return UserProto.parseFrom(input)
        } catch (exception: InvalidProtocolBufferException) {
            throw CorruptionException("Cannot read proto.", exception)
        }
    }

    override suspend fun writeTo(t: UserProto, output: OutputStream) {
        t.writeTo(output)
    }

    override val defaultValue: UserProto
        get() = UserProto.getDefaultInstance()
}
  1. 写入和读取数据
private fun dataStoreProto() {
    //将内容写入 Proto DataStore
    lifecycleScope.launch {
        context.userInfoStore.updateData {
            it.toBuilder()
                .setName("newName")
                .setAge(18)
                .build()
        }
    }

    //读取数据
    lifecycleScope.launch {
        val user = userInfoStore.data.first()
    }
}

封装后使用方法

封装后,只需在项目中DataStoreManager中声明要保存的DataStore实例和对象类型,比如此处我保存了个人信息和设置信息的协议对象数据:

object DataStoreManager {

    //退出登录时清空缓存
    fun clearAll() {
        GlobalScope.launch {
            dsMineInfo.update { it.toBuilder().clear().build() }
            dsSettingInfo.update { it.toBuilder().clear().build() }
        }
    }

    val dsMineInfo by lazy { ApplicationContext.getContext().dsMineInfo }
    val dsSettingInfo by lazy { ApplicationContext.getContext().dsSettingInfo }

}

/**
 * 个人信息
 */
private val Context.dsMineInfo by dataStore(
    fileName = MineInfoProto::class.simpleName.toString(),
    serializer = getSerializer(
        defaultValue = MineInfoProto.getDefaultInstance(),
        readFrom = { MineInfoProto.parseFrom(it) })
)

/**
 * 设置信息
 */
private val Context.dsSettingInfo by dataStore(
    fileName = SettingInfoProto::class.simpleName.toString(),
    serializer = getSerializer(
        defaultValue = SettingInfoProto.getDefaultInstance(),
        readFrom = { SettingInfoProto.parseFrom(it) })
)

在业务场景对应处使用扩展方法读取数据,此处也可以使用Flow关联LiveData

override fun onCreate() {
    super.onCreate()
    GlobalScope.launch(Dispatchers.IO) {
        val data = DataStoreManager.dsMineInfo.getValue()
        //使用数据
    }
}

在ViewModel中保存数据:

val data = //获取服务器协议数据
viewModel.viewModelScope.saveDataStore(DataStoreManager.dsMainInfo, data)
//或者
viewModel.viewModelScope.launch {
    DataStoreManager.dsMainInfo.save(data)
}

具体封装方法:

/**
 * 保存数据
 */
fun <T : GeneratedMessageLite> CoroutineScope.saveDataStore(
    dataStore: DataStore<T>?,
    data: T?,
    block: ((Boolean) -> Unit)? = null
) {
    this.launch {
        dataStore.save(data, block)
    }
}

suspend fun <T : GeneratedMessageLite> DataStore<T>?.save(
    data: T?,
    block: ((Boolean) -> Unit)? = null
) {
    this?.let { dataStore ->
        withContext(Dispatchers.IO) {
            try {
                val result = data?.let {
                    dataStore.updateData { data }
                    true
                } ?: false
                block?.let {
                    withContext(Dispatchers.Main) { it(result) }
                }
            } catch (e: Exception) {
                Logz.tag("DataStoreUtil").e(e.message)
                block?.let {
                    withContext(Dispatchers.Main) { it(false) }
                }
            }
        }
    }
}

/**
 * 更新数据
 */
suspend fun <T : GeneratedMessageLite> DataStore<T>?.update(
    block: ((Boolean) -> Unit)? = null,
    update: ((T) -> T)
) {
    this?.let { dataStore ->
        withContext(Dispatchers.IO) {
            try {
                dataStore.updateData {
                    update(it)
                }
                block?.let {
                    withContext(Dispatchers.Main) { it(true) }
                }
            } catch (e: Exception) {
                Logz.tag("DataStoreUtil").e(e.message)
                block?.let {
                    withContext(Dispatchers.Main) { it(false) }
                }
            }
        }
    }
}

/**
 * 读取数据
 */
suspend fun <T : GeneratedMessageLite> DataStore<T>?.getValue(): T? {
    return this?.let { dataStore ->
        withContext(Dispatchers.IO) {
            try {
                val data = dataStore.data.first()
                withContext(Dispatchers.Main) { data }
            } catch (e: Exception) {
                Logz.tag("DataStoreUtil").e(e.message)
                withContext(Dispatchers.Main) { null }
            }
        }
    }
}

/**
 * 读取Flow数据
 */
inline fun <reified T : GeneratedMessageLite> DataStore<T>?.getValueAsFlow(): Flow<T>? {
    return this?.let { dataStore ->
        dataStore.data.catch { exception ->
            if (exception is IOException) {
                emit(T::class.java.newInstance())
            } else {
                throw exception
            }
        }
    }
}

/**
 * 创建Protocol Buffer对应的Serializer
 */
internal fun <T : GeneratedMessageLite> getSerializer(
    defaultValue: T,
    readFrom: (input: InputStream) -> T
): Serializer<T> {
    return object : Serializer<T> {
        override val defaultValue: T
            get() = defaultValue

        override suspend fun readFrom(input: InputStream): T {
            try {
                return readFrom(input)
            } catch (exception: InvalidProtocolBufferException) {
                throw CorruptionException("Cannot read proto.", exception)
            }
        }

        override suspend fun writeTo(t: T, output: OutputStream) {
            t.writeTo(output)
        }
    }
}