.riv 文件格式
.riv 文件是从 Rive 编辑器导出的二进制运行时格式,被所有 Rive 运行时使用。
运行时格式
Rive 编辑器将你的项目导出为 .riv 文件,供 Rive 运行时使用。这是画板、形状、动画、状态机等的二进制表示。Rive 运行时读取此文件以在应用程序、游戏、网站等中显示内容。该格式设计为在快速加载、小文件体积以及适应未来变化/新增功能之间取得平衡。
二进制类型
Rive 运行时文件的二进制读取器需要能够从流中读取以下数据类型。
注意: 字节序为小端序(little endian)。
| 类型 | 描述 |
|---|---|
| 可变无符号整数 | LEB128 可变编码的无符号整数(以下简称 varuint) |
| 无符号整数 | 4 字节无符号整数 |
| 字符串 | 无符号整数后跟指定长度的 UTF-8 编码字节数组 |
| 浮点数 | 按 4 字节 IEEE 754 编码的 32 位浮点数 |
注意: 参考二进制读取器
文件头
文件头是写入文件的第一部分内容,为运行时提供基本信息以验证它能否读取此文件。其中包含一个 ToC(目录/字段定义),使运行时能够理解如何跳过它可能不理解的内容和对象。这也是该格式能够适应编辑器未来变化/新增功能的原因之一。较旧的运行时至少可以尝试加载旧文件并显示它,只是不包含它不理解的对象和属性。
| 值 | 类型 |
|---|---|
| 指纹 | 4 字节 |
| 主版本号 | varuint |
| 次版本号 | varuint |
| 文件 ID | varuint |
| ToC | 字节对齐的位数组 |
指纹
文件指纹使导入器能够快速检查它确实在查看一个由 Rive 导出的文件。这是 4 个字节,表示 UTF-8/ASCII 字符串 "RIVE"。在十六进制编辑器中显示如下:
注意: 0x52 0x49 0x56 0x45 / "RIVE"
主版本号
运行时仅兼容单一的 Rive 导出格式主版本。当前主版本格式为 7。如果支持格式主版本 7 的运行时遇到格式主版本 6 的文件,它将立即报错且不会尝试读取任何后续内容,因为这两种格式被认为是根本不同的。这是 Rive 在需要时从根本上改变其导出格式的最后手段 ,我们会尽量少用。
从格式 6 到格式 7 的切换发生在多年前,编辑器已不再导出格式 6 的文件。
如果将来引入新的主版本格式,运行时和导出文件必须匹配主版本号。实际影响如下:
- 如果更新到支持新主版本格式的运行时,则必须将文件重新导出为该主版本格式
- 如果继续使用旧运行时,则必须以旧的兼容主版本格式导出文件
- 添加对新文件格式主版本支持的运行时,也会发布一个运行时大版本更新
示例:假设 Android 运行时当前主版本为 11,当引入对新的文件格式主版本的支持时,对该格式的 Android 运行时支持将在主版本 12 中发布。要采用它,你需要更新到 Android 运行时 12 并将文件重新导出为新的文件格式主版本。
如果需要以编程方式验证文件的格式版本,请从文件头读取 Major Version 和 Minor Version 值。当文件格式不兼容时,Rive 运行时也会显示描述性的导入错误。
注意: 主版本之间不兼容。例如,支持格式版本 6 的运行时无法读取格式版本 7 的文件,反之亦然。每当引入新的主文件格式版本时,所有支持的运行时会发布相应的大版本更新。
次版本号
只要主版本号相同,次版本号的变化是相互兼容的。但是,如果运行时的次版本号不同,某些较新的功能可能不可用。例如,主版本 7 引入了状态机。我们正在为状态机添加新的状态类型。版本 7.0 的运行时可能无法加载 7.1 文件中导出的所有状态。但是,运行时仍然可以播放状态机,只是在转换到它不理解的状态时不会执行任何操作。
注意: 当文件使用了较旧的兼容运 行时不支持的新功能时,文件仍然可以加载,且支持的功能会继续正常工作。不支持的功能会被视为空操作。Rive 运行时设计为避免在此兼容场景下发生崩溃或未定义行为。
版本兼容性示例:
| 运行时版本 | 文件版本 | 兼容性 |
|---|---|---|
| 6.1 | 6.0 | 是 |
| 6.1 | 6.2 | 是 |
| 6.1 | 7.0 | 否 |
| 7.0 | 6.1 | 否 |
| 7.0 | 7.1 | 是 |
文件 ID
这是文件的唯一标识符,将来可以通过我们的 API 用于区分文件。API 尚未定义,但计划的功能包括按需重新导出文件的新版本、获取文件详情等。目前,这可用于验证此导出是从哪个文件生成的。
ToC
文件头的目录(Table of Contents)部分是文件中所有属性及其底层类型的列表。这使运行时能够跳过它想跳过或不理解的属性。它通过为每个属性 ID 提供底层类型来实现此目的。
字段类型
有 5 种基本底层类型,但它们以 4 种不同的方式进行序列化。了解类型的序列化方式使运行时能够知道如何读取它。即使读取了错误的值或解释不正确,重要的是能够读取过去,以便安全地读取文件的其余部分。
例如,布 尔值可以读取为无符号整数,因为底层类型和序列化器是兼容的。即使将布尔值作为整数读取不会提供该属性的有效值,运行时仍然可以读取过去。
ToC 数据
已知属性列表序列化为一系列可变无符号整数,以 0 作为终止符。有效的属性键由非零的无符号整数 ID/键来区分。属性之后是一个位数组,由读取的属性数量 / 4 字节组成。每个属性获得 2 位来定义可以使用哪种底层类型反序列化器来读取过去。
注意: 这里的意图是提供已知的属性类型键及其底层类型,这样如果属性类型未知,读取器可以读取整个值而不会出现缓冲区读取不足或溢出的问题。
| 底层类型 | 2 位值 |
|---|---|
| Uint/Bool | 0 |
| String | 1 |
| Float | 2 |
| Color | 3 |
举例来说,假设一个文件有三个已知属性类型(属性 12 是 uint 值、属性 16 是 string 值、属性 6 是 bool 值),导出器将按如下方式序列化数据:
varuint: 12
varuint: 16
varuint: 6
varuint: 0
2 bits: 0
2 bits: 1
2 bits: 0
注意: 参考 ToC 反序列化器
基线属性
Rive 不会导出自上一个主版本以来系统已知的属性。我们在切换到新的主版本时进行基线化,因为不会有需要读取较新属性的次版本。在切换到最新主版本后新引入的属性,将随着新次版本的发布而导出。
内容
文件的其余部分就是一个对象列表,每个对象包含其属性和值的列表。对象以 varuint 类型键表示,紧接着是属性列表。属性以 0 varuint 终止。如果读取到非 0 值,则预期它是属性的类型键。如果运行时知道该类型键,它会知道底层类型以及如何解码。类型键后面的字节将是前面指定的二进制类型之一。如果未知,它可以从 ToC 中确定底层类型并读取过去。
Core 定义
所有对象和属性定义在一组我们称为 Core Definitions 的文件中。它们以一系列 JSON 对象定义,帮助 Rive 生成序列化、反序列化和动画属性代码。C++ 和 Flutter 运行时都有辅助工具来读取和生成这些类型的大量样板代码。
对象
Core 对象由其 Core 类型键表示。例如,Shape 的 core 类型键为 3。同样,你可以看到 C++ 运行时生成的代码也使用相同的键标识 Shape。
属性
属性同样由 Core 类型键表示。这些键在所有对象中唯一,因此属性键 13 始终是 Node 对象的 X 值,并在运行时中匹配。Node 的 X 值已知为浮点值,因此当遇到它时会按浮点数解码。属性键 0 保留为空终止符(表示当前对象的属性已读取完毕)。
序列化对象示例
| 数据 | 类型/大小 | 描述 |
|---|---|---|
| 2 | varuint | 类型为 2 的对象(Node) |
| 13 | varuint | Node 的 X 属性 |
| 100.0 | 4 字节浮点数 | Node 的 X 值 |
| 14 | varuint | Node 的 Y 属性 |
| 22.0 | 4 字节浮点数 | Node 的 Y 值 |
| 0 | varuint | 空终止符。属性读取完毕,Node 读取完成。 |
上下文
对象总是在彼此的上下文中提供。Shape 总是在 Artboard 之后提供。Node 的 artboard 始终可以通过查找最近读取的 Artboard 来确定。这个概念被广泛用于为需要上下文的对象提供上下文。另一个示例:KeyFrame 总是在 LinearAnimation 之后提供,这意味着你始终可以通过跟踪最后一个读取的 LinearAnimation 来确定 KeyFrame 属于哪个 LinearAnimation。
层级结构
Artboard 内的对象可以父子关联到 Artboard 中的其他对象。这种映射更为复杂,需要标识符来查找父对象。标识符以 core def 属性的形式提供。该值始终是一个无符号整数,表示 Artboard 内作为有效父对象的 ContainerComponent 派生对象的索引。