Jetpack DataStore 是一种经过改进的新数据存储解决方案,旨在取代 SharedPreferences
。DataStore 基于 Kotlin 协程和 Flow 构建而成,提供以下两种不同的实现:
SharedPreferences
的一些缺点。此实现不需要预定义的架构,也不确保类型安全。功能 | 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,请始终谨记以下规则:
请勿在同一进程中为给定文件创建多个 DataStore 实例,否则会破坏所有 DataStore
功能。如果给定文件在同一进程中有多个有效的 DataStore
,DataStore
在读取或更新数据时将抛出
IllegalStateException
。
DataStore 的通用类型必须不可变。更改 DataStore
中使用的类型会导致 DataStore
提供的所有保证失效,并且可能会造成严重的、难以发现的 bug。强烈建议您使用可保证不可变性、具有简单的 API
且能够高效进行序列化的协议缓冲区。
切勿在同一个文件中混用 SingleProcessDataStore 和 MultiProcessDataStore。如果您打算从多个进程访问 DataStore,请始终使用 MultiProcessDataStore
。
Preference DataStore
API 类似于 SharedPreferences
,但与后者相比存在一些显著差异:
Flow
添加依赖:
dependencies {
implementation("androidx.datastore:datastore-preferences:1.0.0")
}
Preferences DataStore 实现使用 DataStore 和 Preferences 类将简单的键值对保留在磁盘上。
使用由 preferencesDataStore
提供的属性委托来创建 Datastore
实例。只需在 Kotlin 文件顶层调用该实例一次,便可在应用的所有其余部分通过此属性访问该实例。这样可以更轻松地将 DataStore
保留为单例。
private const val USER_PREFERENCES_NAME = "user_preferences"
private val Context.dataStore by preferencesDataStore(
name = USER_PREFERENCES_NAME
)
由于 Preferences DataStore 不使用预定义的架构,因此必须使用相应的键类型函数为需要存储在 DataStore
实例中的每个值定义一个键。例如,如需为 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 提供了一个 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()
将抛出异常。
下面是一个在 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
迁移到 DataStore
,我们需要更新 DataStore
构建器以向迁移列表传入 SharedPreferencesMigration
。DataStore
能够自动从 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 实现使用 DataStore 和协议缓冲区将类型化的对象保留在磁盘上。
SharedPreferences
和 Preferences 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 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 来存储类型化对象涉及两个步骤:
Serializer
的类,其中 T
是 proto
文件中定义的类型。此序列化器类会告知 DataStore
如何读取和写入您的数据类型。请务必为该序列化器添加默认值,以便在尚未创建任何文件时使用。dataStore
创建的属性委托来创建 DataStore
的实例,其中 T
是 proto
文件中定义的类型。在您的 Kotlin
文件顶层调用该实例一次,便可在应用的所有其余部分通过此属性委托访问该实例。filename
参数会告知 DataStore
使用哪个文件存储数据,而 serializer
参数会告知 DataStore
第 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
,以确保协议缓冲区生成对象。
为了创建 DataStore 实例,我们使用 dataStore 委托,并将 Context 作为接收器。此委托有两个必需参数:
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
)
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 提供了一个挂起函数 updateData()
,用于以事务方式更新存储的对象。updateData()
在读取-写入-修改原子操作中用事务的方式更新数据。一旦数据持久存储在磁盘中,协程便会完成。
suspend fun updateShowCompleted(completed: Boolean, name: String) {
context.userDataStore.updateData { preferences ->
preferences.toBuilder()
.setShowCompleted(completed)
.setExampleCounter(preferences.exampleCounter + 1)
.setName(name)
.build()
}
}
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 操作。
下面提供一种通过 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 多进程功能目前仅在
1.1.0 Alpha
版中提供
您可以将 DataStore 配置为访问不同进程中的相同数据,并保证数据一致性与单个进程中相同。具体而言,DataStore 可保证:
假设有一个包含一项服务和一个 activity 的示例应用:
<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)
}
}
}
val settings: Settings by dataStore.data.collectAsState()
Text(
text = "Last updated: $${settings.timestamp}",
)
MultiProcessDataStoreFactory
构造 DataStore
对象。val dataStore: DataStore<Settings> = MultiProcessDataStoreFactory.create(
serializer = SettingsSerializer(),
produceFile = {
File("${context.cacheDir.path}/myapp.preferences_pb")
}
)
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(...)
MMKV: 基于内存映射mmap
的键值对存储
MMKV出现的原因是解决微信聊天用户文字发生崩溃时,用来追溯崩溃发生的聊天文本,在显示每条文本到聊天页面之前先同步写入磁盘。所以MMKV适合的场景是高频、同步、磁盘写入。
SharedPreference读写可能存在卡顿(同步方式)。
DataStore读写都是在后台进行的,不存在卡顿,它使用协程来实现。MMKV有可能会造成数据丢失(内存中的数据没来得及写回磁盘)概率较低。
如果考虑支持多进程或高频写入需求,MMKV可能是唯一选择,否则DataStore是最佳选择,因为性能各方面比SharedPreference更完美。不过,DataStore最新的版本也开始支持多进程了。
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 之间的数字无法使用。它们专用于协议缓冲区实现。
字段规则:
message: 消息类型, 同一.proto
文件中可以指定多个 message。
注释:.proto文件中支持//
和 /* ... */
语法的注释
保留字段:使用 reserved
可以指定已删除的字段名称或编号为已保留,确保将来用户修改了该字段会得到编译报错,避免导致严重问题。不能在同一 reserved 语句中混用字段名称和字段编号。
message Foo {
reserved 2, 15, 9 to 11;
reserved "foo", "bar";
}
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;
}
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。
message SampleMessage {
oneof test_oneof {
string name = 4;
SubMessage sub_message = 9;
}
}
Map: map
其中 key_type
可以是任何整数或字符串类型(因此,除浮点类型和 bytes 以外的任何标量类型)。请注意,枚举不是有效的 key_type
。value_type
可以是除其他映射外的任何类型。map
向后兼容性: 此映射语法等效于线上传输的内容,因此不支持映射的协议缓冲区实现仍然可以处理您的数据:
message MapFieldEntry {
key_type key = 1;
value_type value = 2;
}
repeated MapFieldEntry map_field = N;
任何支持映射的协议缓冲区实现都必须生成并接受上述定义可以接受的数据。
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;
}