Android Jetpack 系列(四)DataStore 全面解析与实践
1. 简介
在前面的文章《Android SharedPreferences 全面介绍》中,我们全面分析了 SharedPreferences的使用方法和源码原理。它虽然简单强大,但在应对带有并发、系统扩展性需求的场景下,显得力不从心:
- 并发读写时数据不稳定
- apply() 是异步写入,但不能确保按顺序写入
- 不支持类型安全,只支持基础数据类型
- 不支持响应式读取
为了解决这些问题,Google 在 Jetpack 中推出了新一代本地数据存储方案:Jetpack DataStore。
DataStore 全部基于 Kotlin程协和 Flow 响应模型,可提供两种不同的实现方式:用于存储键值对的 Preferences DataStore 和 用于存储输入对象的 Proto DataStore。数据可支持采用异步、一致和事务的方式进行存储,大大提升了安全性、扩展性和符合 MVVM 设计的能力,是现代 Android 架构推荐使用的本地数据存储方式之一。
2. 了解 DataStore
功能 | SharedPreferences | PreferencesDataStore | ProtoDataStore |
异步 API | ✅(仅用于通过监听器读取更改的值) | ✅(通过 Flow) | ✅(通过 Flow) |
同步 API | ✅(但调用界面线程并不安全) | ❌ | ❌ |
可安全调用界面线程 | ❌ | ✅(系统会将工作转移到 Dispatchers.IO | ✅(系统会将工作转移到 Dispatchers.IO |
可以报告错误 | ❌ | ✅ | ✅ |
避免运行时异常 | ❌ | ✅ | ✅ |
具有高度一致性保证的事务性 API | ❌ | ✅ | ✅ |
处理数据迁移 | ❌ | ✅(通过 SharedPreferences) | ✅(通过 SharedPreferences) |
Preferences 与 Proto DataStore 比较
虽然 Preferences 和 Proto DataStore 都可保存数据,但其操作方式不同:
- PreferenceDatastore 与 SharedPreferences 一样,是基于键值来访问数据,无需预先定义模式。
- Proto DataStore 使用protocol-buffers定义模式。使用protocol-buffers支持存留强类型数据。 与 XML 及其他类似的数据格式相比,这种数据更快、更小、更简单且更明确。
Room 与 Datastore 比较
如果需要部分更新、引用完整性或大型/复杂数据集,则应考虑使用 Room 而不是 DataStore。DataStore 是小型或简单数据集的理想选择,不支持部分更新或引用完整性。
3. Preferences DataStore 键值对存储使用指南
3.1 添加项目依赖
dependencies {implementation "androidx.datastore:datastore-preferences:1.1.7"
}
3.2. 获取 DataStore 实例
val Context.dataStore: DataStore<Preferences> by preferencesDataStore(name = "settings")
注意:dataStore 应该是一个全局唯一实例,建议绑定在 Application 或 Context 扩展上。
3.3. 读取数据
suspend fun getFirstLaunch(context: Context): Boolean {val firstLaunch: Boolean = context.dataStore.data.first()[booleanPreferencesKey("first_launch")] ?: truereturn firstLaunch
}fun getFirstLaunchFlow(context: Context): Flow<Boolean> {val firstLaunchFlow: Flow<Boolean> = context.dataStore.data.map { it[booleanPreferencesKey("first_launch")] ?: true }return firstLaunchFlow
}suspend fun getUserName(context: Context): String {val userName: String = context.dataStore.data.first()[stringPreferencesKey("username")] ?: ""return userName
}fun getUserNameFlow(context: Context): Flow<String> {val userNameFlow: Flow<String> = context.dataStore.data.map { it[stringPreferencesKey("username")] ?: "" }return userNameFlow
}suspend fun getAge(context: Context): Int {val age: Int = context.dataStore.data.first()[intPreferencesKey("age")] ?: 0return age
}fun getAgeFlow(context: Context): Flow<Int> {val ageFlow: Flow<Int> = context.dataStore.data.map { it[intPreferencesKey("age")] ?: 0 }return ageFlow
}
说明:
读取数据的返回结果可以是标准类型也可以是Flow类型,如果是标准类型,必须要运行在suspend 作用域,如果是Flow类型,它能使其具备响应式能力,支持自动更新监听,可结合 ViewModel + StateFlow 使用。
3.4. 写入数据
suspend fun saveFirstLaunch(context: Context, firstLaunch: Boolean) {context.dataStore.edit { prefs -> prefs[booleanPreferencesKey("first_launch")] = firstLaunch }
}suspend fun saveUsername(context: Context, userName: String) {context.dataStore.edit { prefs -> prefs[stringPreferencesKey("username")] = userName }
}suspend fun saveAge(context: Context, age: Int) {context.dataStore.edit { prefs -> prefs[intPreferencesKey("age")] = age }
}
说明:
1. 所有写入都是原子操作
2. 写入操作也必须要去行在suspend 作用域。
3.5 监听数据变化
fun observeUserName(context: Context): Flow<String> {return context.dataStore.data.map { preferences ->preferences[stringPreferencesKey("username")] ?: ""}.distinctUntilChanged()
}lifecycleScope.launch {observeUserName(context).collect { userName ->Log.d("TAG", "userName change: $userName")}
}
1. 使用 data.map { }
搭配 distinctUntilChanged()
可以订阅特定键
2. 通过Flow.collect监听值变化
如果还有多个键需要监听,也可以用多个 map 和 distinctUntilChanged() 创建多个 Flow,各自独立监听不同的键。
data class UserInfo(val username: String,val age: Int
)fun observeUserInfo(context: Context): Flow<UserInfo> {val usernameFlow = context.dataStore.data.map { it[stringPreferencesKey("username")] ?: "" }.distinctUntilChanged()val ageFlow = context.dataStore.data.map { it[intPreferencesKey("age")] ?: 0 }.distinctUntilChanged()return combine(usernameFlow, ageFlow) { username, age ->UserInfo(username, age)}
}lifecycleScope.launch {observeUserInfo(context).collect { settings ->settings.usernamesettings.age}
}
3.6 其他常用操作
// 清空全部
suspend fun clearAll(context: Context) {context.dataStore.edit { it.clear() }
}// 删除指定键
suspend fun <T> remove(context: Context, key: Preferences.Key<T>) {context.dataStore.edit { it.remove(key) }
}// 是否存在
suspend fun contains(context: Context, key: Preferences.Key<String>): Boolean {return context.dataStore.data.first().contains(key)
}
fun containsFlow(context: Context, key: Preferences.Key<String>): Flow<Boolean> {return context.dataStore.data.map { it.contains(key) }
}
4. Proto DataStore 结构化存储方案使用指南
4.1. 添加项目依赖
dependencies {implementation "androidx.datastore:datastore:1.1.7"implementation " com.google.protobuf:protobuf-java:3.20.1"
}
4.2. 配置 proto 并生成类
DataStore进行对象的存储需要依赖 Protocol Buffers 先使对象序列化,在读取时再进行反序列化,关于 Protocol Buffers 的详细介绍可参考之前的文章《Android序列化(五) 之 Protocol Buffers》。
配置proto
syntax = "proto3";option java_package = "com.zyx.datastore.demo.proto";
option java_outer_classname = "UserProto";message User {string username = 1;int32 age = 2;bool is_premium = 3;
}
4.3. 创建 Proto DataStore 实例
定义 Serializer
object UserSerializer: Serializer<UserProto.User> {override val defaultValue: UserProto.Userget() = UserProto.User.getDefaultInstance()override suspend fun readFrom(input: InputStream): UserProto.User {try {return UserProto.User.parseFrom(input)} catch (e: InvalidProtocolBufferException) {throw CorruptionException("Cannot read proto", e)}}override suspend fun writeTo(t: UserProto.User, output: OutputStream) {t.writeTo(output)}
}
获取 DataStore 实例
val Context.userDataStore by dataStore(fileName = "user ", serializer = UserSerializer)
4.4. 读取数据
suspend fun getUser(context: Context): User {return context.userDataStore.data.first()
}fun getUserFlow(context: Context): Flow<User> {return context.userDataStore.data
}
4.5. 写入数据
suspend fun updateUsername(context: Context, newName: String) {context.userDataStore.updateData { current ->current.toBuilder().setUsername(newName).build()}
}suspend fun updateAge(context: Context, age: Int) {context.userDataStore.updateData { current ->current.toBuilder().setAge(age).build()}
}suspend fun updateUsernameAndAge(context: Context, newName: String, age: Int) {context.userDataStore.updateData { current ->current.toBuilder().setUsername(newName).setAge(age).build()}
}
5. 源码原理简析
5.1. 创建对象
以 Preferences 为例,前面我们在获取 DataStore 实例时,使用了以下代码:
val Context.dataStore: DataStore<Preferences> by preferencesDataStore(name = "settings")
这里的 preferencesDataStore 方法就是一个包级函数,代码如下:
PreferenceDataStoreDelegate.android.kt
@Suppress("MissingJvmstatic")
public fun preferencesDataStore(name: String,corruptionHandler: ReplaceFileCorruptionHandler<Preferences>? = null,produceMigrations: (Context) -> List<DataMigration<Preferences>> = { listOf() },scope: CoroutineScope = CoroutineScope(Dispatchers.IO + SupervisorJob())
): ReadOnlyProperty<Context, DataStore<Preferences>> {return PreferenceDataStoreSingletonDelegate(name, corruptionHandler, produceMigrations, scope)
}/*** Delegate class to manage Preferences DataStore as a singleton.*/
internal class PreferenceDataStoreSingletonDelegate internal constructor(private val name: String,private val corruptionHandler: ReplaceFileCorruptionHandler<Preferences>?,private val produceMigrations: (Context) -> List<DataMigration<Preferences>>,private val scope: CoroutineScope
) : ReadOnlyProperty<Context, DataStore<Preferences>> {private val lock = Any()@GuardedBy("lock")@Volatileprivate var INSTANCE: DataStore<Preferences>? = null/*** Gets the instance of the DataStore.** @param thisRef must be an instance of [Context]* @param property not used*/override fun getValue(thisRef: Context, property: KProperty<*>): DataStore<Preferences> {return INSTANCE ?: synchronized(lock) {if (INSTANCE == null) {val applicationContext = thisRef.applicationContextINSTANCE = PreferenceDataStoreFactory.create(corruptionHandler = corruptionHandler,migrations = produceMigrations(applicationContext),scope = scope) {applicationContext.preferencesDataStoreFile(name)}}INSTANCE!!}}
}
方法返回 ReadOnlyProperty 对象,可见在getValue方法里面调用了 PreferenceDataStoreFactory.create 方法去创建DataStore对象,其中 applicationContext.preferencesDataStoreFile(name) 就是指定文件存储位置,即:/data/data/【包名】/files/datastore/【自定义名称】.preferences_pb。
PreferenceDataStoreFactory.jvmAndroid.kt
public actual object PreferenceDataStoreFactory {@JvmOverloadspublic 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 =FileStorage(PreferencesFileSerializer) {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},corruptionHandler = corruptionHandler,migrations = migrations,scope = scope)return PreferenceDataStore(delegate)}@JvmOverloadspublic actual fun create(storage: Storage<Preferences>,corruptionHandler: ReplaceFileCorruptionHandler<Preferences>?,migrations: List<DataMigration<Preferences>>,scope: CoroutineScope,): DataStore<Preferences> {return PreferenceDataStore(DataStoreFactory.create(storage = storage,corruptionHandler = corruptionHandler,migrations = migrations,scope = scope))}
……
}
create方法会去创建了一个 DataStore<Preferences> 对象,通过委托的方式返回一个 PreferenceDataStore 对象,而实际上对象是通过 DataStoreFactory.create 方法进行创建
DataStoreFactory.jvm.kt
public actual object DataStoreFactory {
……@JvmOverloadspublic actual fun <T> create(storage: Storage<T>,corruptionHandler: ReplaceFileCorruptionHandler<T>?,migrations: List<DataMigration<T>>,scope: CoroutineScope,): DataStore<T> =DataStoreImpl(storage = storage,corruptionHandler = corruptionHandler ?: NoOpCorruptionHandler(),initTasksList = listOf(DataMigrationInitializer.getInitializer(migrations)),scope = scope)
}
到此能发现其最终实现的类就是 DataStoreImpl。
看回 PreferenceDataStore 对象,updateData 方法的 transform 参数,便是外部进行写入数据时调用 edit 方法所传入的 transform。
PreferenceDataStoreFactory.kt
internal class PreferenceDataStore(private val delegate: DataStore<Preferences>) :DataStore<Preferences> by delegate {override suspend fun updateData(transform: suspend (t: Preferences) -> Preferences):Preferences {return delegate.updateData {val transformed = transform(it)(transformed as MutablePreferences).freeze()transformed}}
}
Preferences.kt
public suspend fun DataStore<Preferences>.edit(transform: suspend (MutablePreferences) -> Unit
): Preferences {return this.updateData {it.toMutablePreferences().apply { transform(this) }}
}
5.2. 写入数据
紧接上面源码分析,updateData 方法除了调用外部实现的 transform(it) 外还会有自己内部逻辑,继续来看 DataStoreImpl 类的实现:
DataStoreImpl.kt
override suspend fun updateData(transform: suspend (t: T) -> T): T {val parentContextElement = coroutineContext[UpdatingDataContextElement.Companion.Key]parentContextElement?.checkNotUpdating(this)val childContextElement = UpdatingDataContextElement(parent = parentContextElement,instance = this)return withContext(childContextElement) {val ack = CompletableDeferred<T>()val currentDownStreamFlowState = inMemoryCache.currentStateval updateMsg =Message.Update(transform, ack, currentDownStreamFlowState, coroutineContext)writeActor.offer(updateMsg)ack.await()}
}
private val writeActor = SimpleActor<Message.Update<T>>(scope = scope,onComplete = {it?.let {inMemoryCache.tryUpdate(Final(it))}if (storageConnectionDelegate.isInitialized()) {storageConnection.close()}},onUndeliveredElement = { msg, ex ->msg.ack.completeExceptionally(ex ?: CancellationException("DataStore scope was cancelled before updateData could complete"))}
) { msg ->handleUpdate(msg)
}
这段代码中,重点关注 writeActor,它内部维护着一个消息队列,当新消息来到时会继续调用 handleUpdate 方法:
private suspend fun handleUpdate(update: Message.Update<T>) {update.ack.completeWith(runCatching {val result: Twhen (val currentState = inMemoryCache.currentState) {is Data -> { result = transformAndWrite(update.transform, update.callerContext)}is ReadException, is UnInitialized -> {if (currentState === update.lastState) {readAndInitOrPropagateAndThrowFailure()result = transformAndWrite(update.transform, update.callerContext)} else {throw (currentState as ReadException).readException}}is Final -> throw currentState.finalException}result})
}
private suspend fun transformAndWrite(transform: suspend (t: T) -> T,callerContext: CoroutineContext
): T = coordinator.lock {val curData = readDataOrHandleCorruption(hasWriteFileLock = true)val newData = withContext(callerContext) { transform(curData.value) }// Check that curData has not changed...curData.checkHashCode()if (curData.value != newData) {writeData(newData, updateCache = true)}newData
}
handleUpdate 方法再调用到 transformAndWrite 方法,该方法主要是给要进行写入的文件加锁,然后再调用 writeData 方法进行数据写入:
internal suspend fun writeData(newData: T, updateCache: Boolean): Int {var newVersion = 0storageConnection.writeScope {newVersion = coordinator.incrementAndGetVersion()writeData(newData)if (updateCache) {inMemoryCache.tryUpdate(Data(newData, newData.hashCode(), newVersion))}}return newVersion
}
storageConnection 对象是类 DataStoreImpl 上的变量,定义如下:
private val storageConnectionDelegate = lazy {storage.createConnection()
}
internal val storageConnection by storageConnectionDelegate
它的实现便是通过 storage.createConnection() 获取,而 storage 就是上面在创建对象调用 PreferenceDataStoreFactory.create 方法内传入的FileStorage(PreferencesFileSerializer),所以 writeScope 内的 writeData接口方法真正实现在 FileStorage.kt 中:
FileStorage.kt
internal class FileWriteScope<T>(file: File, serializer: Serializer<T>) :FileReadScope<T>(file, serializer), WriteScope<T> {override suspend fun writeData(value: T) {checkNotClosed()val fos = FileOutputStream(file)fos.use { stream ->serializer.writeTo(value, UncloseableOutputStream(stream))stream.fd.sync()}}
}
这里获取文件输出流,并通过Serializer 的 writeTo 方法进行写入数据。也从上面创建对象调用PreferenceDataStoreFactory.create 方法内传入的的FileStorage(PreferencesFileSerializer)可知,该方法的实现在 PreferencesFileSerializer 中:
PreferencesFileSerializer.jvmAndroid.kt
@Suppress("InvalidNullabilityOverride") // Remove after b/232460179 is fixed
@Throws(IOException::class, CorruptionException::class)
override suspend fun writeTo(t: Preferences, output: OutputStream) {val preferences = t.asMap()val protoBuilder = PreferenceMap.newBuilder()for ((key, value) in preferences) {protoBuilder.putPreferences(key.name, getValueProto(value))}protoBuilder.build().writeTo(output)
}
方法内是通过使用 PreferenceMap 来实现数据的写入,PreferenceMap 的特点是:
1. Map 的 key 是泛型的 Preferences.Key<T>,value 是 Any?,允许不同类型的值共存;
2. PreferenceMap 的内部序列化也是使用 Protocol Buffer 实现。
5.3. 读取数据
获取数据是通过 data 返回一个 flow 对象,每当调用 .data.first() 或 .data.collect {} 时,就会触发这个 Flow 的执行流程。
DataStoreImpl.kt
override val data: Flow<T> = flow {val startState = readState(requireLock = false)when (startState) {is Data<T> -> emit(startState.value)is UnInitialized -> error(BUG_MESSAGE)is ReadException<T> -> throw startState.readExceptionis Final -> return@flow}emitAll(inMemoryCache.flow.onStart { incrementCollector() }.takeWhile {it !is Final}.dropWhile { it is Data && it.version <= startState.version }.map {when (it) {is ReadException<T> -> throw it.readExceptionis Data<T> -> it.valueis Final<T>,is UnInitialized -> error(BUG_MESSAGE)}}.onCompletion { decrementCollector() })
}
1. 用 readState() 从缓存或文件中读取当前状态,结果是 State<T> 的一个子类;
2. 判断读取结果类型并处理,正常读取到的数据,直接发射(emit)出去;
3. emitAll是监听数据变化并持续发射新值,从内存缓存 (inMemoryCache) 中持续观察数据变化。
6. 多进程支持
DataStore 默认是单进程安全的。在早期版本(1.0.x)中,它并未设计为支持多进程并发访问,因此在多进程环境中同时读写可能引发数据不一致或竞争问题。
从 androidx.datastore:datastore-core:1.1.0-alpha01 起,官方引入了对多进程的支持,提供了 MultiProcessDataStoreFactory 用于创建具备多进程访问能力的 DataStore 实例。
与单进程下提供的 preferencesDataStore 和 dataStore 这类简洁扩展方法不同,官方并未为多进程场景提供类似封装。不过我们可以参考其实现方式,自定义多进程版本的封装方法。
6.1. Preferences DataStore
可以参照 preferencesDataStore 的实现思路,封装一个 preferencesDataStoreMulti 方法:
fun preferencesDataStoreMulti(name: String): ReadOnlyProperty<Context, DataStore<Preferences>> {return PreferenceDataStoreMultiDelegate(name)
}internal class PreferenceDataStoreMultiDelegate internal constructor(private val fileName: String) :ReadOnlyProperty<Context, DataStore<Preferences>> {private val lock = Any()@GuardedBy("lock")@Volatileprivate var INSTANCE: DataStore<Preferences>? = nulloverride fun getValue(thisRef: Context, property: KProperty<*>): DataStore<Preferences> {return INSTANCE ?: synchronized(lock) {if (INSTANCE == null) {val applicationContext = thisRef.applicationContext// 由于 PreferencesFileSerializer 是 internal object,只能通过反射获取val clazz = Class.forName("androidx.datastore.preferences.core.PreferencesFileSerializer")val serializer = clazz.getField("INSTANCE").get(null) as Serializer<Preferences>INSTANCE = MultiProcessDataStoreFactory.create(serializer = serializer,produceFile = {applicationContext.dataStoreFile(fileName)})}INSTANCE!!}}
}
然后即可像使用单进程的 preferencesDataStore 一样,定义全局多进程安全的 DataStore 实例:
val Context.dataStoreMulti: DataStore<Preferences> by preferencesDataStoreMulti(name = "multi_settings")
6.2. Proto DataStore
Proto DataStore 的多进程封装方式类似,也可参考单进程的 dataStore 扩展方法封装如下
fun <T> dataStoreMulti(fileName: String, serializer: Serializer<T>): ReadOnlyProperty<Context, DataStore<T>> {return DataStoreMultiDelegate(fileName, serializer)
}internal class DataStoreMultiDelegate<T> internal constructor(private val fileName: String, private val serializer: Serializer<T>) : ReadOnlyProperty<Context, DataStore<T>> {private val lock = Any()@GuardedBy("lock")@Volatileprivate var INSTANCE: DataStore<T>? = nulloverride fun getValue(thisRef: Context, property: KProperty<*>): DataStore<T> {return INSTANCE ?: synchronized(lock) {if (INSTANCE == null) {val applicationContext = thisRef.applicationContextINSTANCE = MultiProcessDataStoreFactory.create(serializer = serializer,produceFile = {applicationContext.dataStoreFile(fileName)})}INSTANCE!!}}
}
然后即可像使用单进程的 DataStore一样,定义全局多进程安全的 DataStore 实例:
val Context.userDataStoreMulti by dataStoreMulti(fileName = "multi_user", serializer = UserSerializer)
7. 总结
DataStore 是 Google 面向现代 Android 架构推出的响应式本地数据存储方案,它通过协程、Flow 和类型安全特性,为开发者提供更强的能力和更清晰的数据管理模型。
但这并不意味着 SharedPreferences 立即过时。两者各有适用场景:
-
如果你正在开发一个新的 MVVM 架构项目,推荐使用 DataStore,它能很好地与 ViewModel、StateFlow 等 Jetpack 组件协同工作,且具有良好的并发与响应式特性。
-
如果你的项目已经大量使用 SharedPreferences,且数据量不大、读写简单、依赖不想扩大,那么保留 SharedPreferences 依然是可行的选择。
-
如果需要结构化、跨模块的缓存或配置中心系统,则优先考虑 Proto DataStore。
最佳实践建议:
-
Preferences DataStore 可用于替代 SharedPreferences 的简单键值对配置;
-
Proto DataStore 更适合管理复杂结构的配置和缓存;
-
SharedPreferences 可作为过渡或轻量场景的方案继续使用。
总之,在 Jetpack 架构体系下,DataStore 的引入是一次本地数据持久化的升级,它不只是替代品,更是现代响应式架构的一部分。