DataStore 简介

Jetpack DataStore 是一种经过改进的新数据存储解决方案,旨在取代 SharedPreferences。DataStore 基于 Kotlin 协程和 Flow 构建而成,提供以下两种不同的实现:

  • Preferences DataStore 用于键值对存储。数据以异步、一致和事务性的方式存储,有助于避免 SharedPreferences 的一些缺点。此实现不需要预定义的架构,也不确保类型安全。
  • Proto DataStore 用于存储类型化对象,数据作为自定义数据类型的实例进行存储。此实现要求您使用协议缓冲区来定义架构,但可以确保类型安全。 与 XML 和其他类似的数据格式相比,协议缓冲区速度更快、规格更小、使用更简单,并且更清楚明了。
功能 SharedPreferences PreferencesDataStore ProtoDataStore
异步 API ✅(仅用于通过监听器读取已更改的值) ✅(通过 Flow 以及 RxJava 2 和 3 Flowable) ✅(通过 Flow 以及 RxJava 2 和 3 Flowable)
同步 API ✅(但无法在界面线程上安全调用)
可在界面线程上安全调用 ✅(这项工作已在后台移至 Dispatchers.IO) ✅(这项工作已在后台移至 Dispatchers.IO)
可以提示错误
不受运行时异常影响
包含一个具有强一致性保证的事务性 API
处理数据迁移
类型安全 ✅ 使用协议缓冲区

SharedPreferences的缺陷:

  • SharedPreferences 有一个看上去可以在界面线程中安全调用的同步 API,但是该 API 实际上执行磁盘 I/O 操作。此外,apply() 会阻断 fsync() 上的界面线程。每次有服务启动或停止以及每次 activity 在应用中的任何地方启动或停止时,系统都会触发待处理的 fsync() 调用。界面线程在 apply() 调度的待处理 fsync() 调用上会被阻断,这通常会导致 ANR

  • SharedPreferences 还会将解析错误作为运行时异常抛出。

如果您当前在使用 SharedPreferences 存储数据,请考虑迁移到 DataStore

注意:如果您需要支持大型或复杂数据集、部分更新或参照完整性,请考虑使用 Room,而不是 DataStore。DataStore 的目的是存储简单的小型数据集, 但不支持部分更新或引用完整性。

为了正确使用 DataStore,请始终谨记以下规则:

  1. 请勿在同一进程中为给定文件创建多个 DataStore 实例,否则会破坏所有 DataStore
    功能。如果给定文件在同一进程中有多个有效的 DataStoreDataStore 在读取或更新数据时将抛出
    IllegalStateException

  2. DataStore 的通用类型必须不可变。更改 DataStore 中使用的类型会导致 DataStore 提供的所有保证失效,并且可能会造成严重的、难以发现的 bug。强烈建议您使用可保证不可变性、具有简单的 API
    且能够高效进行序列化的协议缓冲区。

  3. 切勿在同一个文件中混用 SingleProcessDataStore 和 MultiProcessDataStore。如果您打算从多个进程访问 DataStore,请始终使用 MultiProcessDataStore

Preferences DataStore 的使用

Preference DataStore API 类似于 SharedPreferences,但与后者相比存在一些显著差异:

  • 以事务方式处理数据更新
  • 公开表示当前数据状态的 Flow
  • 不提供存留数据的方法(apply()、commit())
  • 不返回对其内部状态的可变引用
  • 通过类型化键提供类似于 Map 和 MutableMap 的 API

添加依赖:

dependencies {
    implementation("androidx.datastore:datastore-preferences:1.0.0") 
}

Preferences DataStore 实现使用 DataStore 和 Preferences 类将简单的键值对保留在磁盘上。

创建 Preferences DataStore

使用由 preferencesDataStore 提供的属性委托来创建 Datastore<Preferences> 实例。只需在 Kotlin 文件顶层调用该实例一次,便可在应用的所有其余部分通过此属性访问该实例。这样可以更轻松地将 DataStore 保留为单例。

private const val USER_PREFERENCES_NAME = "user_preferences"

private val Context.dataStore by preferencesDataStore(
    name = USER_PREFERENCES_NAME
)

从 Preferences DataStore 读取数据

由于 Preferences DataStore 不使用预定义的架构,因此必须使用相应的键类型函数为需要存储在 DataStore<Preferences> 实例中的每个值定义一个键。例如,如需为 int 值定义一个键,请使用 intPreferencesKey()。然后,使用 DataStore.data 属性,通过 Flow 提供适当的存储值。

private object PreferencesKeys {
     val SHOW_COMPLETED = booleanPreferencesKey("show_completed")
     val EXAMPLE_COUNTER = intPreferencesKey("example_counter")
 }

 val counterFlow: Flow<Int> = context.dataStore.data.map { preferences ->
         preferences[EXAMPLE_COUNTER] ?: 0
 }
	
 val completeFlow: Flow<Boolean> = context.dataStore.data.map { preferences ->
        preferences[SHOW_COMPLETED] ?: false
}	

如果要读取的内容很多,可以定义一个data class来存储,在dataStore.data.map中返回该数据类对象即可:

data class UserPreferences(val count: Int, val show: Boolean)

val userPreferenceFlow = context.dataStore.data.map { preferences ->
    val count = preferences[EXAMPLE_COUNTER] ?: 0
    val show = preferences[SHOW_COMPLETED] ?: false
    UserPreferences(count, show)
}

处理读取数据时的异常

当 DataStore 从文件读取数据时,如果读取数据期间出现错误,系统会抛出 IOExceptions。我们可以通过以下方式处理这些事务:在 map() 之前使用 catch() Flow 运算符,并且在抛出的异常是 IOException 时发出 emptyPreferences()。如果出现其他类型的异常,最好重新抛出该异常。

val userPreferenceFlow = context.dataStore.data
    .catch { exception ->
        // dataStore.data throws an IOException when an error is encountered when reading data
        if (exception is IOException) {
            emit(emptyPreferences())
        } else {
            throw exception
        }
    }.map { preferences ->
        val count = preferences[EXAMPLE_COUNTER] ?: 0
        val show = preferences[SHOW_COMPLETED] ?: false
        UserPreferences(count, show)
    }

也可以选择在外面包裹一层 try-catch 进行处理。

将数据写入 Preferences DataStore

Preferences DataStore 提供了一个 edit() 函数,用于以事务方式更新 DataStore 中的数据。该函数的 transform 参数接受代码块,您可以在其中根据需要更新值。转换块中的所有代码均被视为单个事务。

 suspend fun updateShowCompleted(showCompleted: Boolean) {
     try {
         context.dataStore.edit { preferences ->
             val currentCounterValue = preferences[EXAMPLE_COUNTER] ?: 0
             preferences[EXAMPLE_COUNTER] = currentCounterValue + 1
             preferences[SHOW_COMPLETED] = showCompleted
         }
     } catch (e: IOException) {
         println(e)
     }
 }

如果在读取或写入磁盘时发生错误,edit() 可能会抛出 IOException。如果转换块中出现任何其他错误,edit() 将抛出异常。

结合 ViewModel 和 Compose 使用的完整示例

下面是一个在 Compose 中使用包含 ViewModel 、Repository 和 DataStore 的完整示例:

// DataStore.kt
private const val USER_PREFERENCES_NAME = "user_preferences"

private val Context.dataStore by preferencesDataStore(
    name = USER_PREFERENCES_NAME
)

private object PreferencesKeys {
    val SHOW_COMPLETED = booleanPreferencesKey("show_completed")
    val EXAMPLE_COUNTER = intPreferencesKey("example_counter")
}

data class UserPreferences(val count: Int, val show: Boolean)

class UserPreferencesRepository(val context: Context) {  

    val userPreferenceFlow = context.dataStore.data
        .catch { exception ->
            // dataStore.data throws an IOException when an error is encountered when reading data
            if (exception is IOException) {
                emit(emptyPreferences())
            } else {
                throw exception
            }
        }.map { preferences ->
            val count = preferences[EXAMPLE_COUNTER] ?: 0
            val show = preferences[SHOW_COMPLETED] ?: false
            UserPreferences(count, show)
        } 

    suspend fun updateShowCompleted(showCompleted: Boolean) {
        try {
            context.dataStore.edit { preferences ->
                val currentCounterValue = preferences[EXAMPLE_COUNTER] ?: 0
                preferences[EXAMPLE_COUNTER] = currentCounterValue + 1
                preferences[SHOW_COMPLETED] = showCompleted
            }
        } catch (e: IOException) {
            println(e)
        }
    }
}

class DataStoreViewModel(private val repository: UserPreferencesRepository): ViewModel() {

    val userPreference = repository.userPreferenceFlow

    fun updateShowCompleted(showCompleted: Boolean) {
        viewModelScope.launch { repository.updateShowCompleted(showCompleted) }
    }

    // Define ViewModel factory in a companion object
    companion object {
        val Factory: ViewModelProvider.Factory = viewModelFactory {
            initializer {
                val repository = (this[APPLICATION_KEY] as MyApp).userPreferencesRepository
                DataStoreViewModel(repository)
            }
        }
    }
}
// MyApp.kt
class MyApp: Application() {
    val userPreferencesRepository by lazy { UserPreferencesRepository(this)}
}
// DataStoreActivity.kt
class DataStoreActivity: ComponentActivity() {

    val viewModel by viewModels<DataStoreViewModel> { DataStoreViewModel.Factory }

    @OptIn(ExperimentalLifecycleComposeApi::class)
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContent {
            MyComposeApplicationTheme {
                Surface(
                    modifier = Modifier.fillMaxSize(),
                    color = MaterialTheme.colorScheme.background
                ) {
                    val userPreferences by viewModel.userPreference.collectAsStateWithLifecycle(UserPreferences(0, false))
                    Column {
                        Text("$userPreferences")
                        Button(onClick = {
                            viewModel.updateShowCompleted(true)
                        }) {
                            Text("更新userPreferences")
                        }
                    }
                }
            }
        }
    }
}

从 SharedPreferences 迁移到 Preferences DataStore

为了能够将SharedPreferences迁移到 DataStore,我们需要更新 DataStore 构建器以向迁移列表传入 SharedPreferencesMigrationDataStore 能够自动从 SharedPreferences 迁移到 DataStore。迁移需在 DataStore 中的任何数据访问操作可发生之前运行。这意味着,必须在 DataStore.data 发出任何值之前和 DataStore.edit() 可以更新数据之前,成功完成迁移。

private const val USER_PREFERENCES_NAME = "user_preferences"

private val Context.dataStore by preferencesDataStore(
    name = USER_PREFERENCES_NAME,
    produceMigrations = { context ->
        // Since we're migrating from SharedPreferences, add a migration based on the SharedPreferences name
        listOf(SharedPreferencesMigration(context, USER_PREFERENCES_NAME))
    }
)

注意:由于键只能从 SharedPreferences 迁移一次,因此在代码迁移到 DataStore 之后,您应停止使用旧 SharedPreferences

Proto DataStore 的使用

Proto DataStore 实现使用 DataStore 和协议缓冲区将类型化的对象保留在磁盘上。

SharedPreferencesPreferences DataStore 的一个缺点是无法定义架构,保证不了存取键时使用了正确的数据类型。Proto DataStore 可利用协议缓冲区定义架构来解决此问题。通过使用协议,DataStore 可以知道存储的类型,并且无需使用键便能提供类型。

接下来,我们看看如何将 Proto DataStore 和协议缓冲区添加到项目中中。

添加依赖项:

plugins {
    ...
    id "com.google.protobuf" version "0.8.18"
}

dependencies {
    implementation  "androidx.datastore:datastore:1.0.0"
    implementation  "com.google.protobuf:protobuf-javalite:3.21.12"
    implementation  "com.google.protobuf:protobuf-kotlin-lite:3.21.12"
    ...
}

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

    // 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'
                }
                kotlin {}
            }
        }
    }
}

协议缓冲区是一种对结构化数据进行序列化的机制。您只需对数据结构化的方式进行一次定义,编译器便会生成源代码,轻松写入和读取结构化数据。

创建 proto 文件

Proto DataStore 要求在 app/src/main/proto/ 目录的 proto 文件中保存预定义的架构。此架构用于定义您在 Proto DataStore 中保存的对象的类型。如需详细了解如何定义 proto 架构,请参阅 protobuf 语言指南

app/src/main/proto 目录中创建一个名为 user_prefs.proto 的新文件。添加内容如下:

syntax = "proto3";

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

message UserPreferences { 
  bool show_completed = 1;
  int32 example_counter = 2;
  string name = 3;
}

注意:UserPreferences 类在编译时会从 proto 文件中定义的 message 中生成。请务必重新构建该项目。

创建 Proto DataStore

创建 Proto DataStore 来存储类型化对象涉及两个步骤:

  1. 定义一个实现 Serializer<T> 的类,其中 Tproto 文件中定义的类型。此序列化器类会告知 DataStore 如何读取和写入您的数据类型。请务必为该序列化器添加默认值,以便在尚未创建任何文件时使用。
  2. 使用由 dataStore 创建的属性委托来创建 DataStore<T> 的实例,其中 Tproto 文件中定义的类型。在您的 Kotlin 文件顶层调用该实例一次,便可在应用的所有其余部分通过此属性委托访问该实例。filename 参数会告知 DataStore 使用哪个文件存储数据,而 serializer 参数会告知 DataStore1 步中定义的序列化器类的名称。

1. 创建序列化器

如需告知 DataStore 如何读取和写入我们在 proto 文件中定义的数据类型,我们需要实现序列化器。如果磁盘上没有数据,序列化器还会定义默认返回值。

创建一个名为 UserPreferencesSerializer 的新文件:

object UserPreferencesSerializer : Serializer<UserPreferences> {
    override val defaultValue: UserPreferences = UserPreferences.getDefaultInstance()
    override suspend fun readFrom(input: InputStream): UserPreferences {
        try {
            return UserPreferences.parseFrom(input)
        } catch (exception: InvalidProtocolBufferException) {
            throw CorruptionException("Cannot read proto.", exception)
        }
    }

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

提示:如果发现找不到 UserPreferences 对象或相关方法,请在项目目录右键选择 Reload from Disk , 若仍未找到,请执行 Clean and Rebuild,以确保协议缓冲区生成对象。

2. 创建 DataStore

为了创建 DataStore 实例,我们使用 dataStore 委托,并将 Context 作为接收器。此委托有两个必需参数:

  • DataStore 会处理的文件的名称。
  • DataStore 使用的类型的序列化器。我们使用前面定义的序列化器:UserPreferencesSerializer
private const val DATA_STORE_FILE_NAME = "user_prefs.pb"

private val Context.userDataStore: DataStore<UserPreferences> by dataStore(
    fileName = DATA_STORE_FILE_NAME,
    serializer = UserPreferencesSerializer
)

从 Proto DataStore 中读取数据

val exampleCounterFlow: Flow<Int> = context.userDataStore.data.map { preferences ->
        // The exampleCounter property is generated from the proto schema.
        preferences.exampleCounter
}

val userPreferencesFlow: Flow<UserPreferences> = context.userDataStore.data

读取数据时处理异常

由于 DataStore 从文件中读取数据,因此如果读取数据时出现错误,系统会抛出 IOException。我们可以使用 catch Flow 转换来处理这些异常,只需记录错误即可:

val userPreferencesFlow: Flow<UserPreferences> = context.userDataStore.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
        }
    }

将数据写入 Proto DataStore

Proto DataStore 提供了一个挂起函数 updateData() ,用于以事务方式更新存储的对象。updateData() 在读取-写入-修改原子操作中用事务的方式更新数据。一旦数据持久存储在磁盘中,协程便会完成。

suspend fun updateShowCompleted(completed: Boolean, name: String) {
     context.userDataStore.updateData { preferences ->
         preferences.toBuilder()
             .setShowCompleted(completed)
             .setExampleCounter(preferences.exampleCounter + 1)
             .setName(name)
             .build()
     }
 }

以 Json 序列化的方式使用 Proto DataStore

下面提供一种通过 Json 序列化的方式来使用 Proto DataStore ,可以不用创建proto文件,依然可以保证类型安全。

添加依赖:

plugins {
	id 'org.jetbrains.kotlin.plugin.serialization'
	...
}
dependencies {
	implementation  "androidx.datastore:datastore:1.0.0" 
	implementation "org.jetbrains.kotlinx:kotlinx-serialization-json:1.4.1"
}
// 根目录下添加
plugins {
	id 'org.jetbrains.kotlin.plugin.serialization' version '1.7.21'  apply false
}

定义Serializable的数据类:

@Serializable
data class UserInfo(
    val name: String = "未知",
    val age: Int = 0,
    val sex: Sex = Sex.MALE,
    val postList: List<PostInfo> = listOf()
)

@Serializable
data class PostInfo(val title: String, val time: Long)

enum class Sex { MALE, FEMALE }

定义 UserInfoSerializer 序列化器:

object UserInfoSerializer: Serializer<UserInfo> {
    override suspend fun readFrom(input: InputStream): UserInfo {
        return try {
            Json.decodeFromString(
                deserializer = UserInfo.serializer(),
                string = input.readBytes().decodeToString()
            )
        } catch (e: SerializationException) {
            e.printStackTrace()
            defaultValue
        }
    }

    override suspend fun writeTo(t: UserInfo, output: OutputStream) {
        output.write(
            Json.encodeToString(
                serializer = UserInfo.serializer(),
                value = t
            ).encodeToByteArray()
        )
    }

    override val defaultValue: UserInfo
        get() = UserInfo()
}

这里 UserInfoSerializer 使用Json.encodeToString()Json.decodeFromString() 进行序列化和反序列化。

定义 dataStore :

val Context.userInfoDataStore by dataStore("app-settings.json", UserInfoSerializer)

定义UserInfoViewModel

class UserInfoViewModel(val app : Application): ViewModel() {

    val userInfo: Flow<UserInfo> = app.userInfoDataStore.data

    fun updateUserInfo(user: UserInfo) {
        viewModelScope.launch {
            app.userInfoDataStore.updateData {
                user
            }
        }
    }

    fun addUserPostInfo(postInfo: PostInfo) {
        viewModelScope.launch {
            app.userInfoDataStore.updateData { userInfo ->
                val list = userInfo.postList.toMutableList().apply { add(postInfo) }
                userInfo.copy(postList = list)
            }
        }
    }

    // Define ViewModel factory in a companion object
    companion object {
        val Factory: ViewModelProvider.Factory = viewModelFactory {
            initializer {
                val app = this[APPLICATION_KEY]!!
                UserInfoViewModel(app)
            }
        }
    }
}

在Activity中调用:

class DataStoreProtoByJsonActivity: ComponentActivity() {
    val viewModel by viewModels<UserInfoViewModel> { UserInfoViewModel.Factory }

    @OptIn(ExperimentalLifecycleComposeApi::class)
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContent {
            MyComposeApplicationTheme {
                Surface(
                    modifier = Modifier.fillMaxSize(),
                    color = MaterialTheme.colorScheme.background
                ) {
                    val userInfo by viewModel.userInfo.collectAsStateWithLifecycle(UserInfo())
                    Column {
                        Text("$userInfo")
                        Button(onClick = {
                            val user = UserInfo("张三", 66, Sex.MALE,
                                    listOf(PostInfo("你好", System.currentTimeMillis()))
                                )
                            viewModel.updateUserInfo(user)
                            // viewModel.addUserPostInfo(PostInfo("还会", System.currentTimeMillis()))
                        }) {
                            Text("更新UserInfo")
                        }
                    }
                }
            }
        }
    }
}

在同步代码中使用 DataStore

DataStore 的主要优势之一是异步 API,但可能不一定始终能将周围的代码更改为异步代码。如果您使用的现有代码库采用同步磁盘 I/O,或者您的依赖项不提供异步 API,就可能出现这种情况。

Kotlin 协程提供 runBlocking() 协程构建器,以帮助消除同步与异步代码之间的差异。您可以使用 runBlocking() 从 DataStore 同步读取数据。以下代码会阻塞发起调用的线程,直到 DataStore 返回数据:

val exampleData = runBlocking { context.dataStore.data.first() }

对界面线程执行同步 I/O 操作可能会导致 ANR 或界面卡顿。您可以通过从 DataStore 异步预加载数据来减少这些问题:

override fun onCreate(savedInstanceState: Bundle?) {
    lifecycleScope.launch {
        context.dataStore.data.first()
        // You should also handle IOExceptions here.
    }
}

这样,DataStore 可以异步读取数据并将其缓存在内存中。以后使用 runBlocking() 进行同步读取的速度可能会更快,或者如果初始读取已经完成,可能也可以完全避免磁盘 I/O 操作。

在多进程代码中使用 DataStore

注意:DataStore 多进程功能目前仅在 1.1.0 Alpha 版中提供

您可以将 DataStore 配置为访问不同进程中的相同数据,并保证数据一致性与单个进程中相同。具体而言,DataStore 可保证:

  • 读取仅返回已持久存储到磁盘的数据。
  • 写入后读取的一致性。
  • 写入会序列化。
  • 写入绝不会阻塞读取。

假设有一个包含一项服务和一个 activity 的示例应用:

  1. 服务在单独的进程中运行,并会定期更新 DataStore
<service
  android:name=".MyService"
  android:process=":my_process_id" />
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
      scope.launch {
          while(isActive) {
              dataStore.updateData {
                  Settings(lastUpdate = System.currentTimeMillis())
              }
              delay(1000)
          }
      }
}
  1. 同时,应用会收集这些更改并更新其界面
val settings: Settings by dataStore.data.collectAsState()
Text(
  text = "Last updated: $${settings.timestamp}",
)
  1. 为了能够在不同进程中使用 DataStore,需要使用 MultiProcessDataStoreFactory 构造 DataStore 对象。
val dataStore: DataStore<Settings> = MultiProcessDataStoreFactory.create(
   serializer = SettingsSerializer(),
   produceFile = {
       File("${context.cacheDir.path}/myapp.preferences_pb")
   }
)
  1. serializer 会告知 DataStore 如何读取和写入您的数据类型。请务必为该序列化器添加默认值,以便在尚未创建任何文件时使用。以下是使用 kotlinx.serialization 的实现示例:
@Serializable
data class Settings(
   val lastUpdate: Long
)

@Singleton
class SettingsSerializer @Inject constructor() : Serializer<Settings> {

   override val defaultValue = Settings(lastUpdate = 0)

   override suspend fun readFrom(input: InputStream): Timer =
       try {
           Json.decodeFromString(
               Settings.serializer(), input.readBytes().decodeToString()
           )
       } catch (serialization: SerializationException) {
           throw CorruptionException("Unable to read Settings", serialization)
       }

   override suspend fun writeTo(t: Settings, output: OutputStream) {
       output.write(
           Json.encodeToString(Settings.serializer(), t)
               .encodeToByteArray()
       )
   }
}

您可以使用 Hilt 依赖项注入,以确保您的 DataStore 实例在每个进程中具有唯一性:

@Provides
@Singleton
fun provideDataStore(@ApplicationContext context: Context): DataStore<Settings> =
   MultiProcessDataStoreFactory.create(...)

DataStore VS MMKV

MMKV: 基于内存映射mmap的键值对存储

  • 1)速度快,是因为写的是内存
  • 2)支持多进程

MMKV出现的原因是解决微信聊天用户文字发生崩溃时,用来追溯崩溃发生的聊天文本,在显示每条文本到聊天页面之前先同步写入磁盘。所以MMKV适合的场景是高频、同步、磁盘写入。

SharedPreference读写可能存在卡顿(同步方式)。

DataStore读写都是在后台进行的,不存在卡顿,它使用协程来实现。MMKV有可能会造成数据丢失(内存中的数据没来得及写回磁盘)概率较低。

如果考虑支持多进程或高频写入需求,MMKV可能是唯一选择,否则DataStore是最佳选择,因为性能各方面比SharedPreference更完美。不过,DataStore最新的版本也开始支持多进程了。

proto 3 语法简介

protocol buffers 语言的最新版本是proto3版本,protocol buffers 是语言无关、平台无关的协议缓冲区语言。由于 Proto DataStore 是基于 Proto 文件的,因此有必要了解一下 proto 3 的语法规则。

protocol buffers 最新文档地址:https://protobuf.dev

简单例子:

syntax = "proto3";

message SearchRequest {
  string query = 1;
  int32 page_number = 2;
  int32 result_per_page = 3;
}
  • 第一行指定正在使用的protocol buffers语法版本这里是proto3,如果不指定,将默认为使用 proto2。这必须是文件的第一个非空、非注释行。

  • SearchRequest消息定义指定了三个字段(名称/值对),每个字段都有名称和类型。

  • 字段类型:常用标量类型对应Java/kotlin中的类型: int32 -> int int64->long float->float double->double string->string bytes->ByteString bool->boolean。字段类型也可以是复合类型,包括枚举 和其他消息类型。

  • 字段编号:每个字段名称右边的数字是唯一编号[1 - 15] 用一个字节进行编码, [16. 2047] 占用两个字节。最小字段编号为 1,最大值为2^29 - 1,即 536870911。 19000 到 19999 之间的数字无法使用。它们专用于协议缓冲区实现。

  • 字段规则

    • singular: 格式正确的消息可以有这个字段的零个或一个(但不能多于一个)。这是 proto3语法的默认字段规则。
    • optional:与 singular 相同,不过您可以检查该值是否明确设置。optional 字段处于以下两种可能状态之一:该字段已设置(会序列化),未设置字段(不会序列化)。
    • repeated:在格式正确的消息中,此字段类型可以重复零次或多次。
  • message: 消息类型, 同一.proto文件中可以指定多个 message。

  • 注释:.proto文件中支持///* ... */ 语法的注释

  • 保留字段:使用 reserved 可以指定已删除的字段名称或编号为已保留,确保将来用户修改了该字段会得到编译报错,避免导致严重问题。不能在同一 reserved 语句中混用字段名称和字段编号。

 message Foo {
  reserved 2, 15, 9 to 11;
  reserved "foo", "bar";
} 
  • 根据 .proto 文件生成的内容
    • 对于 Java,编译器会生成 .java 文件,其中包含每种消息类型的类,以及用于创建消息类实例的特殊 Builder 类。
    • 对于 Kotlin,除了 Java 生成的代码之外,编译器还会为每种消息类型生成一个 .kt 文件,其中包含可用于简化消息实例的创建 DSL。
  • 默认值:对于标量消息字段,在解析消息后,就无从判断字段是明确设为默认值(例如布尔值是否设为 false)还是根本不设置:在定义消息类型时,您应该记住这一点。
    • 对于字符串,默认值为空字符串。
    • 对于字节,默认值为空字节。
    • 对于布尔值,默认值为 false。
    • 对于数值类型,默认值为零。
    • 对于枚举,默认值为第一个定义的枚举值,必须为 0。
    • 对于消息字段,系统不会设置此字段。其确切值取决于语言。
    • 重复字段的默认值为空(通常,采用相应语言的空列表)。
  • 枚举:每个枚举定义必须包含一个映射到 0 的第一个常量作为其第一个元素。枚举器常量必须在 32 位整数范围内。将 allow_alias 选项设置为 true,允许别名冲突,序列化时始终使用第一个值。
enum Corpus {
  CORPUS_UNSPECIFIED = 0;
  CORPUS_UNIVERSAL = 1;
  CORPUS_WEB = 2;
  CORPUS_IMAGES = 3;
  CORPUS_LOCAL = 4;
  CORPUS_NEWS = 5;
  CORPUS_PRODUCTS = 6;
  CORPUS_VIDEO = 7;
}
message SearchRequest {
  string query = 1;
  int32 page_number = 2;
  int32 result_per_page = 3;
  Corpus corpus = 4;
}
  • 引用其他消息类型:
message SearchResponse {
  repeated Result results = 1;
}

message Result {
  string url = 1;
  string title = 2;
  repeated string snippets = 3;
}
  • 导入: 可以导入和引用在其他.proto文件中定义的消息类型,import "myproject/other_protos.proto"
  • 嵌套类型: 可以在其他消息类型中定义和使用消息类型。支持嵌套多层。
message SearchResponse {
  message Result {
    string url = 1;
    string title = 2;
    repeated string snippets = 3;
  }
  repeated Result results = 1;
}

如果您要在父消息类型之外重复使用消息类型,请将其引用为 Parent.Type

message SomeOtherMessage {
  SearchResponse.Result result = 1;
}
  • 更新消息类型
    如果现有消息类型不再满足您的所有需求(例如,您希望消息格式有一个额外的字段),但您仍想使用以旧格式创建的代码,不用担心!更新消息类型非常简单,不会破坏任何现有代码。只需记住以下规则即可:

    • 请勿更改任何现有字段的字段编号。

    • 如果您添加新字段,则任何使用“旧”消息格式将代码序列化的消息仍可通过新生成的代码解析。您应该记住这些元素的默认值,以便新代码可以与旧代码生成的消息正确交互。同样,新代码创建的消息也可以用旧代码解析:旧二进制文件在解析时会直接忽略新字段。

    • 您可以移除字段,只要未在更新后的消息类型中再次使用该字段编号即可。建议您重命名该字段,添加前缀“OBSOLETE_”,或将该字段编号预留,以免 .proto 的未来用户不小心重复使用该编号。

    • int32、uint32、int64、uint64 和 bool 都兼容,这意味着您可以将字段从一种类型更改为另一种类型,而不会破坏向前或向后兼容性。

    • sint32 和 sint64 彼此兼容,但与其他整数类型不兼容。

    • 只要字节是有效的 UTF-8,string 和 bytes 就兼容。

    • 如果字节包含编码版本的消息,则嵌入式消息与 bytes 兼容。

    • fixed32 兼容 sfixed32,fixed64 与 sfixed64 兼容。

    • 对于 string、bytes 和消息字段,单数字段与 repeated 字段兼容。给定重复字段的序列化数据作为输入,如果该字段是基元类型字段,则希望此字段为单数的客户端将获取最后一个输入值;如果该字段是消息类型字段,则将合并所有输入元素。请注意,这对于数字类型(包括布尔值和枚举值)通常不安全。数字类型的重复字段可以采用 packed 格式进行序列化,如果单数字段需要,则无法正确解析。

    • 在有线格式方面,enum 与 int32、uint32、int64 和 uint64 兼容(请注意,如果值不适合,它们会被截断)。但请注意,客户端在对消息进行反序列化时可能会以不同的方式处理它们:例如,无法识别的 proto3 enum 类型将保留在消息中,但当消息反序列化时,其表示方式取决于语言。Int 字段始终只保留它们的值。
      将单个 optional 字段或扩展更改为新 oneof 的成员与二进制文件兼容,但对于某些语言(特别是 Go),生成的代码的 API 将以不兼容的方式更改。因此,如 AIP-180 中所述,Google 不会在其公共 API 中做出此类更改。同样,在确定源代码兼容性时,如果您确定一次一次不设置多个代码,将多个字段移至新的 oneof 可能是安全的。将字段移到现有的 oneof 是不安全的。同样,将单个字段 oneof 更改为 optional 字段或扩展也是安全的。

  • 未知字段: 格式正确但解析器无法识别的字段。 3.5 及更高版本中,未知字段在解析期间会保留并包含在序列化输出中。

  • Any 类型: 如需使用 Any 类型,您需要导入 google/protobuf/any.proto

import "google/protobuf/any.proto";

message ErrorStatus {
  string message = 1;
  repeated google.protobuf.Any details = 2;
}

给定消息类型的默认类型网址为 type.googleapis.com/packagename.messagename

  • Oneof: 最多只能同时设置一个字段。设置任意一个成员后,系统会自动清除所有其他成员。如果设置了多个值,则由 proto 中的顺序确定的最后一个设置值将覆盖之前的所有值。如需在 .proto 中定义单一项,您可以使用 oneof 关键字,后跟您的名称。
message SampleMessage {
  oneof test_oneof {
    string name = 4;
    SubMessage sub_message = 9;
  }
}
  • Map: map<key_type, value_type> map_field = N 其中 key_type 可以是任何整数或字符串类型(因此,除浮点类型和 bytes 以外的任何标量类型)。请注意,枚举不是有效的 key_typevalue_type 可以是除其他映射外的任何类型。map<string, Project> projects = 3;

    • 映射字段不能为 repeated。
    • 映射值的线格式格式和映射迭代顺序尚未定义,因此您不能指望地图项按特定顺序排列。
    • 为 .proto 生成文本格式时,映射按键排序。数字键按数字顺序排序。
    • 通过有线连接或合并时,如果存在重复的映射键,则使用最后一个看到的键。从文本格式解析映射时,如果存在重复的键,解析可能会失败。
    • 如果您为某个映射字段提供了键但没有值,序列化此字段时的行为将取决于语言。在 C++、Java、Kotlin 和 Python 中,该类型的默认值已序列化,但在其他语言中则未序列化。
  • 向后兼容性: 此映射语法等效于线上传输的内容,因此不支持映射的协议缓冲区实现仍然可以处理您的数据:

message MapFieldEntry {
  key_type key = 1;
  value_type value = 2;
}

repeated MapFieldEntry map_field = N;

任何支持映射的协议缓冲区实现都必须生成并接受上述定义可以接受的数据。

  • package: 可以向 .proto 文件添加可选的 package 说明,在定义消息类型的字段时,您可以使用软件包说明符。在 Java 和 Kotlin 中,除非您在 .proto 文件中明确提供 option java_package,否则该软件包将用作 Java 软件包。
package foo.bar;
message Open { ... }
message Foo {
  ...
  foo.bar.Open open = 1;
  ...
}
  • 选项/google/protobuf/descriptor.proto 中定义了可用选项的完整列表。
    • java_package(文件选项):option java_package = "com.example.foo"; 要用于生成的 Java/Kotlin 类的软件包。如果 .proto 文件中未提供明确的 java_package 选项,则默认情况下将使用 proto 包(使用 .proto 文件中的“package”关键字指定)。但是,proto 软件包通常不构成良好的 Java 软件包,因为 proto 软件包不应以反向域名开头。如果不生成 Java 或 Kotlin 代码,则此选项无效。
    • java_outer_classname(文件选项):option java_outer_classname = "Ponycopter"; 您要生成的封装容器 Java 类的类名称(以及文件名)。如果 .proto 文件中未指定显式 java_outer_classname,则类名称将通过将 .proto 文件名转换为驼峰式大小写格式(因此,foo_bar.proto 变为 FooBar.java)来构造。如果 java_multiple_files 选项停用,则为该 .proto 文件生成的所有其他类/枚举等等都将在此嵌套封装容器 Java 类中生成为嵌套类/枚举等。如果不生成 Java 代码,此选项将不起作用。
    • java_multiple_files(文件选项):option java_multiple_files = true; 如果为 false,系统将为此 .proto 文件仅生成一个 .java 文件,为顶级消息、服务和枚举生成的所有 Java 类/枚举(例如如果不生成 Java 代码,则此选项无效。)
    • optimize_for(文件选项):option optimize_for = CODE_SIZE; 会对 C++ 和 Java 代码生成器产生影响。可以设置为 SPEED(代码经过高度优化)、CODE_SIZE(生成最少的类) 或 LITE_RUNTIME(依赖精简版运行时库)。

proto3 语法需要注意的事项,见下方代码中的注释

syntax = "proto3"; // 定义这个文件的语法是proto3、默认情况下是proto2 这个指定语法行必须是文件的非空非注释的第一个行。

// 申明一个包
package com.huan.proto;
option java_package = "com.huan.proto"; // 申明一个在 java 中使用的包,如果没有申明这个,则使用外层的 package 申明
option java_outer_classname = "PersonWrapper"; // 表示最后生成java的类名

// 定义一个消息体
// 1、下方每个字段后后面都有一个唯一的标识符,1,2,3,4....,这些标识符是用来在消息的二进制的识别各个字段的,一段开始使用就不可再次改变。
// 2、其中[1,15]的标识符在编码的时候会占一个字节,[16,2047]的标识符会占2个字节,因此我们应该为常用的字段的标识符在[1,15]之内。[19000-19999]为预留的标识符不可使用
// 默认值
// 1、对于strings,默认是一个空string
// 2、对于bytes,默认是一个空的bytes
// 3、对于bools,默认是false
// 4、对于数值类型,默认是0
// 5、对于枚举,默认是第一个定义的枚举值,必须为0;
// 6、对于消息类型(message),域没有被设置,确切的消息是根据语言确定的,

message Person {
    // 对应 java 中的 String 数据类型
    string personName = 1;
    // 对应 java 中的 int 数据类型
    int32 age = 2;
    // 对应 java 中的 double 数据类型
    double salary = 3;
    // 对应 java 中的 float 数据类型
    float weight = 4;
    // 对应 java 中的 boolean 数据类型
    bool isMarry = 5;
    // 对应 java 中的 long 数据类型
    sint64 createTime = 6;
    // 对应 java 中的 byte 数据类型
    bytes content = 7;
    // 对应 java 中的 枚举 数据类型
    SexEnum sex = 8;
    // 对应 java 中的 List 集合
    repeated string friends = 9;
    // 对应 java 中的 map 类型 [deprecated = true]表示这个字段已经被废弃了
    map<string, string> ext = 10 [deprecated = true];

    // 定义一个枚举,不建议在枚举中使用负数,因为枚举值是采用可变编码方式的。
    enum SexEnum {
        option allow_alias = true; // 当打开这个配置时,可以实现将不同的枚举常量指定为相同的值,比如下方的 WOMEN和NOT_KONWN
        MAN = 0; // 在枚举中,第一个值必须是 0
        WOMEN = 1;
        NOT_KNOWN = 1;
    }

    // oneof 表示的字段中,表示只有一个字段有值,那么在此处OnlyOneFieldHasValue下的三个字段同一时刻,只有一个字段有值,如果多次赋值,那么后面的值会覆盖前面的值
    oneof OnlyOneFieldHasValue {
        string username = 11;
        Person person = 12;
        SexEnum personSex = 13;
    }

    // reserved 标识的为保留字段,标识不可用,比如前期使用了数字 10,但是现在删除了,后期别人不知道又使用了数字10,那么这个时候是有问题的,应该预留出来,表示不可用
    reserved 30, 20, 21;
    reserved "could not use field", "myFirends";
}

// 定义一个查询的消息体
message SearchRequest {
    string username = 1;
}

Logo

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

更多推荐