Android DataStore及多进程使用

引入dataStore

arduino 复制代码
implementation "androidx.datastore:datastore-preferences:1.1.0-alpha04"

目前dataStore1.1.0 alpha版本支持多进程使用,稳定版不支持多进程,如果不考虑多进程使用,则可以直接使用稳定版。

谷歌官方建议是,如果使用SharedPreferences,则可以考虑迁移到DataStore

DataStore一共有两种类型:PreferencesDataStore和ProtoDataStore。

  • Preferences DataStore: 使用键存储和访问数据。此实现不需要预定义的架构,也不确保类型安全。
  • Proto DataStore: 将数据作为自定义数据类型的实例进行存储。此实现要求您使用协议缓冲区来定义架构,但可以确保类型安全。

DataStore使用Kotlin实现,如果项目是纯java的话,需要使用rxJava配合。并集成对应rxJava版本的dataStore库。

注意事项

  1. 请勿在同一进程中为给定文件创建多个 DataStore 实例 ,否则会破坏所有 DataStore 功能。如果给定文件在同一进程中有多个有效的 DataStore 实例,DataStore 在读取或更新数据时将抛出 IllegalStateException
  2. DataStore 的通用类型必须不可变。更改 DataStore 中使用的类型会导致 DataStore 提供的所有保证都失效,并且可能会造成严重的、难以发现的 bug。强烈建议您使用可保证不可变性、具有简单的 API 且能够高效进行序列化的协议缓冲区。
  3. 切勿对同一个文件混用 SingleProcessDataStore MultiProcessDataStore 。如果您打算从多个进程访问 DataStore,请始终使用 MultiProcessDataStore

PreferencesDataStore

PreferencesDataStore,从名称上就能看出,用于替换SharedPreferences。PreferencesDataStore提供了内置的从SP迁移的功能,构造PreferencesDataStore时,提供你想要替换的SP文件名称,及需要迁移的key集合,即可自动从给定名称的SP生成一个PreferencesDataStore文件,在迁移完成之后,会删除原有的SP。

由于上方的注意事项,dataStore需要确保在一个应用的一个地方进行统一赋值,避免在多处生成实例。推荐是在某个kotlin文件顶部生成

ini 复制代码
val Context.dataStore: DataStore<Preferences> by preferencesDataStore(name = "settings")

调用上述实例就会生成一个文件名为settings.preferences_pb的datastore文件。

若需要从sp迁移,则需调用创建方法:

kotlin 复制代码
class PreferenceDataStoreFactory 
@JvmOverloads
public fun create(
    corruptionHandler: ReplaceFileCorruptionHandler<Preferences>? = null,
    migrations: List<DataMigration<Preferences>> = listOf(),
    scope: CoroutineScope = CoroutineScope(Dispatchers.IO + SupervisorJob()),
    produceFile: () -> File
)

corruptionHandler为文件读写失败时的回调

migrations即为数据合并处理的操作 ,sp可以使用内置的SharedPreferencesMigration获取针对sp的操作。

kotlin 复制代码
public fun SharedPreferencesMigration(
    context: Context,
    sharedPreferencesName: String,
    keysToMigrate: Set<String> = MIGRATE_ALL_KEYS,
): SharedPreferencesMigration<Preferences>

keysToMigrate即是需要合并的key集合,默认为所有的Key。

scope为文件操作所在的协程作用域,一般使用默认即可。

produceFile需返回一个文件,该文件即为该PreferencesDataStore所对应的文件。这里需要注意指定文件后缀,如果此处返回的文件后缀不是以preferences_pb结尾,则在生成时,会抛出异常。所以我们使用dataStore库中在context上的一个扩展方法所生成的文件。

kotlin 复制代码
public fun Context.preferencesDataStoreFile(name: String): File =
    this.dataStoreFile("$name.preferences_pb")

最终调用如下:

ini 复制代码
private val settingStore:DataStore<Preferences> = PreferenceDataStoreFactory.create(corruptionHandler = null,
    migrations = listOf(
    SharedPreferencesMigration(MyApplication.mApplication,
        SP_FILE_NAME))
    , produceFile = {
        MyApplication.mApplication.preferencesDataStoreFile(DATA_STORE_NAME)
    }
)

获取值

dataStore使用专门的key类型来约定对应的值的数据类型,使用时,我们需要先定义出来我们需要的各个key

scss 复制代码
val testKey = stringPreferencesKey(keyName)//定义一个值为string的key,keyName即为该key的名称

类似sharedPreference,key类型还有

intPreferencesKey
doublePreferencesKey
booleanPreferencesKey
floatPreferencesKey
longPreferencesKey
stringSetPreferencesKey
及独有的
byteArrayPreferencesKey

在同一个keyName上使用不同类型Key去获取值,会导致ClassCastException。

PreferencesDataStore使用kotlin flow去处理数据的获取,因此我们首先需要使用定义好的key从dataStore中获取我们需要的数据流:

ini 复制代码
private val testKeyFlow = settingStore.data.map { preferences ->
    preferences[testKey]
}

现在就可以用使用flow的方法去获取其中的数据

ini 复制代码
GlobalScope.launch {
    val value = testKeyFlow.firstOrNull() ?: ""
}

更新值:

更新值,只需在dataStore上调用edit方法即可:

css 复制代码
GlobalScope.launch {
    dataStore.edit {
        it[testKey] = value
    }
}

多进程支持

DataStore1.1.0-alpha版本支持多进程更新数据,多进程的创建需要通过MultiProcessDataStoreFactory创建:

创建方法有两种:

createFromStorage

kotlin 复制代码
public object MultiProcessDataStoreFactory 

public fun <T> create(
    storage: Storage<T>,
    corruptionHandler: ReplaceFileCorruptionHandler<T>? = null,
    migrations: List<DataMigration<T>> = listOf(),
    scope: CoroutineScope = CoroutineScope(Dispatchers.IO + SupervisorJob())
): DataStore<T> = DataStoreImpl<T>(
    storage = storage,
    initTasksList = listOf(DataMigrationInitializer.getInitializer(migrations)),
    corruptionHandler = corruptionHandler ?: NoOpCorruptionHandler(),
    scope = scope
)

storage是dataStore定义的与文件系统交互的一个接口,库中有两种针对该接口的实现:OkioStorage与FileStorage。

OkioStorage

OkioStorage使用okio库实现了对文件的读写,更易使用,内部封装好了数据缓冲。但是官方默认实现的OkioStorage,仅支持单进程。

kotlin 复制代码
public class OkioStorage<T>(
    private val fileSystem: FileSystem,
    private val serializer: OkioSerializer<T>,
    private val producePath: () -> Path
) : Storage<T> {

    override fun createConnection(): StorageConnection<T> {
        
        return OkioStorageConnection(fileSystem, canonicalPath, serializer) {
            synchronized(activeFilesLock) {
                activeFiles.remove(canonicalPath.toString())
            }
        }
    }
}

internal class OkioStorageConnection<T>(
    private val fileSystem: FileSystem,
    private val path: Path,
    private val serializer: OkioSerializer<T>,
    private val onClose: () -> Unit
) : StorageConnection<T> {


    override val coordinator = createSingleProcessCoordinator()

 }

coordinator即为用于实现文件锁的工具。OkioStorage直接写为createSingleProcessCoordinator,返回单进程的文件锁,因此使用OkioStorage无法实现多进程方案,而且OkioStorage为final类,我们也无法通过覆写来更改使用的文件锁。

FileStorage

FileStorage为使用FileInputStream与FileOutputStream进行文件读写的实现

kotlin 复制代码
class FileStorage<T>(
    private val serializer: Serializer<T>,
    private val coordinatorProducer: (File) -> InterProcessCoordinator = {
        SingleProcessCoordinator()
    },
    private val produceFile: () -> File
) : Storage<T> {
}

coordinatorProducer即为文件锁参数,传入多进程版本文件锁即可实现多进程实现。

FileStorage第一个参数为处理数据如何写入文件以及如何从文件中读取数据的序列化工具,因此使用FileStorage生成跨进程dataStore的话,我们可以直接使用第二个多进程文件创建方法。因为第二个多进程文件创建方法等价于使用FileStorage。

createFromSerializer

kotlin 复制代码
public object MultiProcessDataStoreFactory 

public fun <T> create(
    serializer: Serializer<T>,
    corruptionHandler: ReplaceFileCorruptionHandler<T>? = null,
    migrations: List<DataMigration<T>> = listOf(),
    scope: CoroutineScope = CoroutineScope(Dispatchers.IO + SupervisorJob()),
    produceFile: () -> File
): DataStore<T> = DataStoreImpl<T>(
    storage = FileStorage(
        serializer,
        { MultiProcessCoordinator(scope.coroutineContext, it) },
        produceFile
    ),
    initTasksList = listOf(DataMigrationInitializer.getInitializer(migrations)),
    corruptionHandler = corruptionHandler ?: NoOpCorruptionHandler(),
    scope = scope
)

Serializer:

kotlin 复制代码
public interface Serializer<T> {

    /**
     * Value to return if there is no data on disk.
     */
    public val defaultValue: T

    /**
     * Unmarshal object from stream.
     *
     * @param input the InputStream with the data to deserialize
     */
    public suspend fun readFrom(input: InputStream): T

    /**
     *  Marshal object to a stream. Closing the provided OutputStream is a no-op.
     *
     *  @param t the data to write to output
     *  @output the OutputStream to serialize data to
     */
    public suspend fun writeTo(t: T, output: OutputStream)
}

该接口没有默认实现的工具,我们通过PreferenceDataStoreFactory创建Preference时,库中使用的有一个PreferencesSerializer实现了sp的序列化读取。

kotlin 复制代码
object PreferenceDataStoreFactory
public fun create(
    corruptionHandler: ReplaceFileCorruptionHandler<Preferences>? = null,
    migrations: List<DataMigration<Preferences>> = listOf(),
    scope: CoroutineScope = CoroutineScope(Dispatchers.IO + SupervisorJob()),
    produceFile: () -> File
): DataStore<Preferences> {
    val delegate = create(
        storage = OkioStorage(FileSystem.SYSTEM, PreferencesSerializer) {
            val file = produceFile()
            check(file.extension == PreferencesSerializer.fileExtension) {
                "File extension for file: $file does not match required extension for" +
                    " Preferences file: ${PreferencesSerializer.fileExtension}"
            }
            file.absoluteFile.toOkioPath()
        },
        corruptionHandler = corruptionHandler,
        migrations = migrations,
        scope = scope
    )
    return PreferenceDataStore(delegate)
}

但是PreferencesSerializer实现的接口是OkioSerializer

csharp 复制代码
object PreferencesSerializer : OkioSerializer<Preferences> 

而OkioSerializer与Serializer没有任何关联

kotlin 复制代码
public interface OkioSerializer<T> {

    /**
     * Value to return if there is no data on disk.
     */
    public val defaultValue: T

    /**
     * Unmarshal object from source.
     *
     * @param source the BufferedSource with the data to deserialize
     */
    public suspend fun readFrom(source: BufferedSource): T

    /**
     *  Marshal object to a Sink.
     *
     *  @param t the data to write to output
     *  @param sink the BufferedSink to serialize data to
     */
    public suspend fun writeTo(t: T, sink: BufferedSink)
}

所以目前sp多进程支持的话,我们需要自己实现Serializer,并使用MultiProcessDataStoreFactory的带Serializer参数的构造方法获取dataStore。

不过SP的序列化读写可以参考官方的PreferencesSerializer实现,只需替换输入输出流为inputStream与outputStream即可

kotlin 复制代码
class SharedPreferenceSerializer : Serializer<Preferences> {

    override val defaultValue: Preferences
        get() {
            return emptyPreferences()
        }

    override suspend fun readFrom(input: InputStream): Preferences {
        val preferencesProto = PreferencesMapCompat.readFrom(input)

        val mutablePreferences = mutablePreferencesOf()

        preferencesProto.preferencesMap.forEach { (name, value) ->
            addProtoEntryToPreferences(name, value, mutablePreferences)
        }

        return mutablePreferences.toPreferences()
    }

    override suspend fun writeTo(t: Preferences, output: OutputStream) {
        val preferences = t.asMap()
        val protoBuilder = PreferencesProto.PreferenceMap.newBuilder()

        for ((key, value) in preferences) {
            protoBuilder.putPreferences(key.name, getValueProto(value))
        }
        protoBuilder.build().writeTo(output)
    }

PreferencesMapCompat.readFrom为官方实现的序列化Preference的工具类,里面有做缓冲处理,所以可以直接使用即可。

addProtoEntryToPreferences与getValueProto为官方PreferencesSerializer中的方法,可以直接复用。

ProtoDataStore

ProtoDataStore使用DataStore和Protocol buffer处理数据。

Protocol Buffer,按谷歌的描述,他类似Json,但是更轻量,更快。你可以使用更自然的方式去描述你的数据如何组装在一起,然后你可以使用构造出来的代码,去编写组装数据的序列化读取和输出。

总的来说:协议缓冲区(Protocol Buffer)是定义语言(在.proto文件中创建)、proto编译器为与数据交互而生成的代码、特定于语言的运行时库以及写入文件(或通过网络连接发送)的数据的序列化格式的组合。

Protocol Buffer的优点:

  • 简洁的数据结构
  • 快速解析
  • 多语言支持
  • 自动生成的优化方法
  • 前后兼容的数据更新

Protocol Buffer不适用的情形:

  • 协议缓冲区倾向于假设整个消息可以一次加载到内存中,并且不超过对象图。对于超过少数兆字节的数据,请考虑一个不同的解决方案。在使用较大的数据时,由于串行副本,您可能有效地获得了几个数据副本,这可能会在内存使用中引起令人惊讶的尖峰。

  • 当协议缓冲区序列化时,相同的数据可以具有许多不同的二进制序列化。如果不完全解析它们,则不能比较两个消息以保持平等。

  • 消息未压缩。

  • 对于许多科学和工程用途,如涉及浮点数的大型,多维阵列的计算等,协议缓冲消息的大小和速度不具有优势。对于这些应用,FITS和类似格式的开销较少

  • 协议缓冲区对于非面向对象的语言没有做到良好的支持。

  • 协议缓冲消息并不能自我描述他们的数据。也就是说,如果你不能访问相应的.proto文件,就无法完全描述对应的数据。

  • 非正式标准,不具有法律效益。

Protocol Buffer工作流程:

添加依赖项

使用Proto DataStore,需要修改build.gradle

  • 添加协议缓冲区插件

  • 添加协议缓冲区和 Proto DataStore 依赖项

  • 配置协议缓冲区

rust 复制代码
plugins {
    ...
    id "com.google.protobuf" version "0.8.17"
}

dependencies {
    implementation  "androidx.datastore:datastore-core:1.0.0"
    implementation  "com.google.protobuf:protobuf-javalite:3.18.0"
    ...
}

protobuf {
    protoc {
        artifact = "com.google.protobuf:protoc:3.14.0"
    }

    // Generates the java Protobuf-lite code for the Protobufs in this project. See
    // https://github.com/google/protobuf-gradle-plugin#customizing-protobuf-compilation
    // for more information.
    generateProtoTasks {
        all().each { task ->
            task.builtins {
                java {
                    option 'lite'
                }
            }
        }
    }
}

创建数据对象

在项目的src/main目录下,新建proto目录,在该目录下创建文件UserPreferences.proto

ini 复制代码
syntax = "proto3";

option java_package = "com.codelab.android.datastore";
option java_multiple_files = true;

message UserPreferences {
  // filter for showing / hiding completed tasks
  bool show_completed = 1;
}

rebuild项目,完成之后,在build/generated/source.proto下,就能看到编译出的对应java语言的数据类。

使用混淆的话,需要在混淆文件中添加:

scala 复制代码
-keepclassmembers class * extends androidx.datastore.preferences.protobuf.GeneratedMessageLite {
    <fields>;
}

创建Serializer

数据对象描述数据如何在文件中存储,具体的对象序列化与反序列化需要通过Serializer进行:

kotlin 复制代码
object UserPreferenceSerializer :Serializer<UserPreferences>{
    override val defaultValue: UserPreferences
        get() = UserPreferences.getDefaultInstance()

    override suspend fun readFrom(input: InputStream): UserPreferences {
       return UserPreferences.parseFrom(input)
    }

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

使用protocol buffer进行数据的序列化与反序列化很简单,只需调用生成对象的输入与读取即可。

创建dataStore

有了Serializer,我们就可以直接创建对应的dataStore:

typescript 复制代码
private val dataStore = DataStoreFactory.create(serializer = UserPreferenceSerializer){
    context.dataStoreFile("user_prefs.pb")
}

获取数据

类似PreferencesDataStore,不过无需再定义key,可以直接通过flow获取该数据对象的值

kotlin 复制代码
val userPreferencesFlow: Flow<UserPreferences> = dataStore.data
    .catch { exception ->
        // dataStore.data throws an IOException when an error is encountered when reading data
        if (exception is IOException) {
            Log.e(TAG, "Error reading sort order preferences.", exception)
            emit(UserPreferences.getDefaultInstance())
        } else {
            throw exception
        }
    }

更新数据

写入数据通过dataStore提供的updateData方法,在此函数中以参数的形式获取 dataStore对应数据 的当前状态。并将偏好对象转换为构建器,设置新值,并构建新的偏好。

scss 复制代码
GlobalScope.launch {
    dataStore.updateData {
        it.toBuilder().setShowCompleted(false).build()
    }
}

updateData() 在读取-写入-修改原子操作中用事务的方式更新数据。直到数据持久存储在磁盘中,协程才会完成。

相关推荐
命运之手1 小时前
【Android】自定义换肤框架01之皮肤包制作
android·skin·skinner·换肤框架·不重启换肤
练习本1 小时前
android perfetto使用技巧梳理
android
GitLqr2 小时前
Android - 云游戏本地悬浮输入框实现
android·开源·jitpack
周周的Unity小屋2 小时前
Unity实现安卓App预览图片、Pdf文件和视频的一种解决方案
android·unity·pdf·游戏引擎·webview·3dwebview
单丽尔5 小时前
Gemini for China 大更新,现已上架 Android APP!
android
JerryHe6 小时前
Android Camera API发展历程
android·数码相机·camera·camera api
Synaric7 小时前
Android与Java后端联调RSA加密的注意事项
android·java·开发语言
程序员老刘·8 小时前
如何评价Flutter?
android·flutter·ios
JoyceMill9 小时前
Android 图像效果的奥秘
android
想要打 Acm 的小周同学呀11 小时前
ThreadLocal学习
android·java·学习