pnemyakin/SmartPackager

GitHub: pnemyakin/SmartPackager

一个面向 .NET 和 Unity 的高性能二进制序列化库,通过缓存编译的 Packer 委托实现纳秒级序列化,支持预分配缓冲区的零分配模式。

Stars: 2 | Forks: 0

# SmartPackager SmartPackager 是一个用于 .NET 的二进制序列化库。它将对象、结构体、类和数组打包为紧凑的字节序列,并在另一端进行重建。Packers 在首次使用时解析一次并缓存,因此重复的 pack/unpack 调用无需支付反射成本。 目标框架为 **netstandard2.1**,兼容 .NET 5 到 9 以及 Unity 2021+。 详细文档:[API 参考](Docs/API.md) | [二进制格式](Docs/BinaryFormat.md) | [自定义 Packers](Docs/CustomPackers.md) | [原始 Word 文档](Docs/SmartPackager.docx) ## 环境要求 - .NET Standard 2.1 或更高版本(.NET 5/6/7/8/9, Unity 2021+) - C# 8 或更高版本 - 核心库内部使用了 unsafe code;使用该库的项目不需要启用 `AllowUnsafeBlocks` ## Unity 和 IL2CPP 针对类和结构体的自动 packer 使用 `System.Linq.Expressions` 在运行时编译快速的类型化 getter/setter 委托。这在 Mono 和所有标准 .NET runtime 上都能正常工作,但 IL2CPP(Unity 的提前编译器)不支持运行时代码生成,因此 `Expression.Lambda(...).Compile()` 会在运行时抛出异常。 为了解决这个问题,请在构建库时定义 `UNITY` 预处理器符号。在该符号下,getter/setter 编译路径会被替换为普通的 `FieldInfo.GetValue` / `FieldInfo.SetValue` 反射调用,IL2CPP 可以处理这种方式。普通非托管结构体和所有内置集合类型的打包不受影响;只有针对类和托管结构体的自动 packer 会变慢。 在 Unity 项目中,将 `UNITY` 添加到 **Player Settings → Other Settings → Scripting Define Symbols**,或者在 Unity 外部构建 DLL 时传递 `-define:UNITY`。 性能影响:通过 `FieldInfo` 访问字段大约比编译表达式路径慢 10 倍。对于大多数每帧打包一次或在保存/加载时打包的游戏数据结构,这并不明显,但对于打包密集的热路径,可能需要手写自定义 packer 以避免开销。 ## 快速开始 创建一个 packer 实例并保持它。创建实例是昂贵的步骤(反射发生在这里);Pack/Unpack 调用非常快。 ``` // Single type var packer = Packager.Create(); byte[] bytes = packer.PackUP(player); packer.UnPack(bytes, 0, out PlayerData restored); ``` 将多种类型打包到一个缓冲区中: ``` var packer = Packager.Create(); byte[] bytes = packer.PackUP(42, "hello", 3.14f); packer.UnPack(bytes, 0, out int i, out string s, out float f); ``` 写入预分配的缓冲区(消除输出数组的分配): ``` var packer = Packager.Create(); int needed = packer.CalcNeedSize(42); byte[] buffer = new byte[needed]; packer.PackUP(42, buffer, 0); packer.UnPack(buffer, 0, out int value); ``` ## 支持的类型 ### 非托管值类型 任何 C# 非托管类型 —— 即仅包含非托管字段的普通结构体(如 `int`、`float`、`bool`、`Guid`、enum、嵌套的非托管结构体等) —— 都会自动处理。结构体作为原始内存复制,这是最快的路径。 ### 内置托管和集合类型 | 类型 | 说明 | |---|---| | `string` | UTF-8 编码;支持 null | | `T[]` | 任意元素类型;支持最高 N 维的多维数组 | | `List` | 支持 null 和空值 | | `Dictionary` | 支持 null 和空值 | | `SortedDictionary` | 使用 `Comparer.Default` 恢复 | | `HashSet` | 支持 null 和空值 | | `SortedSet` | 恢复时保留排序顺序 | | `Queue` | 保留 FIFO 顺序 | | `Stack` | 保留栈顶顺序 | | `Nullable` / `T?` | 任意结构体 `T` | | `DateTime` | | | `TimeSpan` | | | `Uri` | 存储为 `OriginalString` | | `Version` | 存储为 `ToString()` | ### 类和结构体的自动打包 任何未在上方列出的类型均由自动 packer 处理。它通过反射遍历类型并收集: - 所有非 `readonly`、非 `const`、且非自动属性后备字段的公共实例字段 - 所有同时具有 getter 和 setter 的公共实例自动属性 - 当类型被标记为 `[SearchPrivateFields]` 时,也包括私有字段 每个字段或属性使用相同的规则递归打包,因此支持上述任意组合的嵌套类型。 ``` public class PlayerData { public int Id; public string Name; public Vec3 Position; // unmanaged struct, copied as raw memory public float Health; public int[] Inventory; } var packer = Packager.Create(); byte[] bytes = packer.PackUP(player); packer.UnPack(bytes, 0, out PlayerData restored); ``` ## 属性 (Attributes) ### `[NotPack]` 从自动打包中排除某个字段或属性。 ``` using SmartPackager.Automatic; public class Entity { public int Id; [NotPack] public int RuntimeCacheSlot; // not written to the buffer } ``` ### `[SearchPrivateFields]` 告诉自动 packer 除了公共字段外,还包括私有实例字段。这不影响属性。 ``` using SmartPackager.Automatic; [SearchPrivateFields] public class SecretStore { private int _secret; public int Public; } ``` ## 自定义 Packers 实现 `IPackagerMethod` 以控制特定类型的打包方式。无需注册 —— 该类会在启动时通过扫描所有已加载的程序集自动发现。 ``` using SmartPackager; using SmartPackager.ByteStack; public class PackColor : IPackagerMethod { public Type TargetType => typeof(Color); public bool IsFixedSize => true; // always 4 bytes public void GetSize(ref StackMeter meter, Color source) => meter.Add(1); // 4 bytes public void PackUP(ref StackWriter writer, Color source) => writer.Write(source.ToArgb()); public void UnPack(ref StackReader reader, out Color destination) => destination = Color.FromArgb(reader.Read()); } ``` 完整指南请参阅 [Docs/CustomPackers.md](Docs/CustomPackers.md),包括开放式泛型 packers。 ## 性能 基准测试运行环境:.NET 9.0, Intel Xeon w5-2455X, BenchmarkDotNet 0.14.0, Release build。 | 操作 | 耗时 | 分配内存 | |---|---:|---:| | Pack `int` | 6.06 ns | 32 B | | Pack `string` (~30 chars) | 21.1 ns | 176 B | | Pack `int[1000]` | 250 ns | 4 952 B | | Pack `PlayerData` | 248 ns | 1 328 B | | Pack `WorldChunk` (16×16 heightmap + float[256]) | 391 ns | 3 160 B | | Pack `M` | 19.8 ns | 88 B | | Unpack `int` | 3.50 ns | — | | Unpack `string` (~30 chars) | 25.0 ns | 168 B | | Unpack `int[1000]` | 505 ns | 8 504 B | | Unpack `PlayerData` | 214 ns | 1 136 B | | Unpack `WorldChunk` | 360 ns | 3 824 B | | Roundtrip `int` (pack + unpack) | 11.0 ns | 32 B | | Pack `int`, pre-allocated buffer | **2.26 ns** | **—** | | Unpack `int`, pre-allocated buffer | **3.01 ns** | **—** | | Pack `PlayerData`, pre-allocated buffer | 169 ns | 584 B | | Unpack `PlayerData`, pre-allocated buffer | 217 ns | 1 136 B | `Pack int` 中的 32 B 是返回的 `byte[]`。传入预分配的缓冲区,数组分配和基础设施开销都会消失:2.26 ns 且零分配。 在使用预分配缓冲区的 `Pack PlayerData` 中,剩余的分配(584 B)来自于 `PlayerData` 内部字符串字段的编码,而非序列化基础设施本身。 运行基准测试: ``` dotnet run -c Release --project SP.Benchmarks -- --job short ``` 基准测试源码:[SP.Benchmarks/PackBenchmarks.cs](SP.Benchmarks/PackBenchmarks.cs) ## 许可证 参见 [LICENSE.txt](LICENSE.txt)。
标签:IL2CPP, NuGet包, Unity, unsafe代码, 二进制序列化, 反射优化, 多人体追踪, 对象序列化, 数据压缩, 游戏开发, 类库, 缓存机制, 网络传输, 表达式树