ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

FlatBuffers Swift 快速上手:内存高效序列化库的 Schema 设计、缓冲构建与零解析读取实战

FlatBuffers Swift 快速上手:内存高效序列化库的 Schema 设计、缓冲构建与零解析读取实战 FlatBuffers Swift 快速上手内存高效序列化库的 Schema 设计、缓冲构建与零解析读取实战【免费下载链接】flatbuffersFlatBuffers: Memory Efficient Serialization Library项目地址: https://gitcode.com/GitHub_Trending/fl/flatbuffersFlatBuffers 是一个内存高效的跨平台序列化库其核心设计是让数据以扁平的二进制缓冲形式存储从而在不经过解析/反序列化的情况下直接访问字段同时天然支持数据结构的向前/向后兼容。本文基于仓库中 Swift 官方的 Documentation.docc/Documentation.md 文档及其配套 Tutorials完整还原从 Schema 编写、flatc 代码生成、FlatBufferBuilder构建缓冲到ByteBuffer直接读取的端到端流程。读完本文你将能用 Swift 独立完成一个包含表table、结构体struct、枚举enum、联合union、向量vector等完整数据模型的序列化与读取并理解其底层“无解析访问”的工作原理。一、FlatBuffers 的核心设计理念官方文档swift/Sources/FlatBuffers/Documentation.docc/Documentation.md将 FlatBuffers 的设计目标概括为以下几个相互支撑的特性它们是理解后文所有 API 用法的基础1. 无需解析即可访问数据与其他序列化方案不同FlatBuffers 把层级化的数据表示成一块扁平的二进制缓冲flat binary buffer使得程序可以不经过解析/拆包就直接访问其中任意字段。这是它区别于 JSON、Protocol Buffers 等方案最本质的一点——数据本身就是可寻址的内存布局而不是需要先还原成对象树才能读取的字节流。2. 内存效率与访问速度零额外分配访问数据所需的全部内存就是缓冲本身。官方文档明确指出在 C 中它需要0 次额外内存分配其他语言实现可能略有差异。适合 mmap 与流式读取由于数据是扁平布局可以只把缓冲的一部分驻留在内存中即可访问非常适合内存映射mmap或流式传输场景。接近原生结构体访问速度字段访问只引入一次额外的间接跳转借助一种类似 vtable 的机制即可支持格式演化和可选字段。这正是它面向游戏或其他对性能敏感项目的根本原因——这些场景下为访问或构造序列化数据而付出大量时间和内存分配是不可接受的。3. 灵活性与强类型可选字段optional fields带来两方面好处一是出色的向前/向后兼容性——对生命周期很长的游戏而言不必在每次发布新版本时重写全部历史数据二是开发者可以自由决定哪些数据写入、哪些不写以及如何设计数据结构。强类型strongly typed意味着错误在编译期暴露而不是在运行时靠手写重复且易错的检查来兜底同时库可以为开发者生成大量可用的代码。4. 极小的代码占用FlatBuffers 只需要少量生成的代码加上一个很小的头文件作为最小依赖即可集成这与后文通过flatc生成 Swift 代码的流程直接对应。二、准备环境安装 flatc 编译器根据官方 Tutorialsswift/Sources/FlatBuffers/Documentation.docc/Tutorials/creating_flatbuffer_schema.tutorial编写 Schema 并生成 Swift 代码的前提是在设备上安装 FlatBuffers 编译器flatc。仓库根目录的 README.md 与 docs/building.md 中说明了通过 CMake 构建编译器的方式克隆仓库后使用cmake生成构建系统并编译即可得到flatc可执行文件。安装完成后即可用它对.fbsSchema 文件执行代码生成。三、从零编写 Schemamonster.fbs官方教程以经典的 Monster怪物示例为主线通过 7 个递进步骤演示 Schema 的完整演化。下面的每一步都对应仓库swift/Sources/FlatBuffers/Documentation.docc/Resources/code/fbs/目录下的monster_step_N.fbs文件。第 1 步创建 Schema 文件新建一个空文件教程中命名为monster.fbs我们将在其中定义一个Monster表table包含位置position、颜色color以及怪物的基本信息。初始文件为空随后逐步填充。第 2 步添加枚举 Color用enum表示颜色底类型为byte1 字节有符号整数包含三个成员enum Color:byte { red, green, blue }第 3 步添加结构体 Vec3用struct表示坐标数据包含怪物在场景中的x、y位置。注意 FlatBuffers 中struct是定长、内联的数据布局字段在缓冲中按声明顺序连续存放读写开销最低enum Color:byte { red, green, blue } struct Vec3 { x:float; y:float; }第 4 步创建 Monster 表table是 FlatBuffers 中最灵活的数据类型——支持可选字段与默认值。这里让 Monster 持有当前位置和颜色且color带有默认值Blue对应枚举成员blueenum Color:byte { red, green, blue } struct Vec3 { x:float; y:float; } table Monster { pos:Vec3; color:Color Blue; }第 5 步补充 Monster 的字段为 Monster 添加名称、魔法值mana、生命值hp、装备equipped、武器列表weapons与移动路径path。其中weapons与path是**向量vector**类型——分别存储Weapon表的偏移量和Vec3结构体enum Color:byte { red, green, blue } struct Vec3 { x:float; y:float; } table Monster { pos:Vec3; color:Color Blue; mana:short 150; hp:short 100; name:string; equipped:Equipment; weapons:[Weapon]; path:[Vec3]; }这里可以观察到 FlatBuffers Schema 的关键约定mana:short 150与hp:short 100表示short2 字节类型并带默认值默认值不会被写入缓冲从而节省空间name:string为字符串字段实际存储的是指向缓冲内字符串数据的偏移量equipped:Equipment引用下文定义的联合类型weapons:[Weapon]是表的向量path:[Vec3]是结构体的向量。第 6 步定义联合 Equipment 与表 Weapon由于Equipment可以是武器官方示例用union表达这种多选一关系同时补上Weapon表含名称与伤害值。联合在底层由一个类型标签字段 对象偏移字段组成访问时需要先判断类型再取对象enum Color:byte { red, green, blue } union Equipment { Weapon } // Optionally add more tables. struct Vec3 { x:float; y:float; } table Monster { pos:Vec3; color:Color Blue; mana:short 150; hp:short 100; name:string; equipped:Equipment; weapons:[Weapon]; path:[Vec3]; } table Weapon { name:string; damage:short; }第 7 步声明根类型并生成代码最后用root_type Monster;声明缓冲的根对象类型并运行flatc --swift monster.fbs生成 Swift 代码。官方教程提醒生成后要把文件导入到你的 Xcode 工程中才能使用。... root_type Monster; // flatc --swift monster.fbs至此我们得到了包含Color枚举、Vec3结构体、Monster/Weapon两张表以及Equipment联合的完整数据模型。四、用 FlatBufferBuilder 构建第一个缓冲代码生成之后进入构建阶段。官方教程create_your_first_buffer.tutorial对应仓库swift/Sources/FlatBuffers/Documentation.docc/Resources/code/swift/swift_code_1.swift到swift_code_10.swift的递进代码。核心 API 类型FlatBufferBuilder与ByteBuffer分别定义在 swift/Sources/FlatBuffers/FlatBufferBuilder.swift 与 swift/Sources/FlatBuffers/ByteBuffer.swift 中。4.1 导入库并创建 Builder首先导入FlatBuffers模块配合Foundation使用字符串等基础类型。随后创建FlatBufferBuilder实例——builder 会持有正在增长的缓冲。构造时可以传入初始容量这里为 1024 字节缓冲会在需要时自动增长import FlatBuffers import Foundation func run() { // create a FlatBufferBuilder, which will be used to serialize objects let builder FlatBufferBuilder(initialSize: 1024) }4.2 序列化字符串字段在开始构造 Monster 之前先准备好它引用的子对象。武器需要名字因此先把Sword和Axe两个字符串写入缓冲得到它们的偏移量Offsetlet weapon1Name builder.create(string: Sword) let weapon2Name builder.create(string: Axe)4.3 构造表start / add / end 三步曲每张生成的表都提供start、add与end三个配套方法。创建两件武器时先startWeapon开启一个 Weapon 表用add写入各字段再endWeapon结束并把起始点传入以获得该表的偏移量// start creating the weapon by calling startWeapon let weapon1Start Weapon.startWeapon(builder) Weapon.add(name: weapon1Name, builder) Weapon.add(damage: 3, builder) // end the object by passing the start point for the weapon 1 let sword Weapon.endWeapon(builder, start: weapon1Start) let weapon2Start Weapon.startWeapon(builder) Weapon.add(name: weapon2Name, builder) Weapon.add(damage: 5, builder) let axe Weapon.endWeapon(builder, start: weapon2Start)4.4 向量vector的两种创建方式把 Sword 与 Axe 的偏移量打包成一个表偏移量向量供 Monster 后续引用// Create a FlatBuffer vector that contains offsets to the sword and axe // we created above. let weaponsOffset builder.createVector(ofOffsets: [sword, axe])官方教程明确指出FlatBuffers 中通常有两种创建向量的方式并提供了覆盖各种场景的便捷方法避免你总是手动start/end便捷方法如createVector(ofOffsets:)表偏移量向量、createVector(ofStructs:)结构体向量直接把 Swift 数组交给 builder手动方式用startVector(len:elementSize:)开始、逐元素push(element:)写入、最后endVector(len:)结束教程代码中以注释形式给出// startVector(len, elementSize: MemoryLayoutOffset.size) // for o in offsets.reversed() { // push(element: o) // } // endVector(len: len)接着为 Monster 添加名字Orc并用createVector(ofStructs:)创建一个由原生 Swift 结构体构成的路径向量。教程特别提醒传给createVector(ofStructs:)的 Swift 结构体必须经过填充padded以符合 FlatBuffers 的对齐标准——生成代码中的Vec3已满足这一要求// Name of the Monster. let name builder.create(string: Orc) let pathOffset fbb.createVector(ofStructs: [ Vec3(x: 0, y: 0), Vec3(x: 5, y: 5), ])4.5 构造 Monster 并理解“不可嵌套”约束官方教程在此处给出了一个至关重要的规则Unlike structs, you should not nest tables or other objects, which is why we created all the strings/vectors/tables that this monster refers to before start. If you try to create any of them between start and end, you will get anassert.即不同于结构体你不能在两张表之间“嵌套”对象——必须在start之前把所有 Monster 引用的字符串、向量、表都创建好若在start与end之间试图创建任何此类对象会触发assert断言。这正是 FlatBuffers 保持扁平内存布局的底层原因表内字段只能引用已存在于缓冲中的偏移量。序列化 Monster 也有两种方式便捷方法createMonster一次性传入所有字段包括联合的类型标签与偏移量。这里给怪物装备了AxeequippedType: .weapon、equippedOffset: axelet orc Monster.createMonster( builder, pos: Vec3(x: 1, y: 2), hp: 300, nameOffset: name, color: .red, weaponsVectorOffset: weaponsOffset, equippedType: .weapon, equippedOffset: axe, pathOffset: pathOffset)手动方式与 Weapon 一致用startMonster 各add方法 endMonster教程代码中以注释形式给出// let start Monster.startMonster(builder) // Monster.add(pos: Vec3(x: 1, y: 2), builder) // Monster.add(hp: 300, builder) // Monster.add(name: name, builder) // Monster.add(color: .red, builder) // Monster.addVectorOf(weapons: weaponsOffset, builder) // Monster.add(equippedType: .weapon, builder) // Monster.addVectorOf(paths: weaponsOffset, builder) // Monster.add(equipped: axe, builder) // var orc Monster.endMonster(builder, start: start)4.6 结束构建并取出字节最后调用builder.finish(offset: orc)告知 builder 缓冲构建完成然后通过sizedByteArray拿到[UInt8]类型的最终字节数组也可以借助ByteBuffer包装sizedBuffer得到Data对象用于落盘或网络传输// Call finish(offset:) to instruct the builder that this monster is complete. builder.finish(offset: orc) // This must be called after finish(). // sizedByteArray returns the finished buf of type [UInt8]. let buf builder.sizedByteArray // or you can use to get an object of type Data let bufData ByteBuffer(data: builder.sizedBuffer)至此一个包含位置、颜色、属性、名字、武器列表、装备联合与路径向量的 Monster 缓冲就构建完成可以被保存、发送或直接读取。五、从 ByteBuffer 零解析读取数据构建完成之后读取端同样直接。官方教程reading_bytebuffer.tutorial对应swift_code_11.swift至swift_code_13.swift核心读取 APIgetRoot/getCheckedRoot定义在 swift/Sources/FlatBuffers/Root.swift 中。5.1 获取根对象访问器从磁盘或网络取得数据后把[UInt8]或Data包装成ByteBuffer再获取根对象的访问器。官方教程给出两种入口getCheckedRoot(byteBuffer:)先校验数据有效性防止从损坏的缓冲中读取返回的是可抛错try!的调用getRoot(byteBuffer:)当你确信数据 100% 正确时使用跳过校验以获得更高性能。// create a ByteBuffer(:) from an [UInt8] or Data() let buf [] // Get your data var byteBuffer ByteBuffer(bytes: buf) // Get an accessor to the root object inside the buffer. let monster: Monster try! getCheckedRoot(byteBuffer: byteBuffer) // let monster: Monster getRoot(byteBuffer: byteBuffer)5.2 直接访问字段拿到Monster访问器后所有字段都可以像访问普通 Swift 属性一样直接读取——这正是“无需解析”的体现hp、mana直接返回数值name返回可选字符串pos返回内联的Vec3结构体其x、y亦直接可读。官方教程同时指出已废弃deprecated的字段不会出现在生成的访问器中let hp monster.hp let mana monster.mana let name monster.name // returns an optional string let pos monster.pos let x pos.x let y pos.y5.3 访问联合类型联合union的访问需要先检查类型标签再按具体类型取出对象。示例中先判断equippedType .weapon再用monster.equipped(type: Weapon.self)取回Weapon对象并读取其属性此例中名字应为Axe、伤害应为5// Get and check if the monster has an equipped item if monster.equippedType .weapon { let _weapon monster.equipped(type: Weapon.self) let name _weapon.name // should return Axe let dmg _weapon.damage // should return 5 }六、深入原理vtable、偏移量与扁平内存布局结合官方文档的概述与上文构建流程可以归纳出 FlatBuffers “零解析访问”背后的三个核心机制以下属于由文档描述与代码结构推断得出的实现原理扁平化布局所有字符串、向量、表都以**相对偏移量offset**互相引用数据在缓冲中按构建顺序连续排布读取时通过“缓冲基址 偏移量”直接定位字段无需递归解析。vtable 机制每张表都关联一个类似 vtable 的结构记录各字段在表内的位置。文档中提到的“一次额外间接跳转”正是该 vtable——它让字段可以缺失可选字段、可以让新版本追加字段从而实现向前/向后兼容访问速度仍接近原生结构体。默认值不落盘Schema 中声明的默认值如color Blue、mana 150在写入时会被省略读取端按 Schema 补回既节省空间又保持兼容。这一点可在第 5 步 Schema 中直接观察到。这也解释了上一节“表不能在 start/end 之间嵌套创建”的约束表的字段值必须是已确定地址的偏移量而嵌套创建会破坏扁平布局的确定性。七、官方配套学习资源与后续路径仓库在 swift/Sources/FlatBuffers/Documentation.docc/Tutorials/ 下提供了完整的 DocC 教程套件与本文内容一一对应creating_flatbuffer_schema.tutorialSchema 编写与代码生成create_your_first_buffer.tutorial用FlatBufferBuilder构建缓冲reading_bytebuffer.tutorial从ByteBuffer读取数据Tutorial_Table_of_Contents.tutorial教程总览其中介绍了 FlatBuffers 是面向 C、C#、C、Go、Java、Kotlin、JavaScript、Lobster、Lua、TypeScript、PHP、Python、Rust 与 Swift 的跨平台库最初由 Google 为游戏开发及其他性能敏感应用而创建。所有教程配对的 Schema 与 Swift 代码示例分别位于 swift/Sources/FlatBuffers/Documentation.docc/Resources/code/fbs/ 与 swift/Sources/FlatBuffers/Documentation.docc/Resources/code/swift/。完整可编译的 Monster 示例还可见于 samples/monster.fbs 与 samples/sample_binary.swift仓库 swift/Sources/FlatBuffers/ 目录下的FlatBufferBuilder.swift、ByteBuffer.swift、Root.swift是上述全部 API 的源码实现tests/swift/ 中则包含对应的测试用例可供深入研读。八、结语本文完整还原了 FlatBuffers Swift 官方文档Documentation.md及其配套教程的技术脉络先通过monster.fbs的 7 步演化理解enum/struct/table/union/vector/root_type等 Schema 语法再以flatc --swift monster.fbs生成代码随后用FlatBufferBuilder按“先子对象、后父表、最后 finish”的顺序构建缓冲最终通过ByteBuffer与getCheckedRoot/getRoot实现零解析读取。掌握这套流程后你可以直接将其迁移到游戏存档、配置热更新、网络协议等对内存占用与访问延迟敏感的场景——这正是 FlatBuffers 从诞生之初就瞄准的领域。【免费下载链接】flatbuffersFlatBuffers: Memory Efficient Serialization Library项目地址: https://gitcode.com/GitHub_Trending/fl/flatbuffers创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
返回列表