ARTICLE DETAIL

建站实战干货

来自一线的建站与推广经验沉淀,每一条都经过真实交付验证。

FlatBuffers FlexBuffers 完全指南:无模式、零拷贝的二进制序列化格式

2026/9/10 15:17:30 拓冰建站 浏览量
FlatBuffers FlexBuffers 完全指南:无模式、零拷贝的二进制序列化格式 FlatBuffers FlexBuffers 完全指南无模式、零拷贝的二进制序列化格式【免费下载链接】flatbuffersFlatBuffers: Memory Efficient Serialization Library项目地址: https://gitcode.com/GitHub_Trending/fl/flatbuffersFlexBuffers 是 FlatBuffers 项目内置的一种无模式schema-less二进制序列化格式专为「无法预知数据结构」的场景设计。本文以其官方文档 docs/source/flexbuffers.md 为骨架结合 include/flatbuffers/flexbuffers.h 的源码实现、docs/source/internals.md 的编码规范与 tests/flexbuffers_test.cpp 的测试用例系统讲解 FlexBuffers 的定位、C/Java 使用方式、底层二进制布局与效率优化技巧帮助你决定何时使用它以及如何在 FlatBuffers 项目中内嵌自由格式数据。FlexBuffers 的定位为什么需要一种无模式格式FlatBuffers 的核心理念是「强类型 模式schema驱动」用.fbs文件定义表、字段类型与默认值换来极致的性能与数据一致性。但现实中有大量数据在写代码时无法预知结构例如日志字段、用户自定义属性、AI 模型输出、动态配置等。FlexBuffers 正是为这种场景设计的独立二进制格式它有两条使用路径独立使用作为一个完整的自描述序列化格式单独编码、传输与解码嵌套使用作为 FlatBuffers 表中的一个字段[ubyte] (flexbuffer)内嵌让结构化数据中携带一段自由格式数据。与普通 FlatBuffers 相比FlexBuffers 放弃了强类型却保留了 FlatBuffers 最独特的优势——访问数据时无需解析、无需拷贝、无需对象分配。这意味着你可以直接对内存中的字节做随机访问甚至可以将大量自由格式数据直接 mmap 到内存中使用这是大多数动态格式如 JSON无法做到的。同时由于采用自动位宽收缩与字符串/键池化FlexBuffers 在很多场景下生成的二进制体积甚至小于普通 FlatBuffers。需要明确的是官方文档明确指出FlexBuffers 的读写速度仍然慢于普通 FlatBuffers因此建议仅在确实需要无模式能力时才使用它。快速上手C三行代码序列化一个整数C 使用只需包含头文件flexbuffers.h它内部依赖flatbuffers.h与util.h#include flatbuffers/flexbuffers.h flexbuffers::Builder fbb; fbb.Int(13); fbb.Finish();完成编码后通过fbb.GetBuffer()即可拿到承载编码结果的std::vectoruint8_t随后可以写入文件、发送到网络或存储到父级 FlatBuffer 中。就这个例子而言整个缓冲区只有3 个字节。读取同样简单auto root flexbuffers::GetRoot(my_buffer); int64_t i root.AsInt64(); // 13这里体现了 FlexBuffers 的一个关键设计整数只按实际需要的大小存储不对 int8/int16/int32/int64 做区分。因此无论底层存了多少位你都可以用AsInt64()读取如果缓冲区里实际存的是浮点数或数字字符串AsInt64()甚至会在读取时自动转换无法转换时返回 0。若想先探明内部真实类型可以调用root.GetType()、root.IsInt()等方法对应源码 include/flatbuffers/flexbuffers.h 中Reference类提供的能力。与 FlatBuffers 最大的不同在于根节点约束FlatBuffers 要求根必须是表table而FlexBuffers 允许任意值作为根哪怕只是一个孤独的整数。构建复杂值Map、Vector 与类型混用通过Builder的 Lambda 接口可以构建任意嵌套结构。例如构造等价于 JSON{ vec: [ -100, Fred, 4.0 ], foo: 100 }的值fbb.Map([]() { fbb.Vector(vec, []() { fbb.Int(-100); fbb.String(Fred); fbb.IndirectFloat(4.0f); }); fbb.UInt(foo, 100); }); fbb.Finish();其中 Map 构造器使用 C11 Lambda 聚合子节点如果你更喜欢传统风格也可以改用StartMap()/EndMap()、StartVector()/EndVector()的 start/end 调用见源码 include/flatbuffers/flexbuffers.h。几个值得注意的特性键必须显式存储与 FlatBuffers 不同Map 的键vec、foo需要真实写入缓冲区不过通过键池化key pooling多个相同结构的对象只需存一份键。键无任何限制FlexBuffers 对字段名没有预定义约束你可以动态使用任意键。允许混用类型上面的 Vector 同时包含整数、字符串和浮点数FlatBuffers 的向量做不到这一点。TypedVector 变体如果向量内元素类型单一可以使用TypedVector它省略类型字节占用更少内存。IndirectFloat的意义它把值改为「按偏移存储」而非内联。表面无差别实际有两个收益一是重复出现的大值尤其是 double 或 64 位整数可以共享同一份存储配合ReuseValue见 include/flatbuffers/flexbuffers.h二是向量内元素位宽由最大元素决定——单个 double 会把整个向量撑到 64 位此时把 double 改为间接存储可以让一批小整数继续享受 8 位编码显著省空间。读取复杂结构auto map flexbuffers::GetRoot(my_buffer).AsMap(); map.size(); // 2 auto vec map[vec].AsVector(); vec.size(); // 3 vec[0].AsInt64(); // -100 vec[1].AsString().c_str(); // Fred vec[1].AsInt64(); // 0 (Number parsing failed). vec[2].AsDouble(); // 4.0 vec[2].AsString().IsTheEmptyString(); // true (Wrong Type). vec[2].AsString().c_str(); // (This still works though). vec[2].ToString().c_str(); // 4 (Or have it converted). map[foo].AsUInt8(); // 100 map[unknown].IsNull(); // true这段代码揭示了 FlexBuffers 读取端的宽容语义对不存在的键map[unknown]会返回一个 null 引用IsNull()为 true而不是抛异常类型不匹配时各As*方法会尽力转换Fred解析为数字失败返回 04.0转字符串返回空串ToString()则会把任意类型统一转成std::string4——源码中Reference::ToString()对 Map/Vector 还会递归展开include/flatbuffers/flexbuffers.h。Java 用法与 C 实现一一对应Java 实现与 C 高度对齐。构建同样的 JSON{ vec: [ -100, Fred, 4.0 ], foo: 100 }FlexBuffersBuilder builder new FlexBuffersBuilder(ByteBuffer.allocate(512), FlexBuffersBuilder.BUILDER_FLAG_SHARE_KEYS_AND_STRINGS); int smap builder.startMap(); int svec builder.startVector(); builder.putInt(-100); builder.putString(Fred); builder.putFloat(4.0); builder.endVector(vec, svec, false, false); builder.putInt(foo, 100); builder.endMap(null, smap); ByteBuffer bb builder.finish();读取FlexBuffers.Map map FlexBuffers.getRoot(bb).asMap(); map.size(); // 2 FlexBuffers.Vector vec map.get(vec).asVector(); vec.size(); // 3 vec.get(0).asLong(); // -100; vec.get(1).asString(); // Fred; vec.get(1).asLong(); // 0 (Number parsing failed). vec.get(2).asFloat(); // 4.0 vec.get(2).asString().isEmpty(); // true (Wrong Type). vec.get(2).asString(); // (This still works though). vec.get(2).toString(); // 4.0 (Or have it converted). map.get(foo).asUInt(); // 100 map.get(unknown).isNull(); // true可以看到 Java 端的asLong/asFloat/asString/toString与 C 端的AsInt64/AsDouble/AsString/ToString一一对应同样的宽松转换与 null 语义。Java 实现位于 java/src/main/javaFlexBuffersBuilder与FlexBuffers类。其他语言的实现FlexBuffers 不止 C 与 Java仓库还提供了 python/flatbuffers/flexbuffers.py含BitWidth、Type枚举及Object/Sized/Blob/String/Key/Vector/TypedVector/Map等读取端类型体系并有配套测试 tests/py_flexbuffers_test.py、TypeScript 实现 ts/flexbuffers.ts 与 ts/flexbuffers 目录、Swift 实现 swift/Sources/FlexBuffers、Rust 实现 rust/flexbuffersCargo 包flexbuffers等API 风格均对齐 C 参考实现。二进制编码原理FlexBuffers 为何如此紧凑FlexBuffers 的详细编码规范记录在 docs/source/internals.md 的 FlexBuffers 章节。它继承了 FlatBuffers 的通用约定所有数据通过偏移访问、所有标量按自身大小对齐、一律使用小端序。但有三点本质差异构建方向相反FlatBuffers 从缓冲区末尾向前构建FlexBuffers 则从前向后构建——子节点先于父节点写入根数据落在缓冲区最后一个字节附近。标量位宽可变整数/浮点按 8/16/32/64 位动态编码当前位宽由父容器决定向量会统一为其所有元素选定一个最小可用位宽编码器自动完成用户通常无需干预。偏移只有一种FlexBuffers 只有无符号偏移表示「从自身存储地址向负方向偏移的字节数」——子数据总是存储在父数据之前。向量的字节布局向量是理解 FlexBuffers 的核心Map 本质上是两个向量的组合。例如整数值1, 2, 3编码为uint8_t 3, 1, 2, 3, 4, 4, 4第一个3是大小字段位于向量之前——父节点的偏移指向第一个元素而非大小字段因此大小字段实际处于索引-1的位置元素1, 2, 3之后是类型字节这是无类型向量SL_VECTOR每个元素跟一个类型字节类型字节永远是uint8_t即使元素本身是更宽的标量。类型字节的构成每个类型字节由两部分组成完整取值见 include/flatbuffers/flexbuffers.h 的BitWidth与Type枚举低 2 位子元素的位宽8/16/32/64。仅在子元素通过偏移访问时如子向量使用内联类型时忽略高 6 位实际类型。例如FBT_INT 1、FBT_UINT 2、FBT_FLOAT 3、FBT_STRING 5、FBT_MAP 9、FBT_VECTOR 10、FBT_BLOB 25、FBT_BOOL 26等。上述例子中的类型字节4即表示8 位宽值为 0内联类型不使用 类型SL_INT值为 1。类型化向量与固定长度向量TypedVectorTYPE_VECTOR_INT/TYPE_VECTOR_UINT/TYPE_VECTOR_FLOAT/TYPE_VECTOR_KEY与普通向量相同但省略类型字节类型由父节点提供的向量类型决定仅对少数类型开放以换取可观的空间节省FixedTypedVectorTYPE_VECTOR_INT2至TYPE_VECTOR_FLOAT4长度为 2/3/4 的固定长度向量连大小字段都不存适合常见的点坐标或颜色数据RGBA 四元素空间进一步压缩。标量、布尔与 null整数TYPE_INT/TYPE_UINT与浮点TYPE_FLOAT可按前述位宽内联存储也可通过TYPE_INDIRECT_*按偏移存储——后者适合把昂贵的 64 位甚至 32 位量放进小位宽向量/Map 中并支持多处处共享同一值布尔TYPE_BOOL与 nullTYPE_NULL编码为内联的无符号整数bool 用 0/1 表示。Blob、字符串与键BlobTYPE_BLOB编码类似向量但元素固定为uint8_t。父位宽只决定大小字段的宽度因此blob 可以很大而不会把元素撑宽字符串TYPE_STRING类似 blob额外多一个0终止字节且必须为 UTF-8 编码便于不支持 UTF-8 指针的语言转换成本地字符串键TYPE_KEY类似字符串但不存大小字段因为 Map 查找不需要大小可以更紧凑代价是数据内不能包含值为 0 的字节长度只能靠strlen判定。虽然键也可以在 Map 之外使用但官方建议普通场景优先用字符串。Map 的布局与二分查找Map 与无类型向量类似但大小字段前有两个前缀index字段-3指向键向量keys vector的偏移可在多个表之间共享-2键向量的字节宽度-1大小从这里开始与TYPE_VECTOR兼容0元素Size类型字节键向量是键的 TypedVector。键与对应值都必须按strcmp排序存储这样查找才能使用二分搜索。键向量与值向量分离的原因在于键向量可以被多个值向量共享也能在代码中被单独当作向量处理。文档给出了{ foo: 13, bar: 14 }的完整字节级示例0 : uint8_t b, a, r, 0 4 : uint8_t f, o, o, 0 8 : uint8_t 2 // key vector of size 2 // key vector offset points here 9 : uint8_t 9, 6 // offsets to bar_key and foo_key 11: uint8_t 2, 1 // offset to key vector, and its byte width 13: uint8_t 2 // value vector of size // value vector offset points here 14: uint8_t 14, 13 // values 16: uint8_t 4, 4 // types注意编码器在 include/flatbuffers/flexbuffers.h 的EndMap中会对键值对自动排序并检测重复键HasDuplicateKeys()从而保证二分查找的正确性。根节点的编码由于根没有父节点其位宽只能自描述。缓冲区最后一个字节是根的字节宽度倒数第二个字节是根的类型之前的数据是根的值。例如根为整数13uint8_t 13, 4, 1 // Value, type, root byte width.将 FlexBuffers 嵌套进 FlatBuffer在.fbs模式中可以用flexbuffer属性把某个字段声明为 FlexBuffers 数据a:[ubyte] (flexbuffer);解析器对此有严格校验源码 src/idl_parser.cpp 明确要求flexbuffer属性只能应用于vector of ubyte否则报错。启用后代码生成器会为该字段生成专用访问器直接返回 FlexBuffers 根引用例如a_flexbuffer_root().AsInt64()免去手工调用GetRoot的步骤在 JSON 解析路径中flexbuffers::Builder会参与把 JSON 值编码为内嵌 FlexBuffer见 src/idl_parser.cpp构建时使用BUILDER_FLAG_SHARE_ALL并强制对齐。这样你就能在「强类型的表」中安全地嵌入一段「自由的动态数据」同时享受零拷贝访问。效率优化建议官方文档总结了若干经过实践验证的优化原则配合源码可以进一步理解其原理优先使用 Vector 而非 Map向量远比 Map 高效。对于小型对象与其用x/y/z三个键的 Map不如直接用向量更好的是 TypedVector最好的是固定长度 TypedVector如坐标、颜色。善用 Map 的向量兼容性Map 向后兼容向量可以直接按向量迭代——只迭代值用map.Values()需要并行访问键则用map.Keys()见 include/flatbuffers/flexbuffers.h。如果打算访问大部分元素按迭代器顺序遍历比逐个按键查找更快因为按键查找涉及对键向量的二分搜索源码 python/flatbuffers/flexbuffers.py 的_LowerBound/_BinarySearch同样体现了这一设计。避免位宽污染不要把需要大位宽的值如 double混入大量小值向量——向量元素会统一升级位宽。此时应改用IndirectDouble。整数会自动选择最小位宽存一个值很小的int64_t实际只占几个比特double 在可无损表示时会被自动降为 float但这种情况很少。嵌套的向量/Map 因为按偏移存储通常不影响外层向量位宽。大数组用 Blob存储大量字节数据应使用 blob。若用 TypedVector大小字段的位宽可能让体积超过预期且不兼容memcpy超过 64k 元素的大规模(u)int16_t数组也建议存成二进制 blob。blob 的构建与使用方式与字符串类似。测试与验证仓库通过 tests/flexbuffers_test.cpp 对上述行为做了完整验证FlexBuffersTest()覆盖键/字符串复用、blob 访问等场景FlexBuffersDeprecatedTest()则专门验证已废弃的FBT_VECTOR_STRING_DEPRECATED类型该类型在 include/flatbuffers/flexbuffers.h 中被标记为 DEPRECATED建议改用FBT_VECTOR或FBT_VECTOR_KEY。Python 侧有 tests/py_flexbuffers_test.pyRust 侧则通过flexbufferscrate 提供等价的测试矩阵。小结FlexBuffers 是 FlatBuffers 家族中「以空间换约束」的互补成员它牺牲强类型换来零解析、零拷贝、零分配的随机访问以及通过自动位宽收缩、键/字符串池化实现的极致紧凑体积又通过 Map 的排序键向量 二分查找、TypedVector/FixedTypedVector/Blob 等特化类型把无模式数据的读写效率推向极致。当你面对动态结构数据、需要 mmap 大规模自由格式数据、或想在强类型 FlatBuffer 中内嵌一段动态负载时FlexBuffers 是官方提供的首选方案——但请记住它的定位仅在确实需要无模式能力时使用常规场景下普通 FlatBuffers 依然更快。【免费下载链接】flatbuffersFlatBuffers: Memory Efficient Serialization Library项目地址: https://gitcode.com/GitHub_Trending/fl/flatbuffers创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考