Android Jetpack 系列(四)DataStore 数据存储
try {DataStore 是 Google 面向现代 Android 架构推出的响应式本地数据存储方案,它通过协程、Flow 和类型安全特性,为开发者提供更强的能力和更清晰的数据管理模型。但这并不意味着 SharedPreferences 立即过时。两者各有适用场景:如果你正在开发一个新的 MVVM 架构项目,推荐使用 DataStore,它能很好地与 ViewModel、StateFlow
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")] ?: true
return 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")] ?: 0
return 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.username
settings.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.User
get() = 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")
@Volatile
private 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.applicationContext
INSTANCE = 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 {
@JvmOverloads
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 =
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)
}
@JvmOverloads
public 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 {
……
@JvmOverloads
public 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.currentState
val 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: T
when (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 = 0
storageConnection.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.readException
is 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.readException
is Data<T> -> it.value
is 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")
@Volatile
private var INSTANCE: DataStore<Preferences>? = null
override 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")
@Volatile
private var INSTANCE: DataStore<T>? = null
override fun getValue(thisRef: Context, property: KProperty<*>): DataStore<T> {
return INSTANCE ?: synchronized(lock) {
if (INSTANCE == null) {
val applicationContext = thisRef.applicationContext
INSTANCE = 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 的引入是一次本地数据持久化的升级,它不只是替代品,更是现代响应式架构的一部分。
更多详细的 DataStore 介绍,请访问 Android 开发者官网。
魔乐社区(Modelers.cn) 是一个中立、公益的人工智能社区,提供人工智能工具、模型、数据的托管、展示与应用协同服务,为人工智能开发及爱好者搭建开放的学习交流平台。社区通过理事会方式运作,由全产业链共同建设、共同运营、共同享有,推动国产AI生态繁荣发展。
更多推荐

所有评论(0)