xLua 复杂值类型(struct)跨语言传递零 GC 优化实战指南(jynew 项目)

原创2026-09-15 16:58:491,382 阅读
文章标签:游戏开发

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 优化通道,需要同时满足以下三个条件(原文规定):

  1. 字段类型受约束:struct 允许嵌套其它 struct,但它以及它嵌套的 struct 只能包含以下几种基本类型:byte、sbyte、short、ushort、int、uint、long、ulong、float、double。例如 UnityEngine 定义的大多数值类型(Vector 系列、Quaternion、Color 等)均满足条件,用户自定义的一些 struct 也可以满足。
  2. 配置 GCOptimize 属性:该 struct 需要配置 GCOptimize 属性。对于常用的 UnityEngine 几个 struct(Vector 系列、Quaternion、Color 等)均已经配置了该属性。这个属性可以通过配置文件或者 C# Attribute 实现。
  3. 加入生成代码列表:使用到该 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:

  1. 收集配置:MergeCfg 会扫描静态类中带 <a href="https://link.gitcode.com/i/db8520d36cded4df808d212c649c9624" target="_blank">GCOptimize] 的字段/属性,以及带 [GCOptimize] 标签的类型本身,统一汇入 GCOptimizeList(见 [Generator.cs),并把 OptimizeFlag 记录进 OptimizeCfg。
  2. 合法性过滤:通过 SizeOf(type) != -1 判断该 struct 是否可以按值布局(即只包含受支持的基本类型),只有 !t.IsPrimitive && SizeOf(t) != -1 的 struct 才会进入生成流程(见 Generator.cs);枚举类型则直接从 LuaCallCSharp 列表合并进来,统一生成 push 类型设置。
  3. 生成打包/解包代码: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 优化时,可按以下清单自查:

  1. struct 及其嵌套 struct 的字段是否只包含 byte、sbyte、short、ushort、int、uint、long、ulong、float、double(decimal 与枚举同样受支持);
  2. 是否已为该 struct 添加 [GCOptimize](或用静态列表/动态列表方式登记);
  3. 使用到该 struct 的 Lua/C# 交互入口(delegate、interface、LuaTable/LuaFunction 泛型接口)是否已添加到生成代码列表;
  4. 若 struct 依赖私有字段 + property 访问,是否配置了 AdditionalProperties;
  5. 是否重新执行了代码生成("XLua/Generate Code"),使生成代码覆盖最新配置;
  6. 调用方式是否落在无 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 压力。

登录后查看全文
jynew