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代码, 二进制序列化, 反射优化, 多人体追踪, 对象序列化, 数据压缩, 游戏开发, 类库, 缓存机制, 网络传输, 表达式树