下面为你详细梳理 protobuf-net 在 Unity 下的源码剖析,内容涵盖其核心原理、与 Google.Protobuf 的主要区别、关键源码结构、GC优化机制,以及在 Unity 环境下的适配要点。
(以 protobuf-net v3.x 为主,适用于 Unity 2020+,.NET Standard 2.0/2.1)
1. protobuf-net 简介
- protobuf-net 是 Marc Gravell 开发的 C# 高性能 Protobuf 序列化库,兼容 Google Protobuf wire 格式。
- 支持 POCO(Plain Old CLR Object)序列化,支持属性/字段标记
[ProtoMember]
,也支持自动推断。 - 性能优于 Google.Protobuf,GC 压力更小,适合 Unity 场景。
2. 核心原理
2.1 运行时元数据建模
- protobuf-net 通过反射或预编译(MetaType/RuntimeTypeModel)分析类型结构,生成序列化/反序列化元数据。
- 支持自动缓存元数据,避免重复反射。
2.2 IL 动态代码生成
- protobuf-net 在序列化/反序列化时,会动态生成 IL 代码(Emit),极大提升性能,减少装箱和临时对象分配。
- 在 IL2CPP 环境下(如 Unity iOS/Android),会自动降级为反射模式,但仍有优化。
2.3 流式读写
- 支持
Stream
直接读写,避免中间字节数组分配。 - 支持
Span<byte>
(.NET Standard 2.1+),进一步减少 GC。
3. 关键源码结构
3.1 入口类
Serializer
:静态类,提供Serialize
/Deserialize
等 API。RuntimeTypeModel
:类型元数据管理器,负责类型注册、元数据缓存、IL 生成。
3.2 序列化流程
-
类型注册
- 自动或手动注册类型到
RuntimeTypeModel
。 - 通过
[ProtoContract]
、[ProtoMember]
标记字段/属性。
- 自动或手动注册类型到
-
元数据分析
- 反射分析类型结构,生成
MetaType
。 - 支持继承、集合、嵌套类型等。
- 反射分析类型结构,生成
-
IL 代码生成
- 生成高效的序列化/反序列化委托,避免反射和装箱。
-
流式读写
- 直接操作
Stream
,按 Protobuf wire 格式写入/读取字段。
- 直接操作
3.3 反序列化流程
- 通过
Serializer.Deserialize<T>(Stream)
读取流,按字段 tag 解析,反射/IL 赋值到对象字段。 - 支持对象池复用(可自定义),减少 new 操作。
4. GC 优化机制
4.1 避免临时对象分配
- 反序列化时,直接填充已分配对象(如 List/数组),不频繁 new。
- 支持对象池(可通过自定义工厂)。
4.2 流式操作
- 不需要中间
byte[]
,直接用Stream
,大幅减少 GC。
4.3 IL 优化
- 动态生成 IL,避免反射和装箱,极大减少 GC Alloc。
4.4 支持 Span/Memory
- 在支持的平台下,优先用
Span<byte>
,零分配。
5. Unity 环境下的适配
5.1 IL2CPP 支持
- protobuf-net 自动检测 IL2CPP 环境,降级为反射模式。
- 性能略低于 JIT,但 GC 仍优于 Google.Protobuf。
5.2 AOT 预编译
- 可用
protobuf-net.BuildTools
生成预编译元数据,避免运行时反射。 - 适合 iOS/Android 等 AOT 平台。
5.3 Unity Package
- 官方提供 Unity 示例和 Package,兼容 Unity 2020+。
- protobuf-net/examples/Unity
6. 关键源码片段分析
6.1 类型注册与元数据缓存
// 自动注册
[ProtoContract]
public class PlayerData {
[ProtoMember(1)] public int id;
[ProtoMember(2)] public string name;
}
// 手动注册
var model = RuntimeTypeModel.Default;
model.Add(typeof(PlayerData), true);
RuntimeTypeModel
会缓存MetaType
,避免重复反射。
6.2 序列化/反序列化
// 序列化到流
using (var ms = new MemoryStream()) {
Serializer.Serialize(ms, playerData);
// ms.ToArray() 可用于网络发送
}
// 反序列化
ms.Position = 0;
var data = Serializer.Deserialize<PlayerData>(ms);
- 内部直接操作流,无需中间数组。
6.3 IL 动态生成
- 见源码
MetaType.CompileInPlace()
,会生成序列化/反序列化委托。 - IL2CPP 下自动降级为反射。
6.4 对象池支持
- 可通过自定义工厂方法,结合 Unity 对象池,减少 GC。
7. 与 Google.Protobuf 的主要区别
特性 | protobuf-net | Google.Protobuf |
---|---|---|
类型支持 | POCO/属性/字段 | 仅生成代码 |
IL 动态生成 | 支持 | 不支持 |
AOT/IL2CPP | 支持(降级反射) | 支持 |
GC 优化 | 优秀 | 一般 |
流式操作 | 支持 | 支持 |
代码生成 | 可选 | 必须 |
Unity 适配 | 好 | 一般 |
8. 实践建议
- 优先用流式 API,避免中间数组。
- AOT 平台用 BuildTools 预编译,提升性能。
- 结合对象池,复用消息对象。
- 用 Profiler 检查 GC Alloc,定位热点。
- 升级到 v3+,新版本性能和兼容性更好。
9. 参考资料
10. 总结
- protobuf-net 在 Unity 下性能优异,GC 压力小,适合高性能场景。
- 其核心优势在于 IL 动态生成、流式操作和灵活的类型支持。
- 在 IL2CPP/AOT 下需注意预编译和反射降级,实际表现仍优于 Google.Protobuf。
- 推荐在 Unity 项目中优先选用 protobuf-net,并结合对象池、流式 API 进一步优化 GC。
下面继续深入剖析 protobuf-net 在 Unity 下的源码实现细节,包括其序列化/反序列化的底层机制、IL2CPP/AOT适配的源码策略、对象池与GC优化的具体实现,以及常见的源码扩展点和调试技巧。
1. 序列化/反序列化底层机制
1.1 序列化流程源码梳理
以 Serializer.Serialize<T>(Stream, T)
为例,核心流程如下:
-
类型元数据获取
var model = RuntimeTypeModel.Default; var metaType = model[typeof(T)];
RuntimeTypeModel
负责类型注册和元数据缓存。MetaType
描述了类型的字段、属性、序列化顺序等。
-
委托生成与缓存
- 首次序列化时,
MetaType
会通过CompileInPlace()
生成高效的序列化委托(IL Emit)。 - 后续调用直接复用委托,避免反射。
- 首次序列化时,
-
流式写入
- 通过
ProtoWriter
类,按 Protobuf wire 格式将字段写入Stream
。 ProtoWriter
内部有缓冲区,减少小块写入带来的GC。
- 通过
-
字段序列化
- 每个字段由
ValueMember
负责序列化,支持基础类型、嵌套对象、集合等。 - 支持自定义序列化器(IProtoSerializer)。
- 每个字段由
源码片段(简化版):
public static void Serialize<T>(Stream dest, T instance)
{
var model = RuntimeTypeModel.Default;
var metaType = model[typeof(T)];
var writer = ProtoWriter.Create(dest, model, null);
metaType.Serialize(writer, instance);
writer.Close();
}
1.2 反序列化流程源码梳理
-
类型元数据获取
- 同上,获取
MetaType
。
- 同上,获取
-
委托生成与缓存
- 反序列化委托同样通过 IL Emit 生成并缓存。
-
流式读取
- 通过
ProtoReader
类,按 Protobuf wire 格式从Stream
读取字段。
- 通过
-
字段赋值
- 通过反射或IL,直接赋值到对象字段/属性,支持嵌套、集合等。
源码片段(简化版):
public static T Deserialize<T>(Stream source)
{
var model = RuntimeTypeModel.Default;
var metaType = model[typeof(T)];
var reader = ProtoReader.Create(source, model, null);
var obj = metaType.Deserialize(reader, null);
reader.Close();
return (T)obj;
}
2. IL2CPP/AOT 适配源码策略
2.1 IL Emit 与反射降级
- 在支持 JIT 的平台(如 Editor/Mono),protobuf-net 用
System.Reflection.Emit
动态生成 IL,极致性能。 - 在 IL2CPP/AOT 平台(如 iOS/Android),Emit 不可用,自动降级为反射模式。
- 相关源码:
MetaType.CompileInPlace()
内部有平台检测逻辑。
2.2 AOT 预编译支持
- protobuf-net 提供 BuildTools,可在构建时生成序列化/反序列化委托,避免运行时反射。
- 生成的代码会被 Unity 编译进包体,IL2CPP 可直接调用。
- 相关源码:
protobuf-net.BuildTools
项目,生成.dll
或.cs
文件。
2.3 反射模式下的GC优化
- protobuf-net 反射模式下仍做了缓存和优化,避免每次都反射。
- 通过
MemberInfo
缓存、委托缓存等手段,尽量减少GC。
3. 对象池与GC优化实现
3.1 内部缓冲区池
ProtoWriter
和ProtoReader
内部有缓冲区池,避免频繁分配小数组。- 相关源码:
ProtoWriter.BufferPool
,ProtoReader.BufferPool
。
3.2 支持外部对象池
- protobuf-net 支持自定义工厂方法(如
RuntimeTypeModel.Default.SetFactory
),可结合 Unity 的对象池系统复用消息对象。 - 这样可以避免频繁 new 消息对象,进一步降低GC。
示例:
RuntimeTypeModel.Default[typeof(MyMessage)].SetFactory(() => MyMessagePool.Get());
3.3 集合类型的复用
- 反序列化 List/Array 时,protobuf-net 会尝试复用已有集合,避免 new。
- 相关源码:
ValueMember.ReadList
,ValueMember.ReadArray
。
4. 常见源码扩展点
4.1 自定义序列化器
- 实现
IProtoSerializer
,可自定义字段的序列化/反序列化逻辑。 - 适合特殊类型(如 Unity Vector3、Color 等)。
4.2 自定义类型工厂
- 通过
SetFactory
,为类型指定自定义构造逻辑(如对象池)。
4.3 ProtoContract/ProtoMember 自动化
- 可用 T4/Source Generator 自动为 POCO 类加上
[ProtoContract]
、[ProtoMember]
,减少手写。
5. 调试与性能分析技巧
5.1 查看序列化元数据
RuntimeTypeModel.Default.GetSchema(typeof(T))
可输出 .proto 格式,便于调试。
5.2 Profiler 检查 GC
- Unity Profiler 监控
GC Alloc
,定位 protobuf-net 相关分配。
5.3 日志与异常
- protobuf-net 内部有详细异常和日志,便于定位序列化/反序列化问题。
6. 典型源码结构(简化)
protobuf-net/
├─ ProtoBuf/
│ ├─ Serializer.cs // 静态入口
│ ├─ RuntimeTypeModel.cs // 类型元数据管理
│ ├─ MetaType.cs // 单类型元数据
│ ├─ ValueMember.cs // 字段/属性元数据
│ ├─ ProtoWriter.cs // 序列化流写入
│ ├─ ProtoReader.cs // 反序列化流读取
│ ├─ IProtoSerializer.cs // 自定义序列化器接口
│ └─ ...
7. 典型问题与源码应对
- IL2CPP下性能下降?
反射模式下性能略低,但通过AOT预编译和对象池可大幅缓解。 - Unity特有类型如何支持?
实现IProtoSerializer
,注册到RuntimeTypeModel
。 - 大对象GC?
结合对象池、流式API、避免大数组分配。
8. 结论
- protobuf-net 源码结构清晰,GC优化手段丰富,适合Unity高性能场景。
- 通过 IL Emit、流式操作、对象池、AOT预编译等多重机制,兼顾性能与兼容性。
- 在 Unity 下建议结合对象池、AOT工具链、Profiler监控,发挥最大优势。