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 开发者官网

Logo

魔乐社区(Modelers.cn) 是一个中立、公益的人工智能社区,提供人工智能工具、模型、数据的托管、展示与应用协同服务,为人工智能开发及爱好者搭建开放的学习交流平台。社区通过理事会方式运作,由全产业链共同建设、共同运营、共同享有,推动国产AI生态繁荣发展。

更多推荐