xLua 复杂值类型(struct)跨语言传递零 GC 优化实战指南(jynew 项目)
xLua 复杂值类型(struct)跨语言传递零 GC 优化实战指南(jynew 项目)
复杂值类型(struct)在 C# 与 Lua 之间的默认传递方式是"引用传递 + 装箱(boxing)",每次传参都会产生新对象并在 Lua 侧释放时触发一次 GC。xLua 提供了基于生成代码的 GC 优化方案:只要 struct 满足字段类型约束并完成 GCOptimize 配置,就能在 Lua 与 C# 之间以零 GC 方式传递。本文以 jynew 仓库中内置的 xLua 源码与示例为核心,完整讲解该方案的适用条件、三种配置方式、底层生成机制与实战用法,帮助你为自定义 struct 一键开启无 GC 通道。
复杂值类型的 GC 问题
xLua 复杂值类型(struct)的默认传递方式是引用传递。这种方式要求先对值类型进行 boxing 装箱,再传递给 Lua;Lua 使用完毕后释放该引用。由于值类型每次 boxing 都会产生一个新对象,当 Lua 侧使用完毕释放该对象的引用时,就会产生一次 GC。
在每帧高频调用的战斗逻辑、技能结算、坐标同步等场景中,如果频繁在 Lua 与 C# 之间传递 Vector3、Quaternion 或自定义 struct,这种隐式装箱会累积大量托管堆分配,直接造成卡顿。
为此,xLua 实现了一套 struct 的 GC 优化方案:只要通过简单的配置,满足条件的 struct 传递到 Lua 侧即可做到无 GC。
struct 需要满足什么条件?
要让一个 struct 走 GC 优化通道,需要同时满足以下三个条件(原文规定):
- 字段类型受约束:struct 允许嵌套其它 struct,但它以及它嵌套的 struct 只能包含以下几种基本类型:
byte、sbyte、short、ushort、int、uint、long、ulong、float、double。例如 UnityEngine 定义的大多数值类型(Vector 系列、Quaternion、Color 等)均满足条件,用户自定义的一些 struct 也可以满足。 - 配置 GCOptimize 属性:该 struct 需要配置
GCOptimize属性。对于常用的 UnityEngine 几个 struct(Vector 系列、Quaternion、Color 等)均已经配置了该属性。这个属性可以通过配置文件或者 C# Attribute 实现。 - 加入生成代码列表:使用到该 struct 的地方,需要添加到生成代码列表。
此外,xLua 官方 FAQ(见 faq.md)给出了更完整的"无 GC 值类型"清单,与上述条件互相印证:
- 所有的基本值类型(所有整数、所有浮点数、
decimal)——本身无 GC; - 所有的枚举类型——需要把该类型加到 GCOptimize;
- 字段只包含值类型的 struct,可嵌套其它只包含值类型的 struct——需要把该类型加到 GCOptimize。
同时,FAQ 也明确了"零 GC"的前提使用方式:使用 delegate 调用 Lua 函数,或者使用 LuaTable、LuaFunction 的无 GC 接口,或者使用数组,这些场景下上述值类型都是无 GC 的。
如何配置:三种配置方式
原文指出配置方式参见 XLua的配置.md 的 GCOptimize 章节,即仓库中的 configure.md。xLua 的所有配置都支持三种方式:打标签(Attribute)、静态列表、动态列表。且遵循"两必须两建议":
- 列表方式均必须是 static 的字段/属性;
- 列表方式均必须放到一个 static 类;
- 建议不用标签方式(在 il2cpp 下会增加不少代码量);
- 建议列表方式配置放 Editor 目录。
方式一:打标签(Attribute)
直接在 struct 上同时标记 <a href="https://link.gitcode.com/i/5a66e362eb6c17424fc956438eb13c5b" target="_blank">GCOptimize] 与 [LuaCallCSharp]。在 [NoGc.cs 示例中就有标准写法:
using XLua;
[GCOptimize]
[LuaCallCSharp]
public struct Pedding
{
public byte c;
}
[GCOptimize]
[LuaCallCSharp]
public struct MyStruct
{
public MyStruct(int p1, int p2)
{
a = p1;
b = p2;
c = p2;
e.c = (byte)p1;
}
public int a;
public int b;
public decimal c;
public Pedding e; // 嵌套另一个满足条件的 struct
}
注意 MyStruct 中嵌套了 Pedding,二者均打上 <a href="https://link.gitcode.com/i/35f2033ee4d036cb4cbee828430a4785" target="_blank">GCOptimize],这正是"允许嵌套其它满足条件的 struct"的直观演示。[GCOptimize] 属性定义位于 [GenAttributes.cs,其构造函数还接受一个可选的 OptimizeFlag 参数。
方式二:静态列表
当无法直接给类型打标签时(例如系统 API、无源码的库、实例化的泛型类型),可以在一个静态类中声明一个静态字段,字段类型只要实现了 IEnumerable<Type> 即可,然后为这个字段加上 [GCOptimize]:
[GCOptimize]
public static List<Type> my_gc_optimize_list = new List<Type>()
{
typeof(MyStruct),
typeof(Vector3),
};
该字段必须放到一个静态类里,建议放到 Editor 目录。
方式三:动态列表
声明一个静态属性并打上 [GCOptimize] 标签,Getter 是代码,可以实现按名字空间、按程序集等灵活筛选:
[GCOptimize]
public static List<Type> by_property
{
get
{
return (from type in Assembly.Load("Assembly-CSharp").GetTypes()
where type.Namespace == "MyGame.Data"
select type).ToList();
}
}
同样需要放到静态类中,建议放到 Editor 目录。
标签方式的补充:旧的 GenFlag.GCOptimize
需要提醒的是,历史上 xLua 曾在 LuaCallCSharpAttribute 中通过 GenFlag.GCOptimize 标记(见 GenAttributes.cs),该枚举值已被标记为 [Obsolete("use GCOptimizeAttribute instead")],请统一使用独立的 GCOptimizeAttribute。
AdditionalProperties:为私有字段打解包
默认情况下 GCOptimize 只对 public 的 field 打解包。如果某些 struct 把 field 做成私有的、通过 property 来访问 field,就需要用到 XLua.AdditionalProperties 这个 GCOptimize 的扩展配置。
标签方式比较简单(在对应成员上加即可);配置方式要求是 Dictionary<Type, List<string>> 类型:Dictionary 的 Key 是要生效的类型,Value 是属性名列表。xLua 对几个 UnityEngine 值类型的官方配置(SysGCOptimize 类)就是最好的参考,见 GenAttributes.cs:
[AdditionalProperties]
static Dictionary<Type, List<string>> AdditionalProperties
{
get
{
return new Dictionary<Type, List<string>>()
{
{ typeof(UnityEngine.Ray), new List<string>() { "origin", "direction" } },
{ typeof(UnityEngine.Ray2D), new List<string>() { "origin", "direction" } },
{ typeof(UnityEngine.Bounds), new List<string>() { "center", "extents" } },
};
}
}
例如 Ray 的 origin、direction 是只读属性,底层依赖私有字段,通过该配置即可让 GC 优化代码正确打解包这些 property。
内置的 UnityEngine 值类型优化
xLua 对常用 UnityEngine 值类型已内置 GC 优化配置。在 GenAttributes.cs 的 SysGenConfig 静态类中可以看到完整清单:
[GCOptimize]类型:UnityEngine.Vector2、Vector3、Vector4、Color、Quaternion、Ray、Bounds、Ray2D;[AdditionalProperties]补充:Ray(origin、direction)、Ray2D(origin、direction)、Bounds(center、extents)。
这意味着在 jynew 项目中直接使用上述类型跨 Lua/C# 传递时,无需任何额外配置即可享受零 GC 优化。
源码视角:优化代码如何生成
GC 优化并非运行时魔法,而是由编辑器期的生成器在"Generate Code"阶段产出的打包/解包代码。关键实现在 Generator.cs:
- 收集配置:
MergeCfg会扫描静态类中带<a href="https://link.gitcode.com/i/db8520d36cded4df808d212c649c9624" target="_blank">GCOptimize]的字段/属性,以及带[GCOptimize]标签的类型本身,统一汇入GCOptimizeList(见 [Generator.cs),并把OptimizeFlag记录进OptimizeCfg。 - 合法性过滤:通过
SizeOf(type) != -1判断该 struct 是否可以按值布局(即只包含受支持的基本类型),只有!t.IsPrimitive && SizeOf(t) != -1的 struct 才会进入生成流程(见 Generator.cs);枚举类型则直接从LuaCallCSharp列表合并进来,统一生成 push 类型设置。 - 生成打包/解包代码:
GenPackUnpack为每个符合条件的 struct 生成 pack(C# → Lua)与 unpack(Lua → C#)代码,并基于SizeOf逐字段计算偏移布局,同时遍历 public 字段与 AdditionalProperties 配置的属性(见 Generator.cs)。
从源码结构可以推断:生成出的代码将 struct 拆解为与 Lua 栈直接交互的原始数据读写,从根本上绕过了 boxing,因此既不产生托管堆分配,也避免了反射的开销。另外在 OptimizeFlag 中还存在 PackAsTable = 1 选项(见 GenAttributes.cs),可将 struct 以 table 形式打包,供特殊场景选用。
实战示例:05_NoGc 完整链路
仓库中的 NoGc.cs 是官方提供的"无 GC"综合示例,覆盖了 struct 优化方案的几乎所有用法,非常适合作为落地模板。
1. 声明无 GC 调用用的 delegate / interface(需加 [CSharpCallLua]):
[CSharpCallLua]
public delegate int IntParam(int p);
[CSharpCallLua]
public delegate Vector3 Vector3Param(Vector3 p);
[CSharpCallLua]
public delegate MyStruct CustomValueTypeParam(MyStruct p);
[CSharpCallLua]
public delegate MyEnum EnumParam(MyEnum p);
[CSharpCallLua]
public delegate decimal DecimalParam(decimal p);
[CSharpCallLua]
public delegate void ArrayAccess(Array arr);
[CSharpCallLua]
public interface IExchanger
{
void exchange(Array arr);
}
2. 声明对应类型的数组(数组访问同样无 GC):
public double[] a1 = new double[] { 1, 2 };
public Vector3[] a2 = new Vector3[] { new Vector3(1, 2, 3), new Vector3(4, 5, 6) };
public MyStruct[] a3 = new MyStruct[] { new MyStruct(1, 2), new MyStruct(3, 4) };
public MyEnum[] a4 = new MyEnum[] { MyEnum.E1, MyEnum.E2 };
public decimal[] a5 = new decimal[] { 1.00001M, 2.00002M };
3. 在 Update 中高频调用(每帧执行,验证零 GC):
f1(1); // 基本类型
f2(new Vector3(1, 2, 3)); // Vector3
f3(new MyStruct(5, 6)); // 自定义复杂值类型
f4(MyEnum.E1); // 枚举
f5(dec1); // decimal
farr(a1); // 基本类型数组
farr(a2); // Vector3 数组
farr(a3); // 自定义 struct 数组
farr(a4); // 枚举数组
farr(a5); // decimal 数组
luaenv.Global.Set(123.0001, mystruct1); // LuaTable 无 GC 接口读写 struct
luaenv.Global.Get(123.0001, out mystruct2);
4. Lua 侧构造与访问 struct(Lua 中可用 CS.UnityEngine.Vector3(7, 8, 9) 构造,也可以用 table 直接传参):
local v3 = CS.UnityEngine.Vector3(7, 8, 9)
local vt = CS.XLuaTest.MyStruct(5, 6)
local r = monoBehaviour:Vector3ParamMethod({x = 1, y = 2, z = rnd}) -- table 转 Vector3
assert(r.x == 1 and r.y == 2 and r.z == rnd)
r = monoBehaviour:StructParamMethod({a = 1, b = rnd, e = {c = rnd}}) -- 嵌套 struct
assert(r.b == rnd and r.e.c == rnd)
从该示例可以总结出启用零 GC 的完整清单:struct 加 [GCOptimize] + [LuaCallCSharp]、跨语言 delegate/interface 加 [CSharpCallLua]、并全部纳入生成代码列表后,执行"XLua/Generate Code"生成代码即可。
配置检查清单
在 jynew 项目中接入自定义 struct 的零 GC 优化时,可按以下清单自查:
- struct 及其嵌套 struct 的字段是否只包含
byte、sbyte、short、ushort、int、uint、long、ulong、float、double(decimal 与枚举同样受支持); - 是否已为该 struct 添加
[GCOptimize](或用静态列表/动态列表方式登记); - 使用到该 struct 的 Lua/C# 交互入口(delegate、interface、LuaTable/LuaFunction 泛型接口)是否已添加到生成代码列表;
- 若 struct 依赖私有字段 + property 访问,是否配置了
AdditionalProperties; - 是否重新执行了代码生成("XLua/Generate Code"),使生成代码覆盖最新配置;
- 调用方式是否落在无 GC 场景内:delegate 调用、LuaTable/LuaFunction 的无 GC 接口、数组访问。
总结
xLua 的 struct GC 优化方案以"生成代码 + 白名单配置"为核心:先通过 GCOptimize(Attribute / 静态列表 / 动态列表三选一)将满足字段约束的 struct 纳入 GCOptimizeList,再由编辑器生成器基于 SizeOf 逐字段生成 pack/unpack 代码,从而在 Lua 与 C# 之间以纯栈数据交互取代装箱分配。原指南中的三条硬性条件(字段类型约束、GCOptimize 配置、加入生成代码列表)与官方示例 NoGc.cs、FAQ 说明 faq.md、完整配置章节 configure.md 相互印证,构成了从"理解原理 → 配置 → 生成 → 验证"的完整闭环。对于以 Lua 承载玩法逻辑的 jynew 项目,合理利用该方案可以显著降低战斗、移动、UI 刷新等高频路径上的 GC 压力。