引入dataStore

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文件顶部生成

val Context.dataStore: DataStore<Preferences> by preferencesDataStore(name = "settings")

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

若需求从sp搬迁,则需调用创立办法:

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的操作。

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上的一个扩展办法所生成的文件。

public fun Context.preferencesDataStoreFile(name: String): File =
    this.dataStoreFile("$name.preferences_pb")

终究调用如下:

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

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中获取咱们需求的数据流:

private val testKeyFlow = settingStore.data.map { preferences ->
    preferences[testKey]
}

现在就能够用运用flow的办法去获取其中的数据

GlobalScope.launch {
    val value = testKeyFlow.firstOrNull() ?: ""
}

更新值:

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

GlobalScope.launch {
    dataStore.edit {
        it[testKey] = value
    }
}

多进程支撑

DataStore1.1.0-alpha版别支撑多进程更新数据,多进程的创立需求经过MultiProcessDataStoreFactory创立:

创立办法有两种:

createFromStorage

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,仅支撑单进程。

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进行文件读写的完成

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

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:

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的序列化读取。

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

object PreferencesSerializer : OkioSerializer<Preferences>

而OkioSerializer与Serializer没有任何关联

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即可

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作业流程:

Android DataStore及多进程使用

增加依赖项

运用Proto DataStore,需求修正build.gradle

  • 增加协议缓冲区插件

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

  • 装备协议缓冲区

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

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言语的数据类。

运用混杂的话,需求在混杂文件中增加:

-keepclassmembers class * extends androidx.datastore.preferences.protobuf.GeneratedMessageLite {
    <fields>;
}

创立Serializer

数据目标描绘数据如何在文件中存储,详细的目标序列化与反序列化需求经过Serializer进行:

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:

private val dataStore = DataStoreFactory.create(serializer = UserPreferenceSerializer){
    context.dataStoreFile("user_prefs.pb")
}

获取数据

相似PreferencesDataStore,不过无需再界说key,能够直接经过flow获取该数据目标的值


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对应数据 的当前状态。并将偏好目标转换为构建器,设置新值,并构建新的偏好。

GlobalScope.launch {
    dataStore.updateData {
        it.toBuilder().setShowCompleted(false).build()
    }
}

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