微信小游戏 Unity 适配器 xLua 复杂值类型(struct)GC 优化完整指南

原创2026-10-02 17:14:421,329 阅读
文章标签:游戏开发移动开发WebAssembly

微信小游戏 Unity 适配器 xLua 复杂值类型(struct)GC 优化完整指南

xLua 中,C# 复杂值类型(struct)默认以引用方式传递到 Lua 侧,每次传递都会产生一次 boxing 与一次 C# GC 分配。本文以 xLua 复杂值类型(struct)gc 优化指南 为骨架,结合仓库内 xLua 的配置、无 GC 示例 与生成器源码 Generator.cs,系统讲解 struct GC 优化方案的工作原理、满足条件、三种配置方式与落地步骤,帮助读者在微信小游戏场景下把 Vector 系列、Quaternion、Color 等值类型在 Lua 与 C# 之间的传递彻底做到无 GC 分配。

复杂值类型为什么会产生 GC

在 xLua 中,复杂值类型(struct)的默认传递方式是引用传递。这个机制的工作过程是:

  1. 值类型需要先进行 boxing(装箱),在托管堆上创建一个新对象;
  2. 将装箱后的引用传递给 Lua 侧使用;
  3. Lua 侧使用完毕、释放该对象的引用时,触发一次 C# GC。

由于每次 boxing 都会产生一个新对象,因此只要 struct 在 Lua 与 C# 之间来回传递一次,托管堆上就会多出一个待回收的垃圾对象。在高频调用(如每帧更新位置、逐物体同步坐标)的场景下,这类分配会迅速累积,直接表现为 GC 频率上升、CPU 峰值抖动,在内存更敏感的微信小游戏运行时(WASM)上影响尤为明显。

为此,xLua 实现了一套 struct 的 GC 优化方案:只要通过简单配置,满足条件的 struct 在传递到 Lua 侧时即可做到无 GC。

struct 需要满足什么条件

要让一个 struct 享受到 GC 优化,需要同时满足以下三个条件:

1. 字段类型约束

struct 允许嵌套其它 struct,但它以及它嵌套的 struct 只能包含以下基本类型:

byte、sbyte、short、ushort、int、uint、long、ulong、float、double

例如 Unity 定义的大多数值类型(Vector 系列、Quaternion、Color 等)以及用户自定义的满足上述字段约束的 struct 均符合条件。字段不能包含 string、引用类型对象或其它不符合约束的类型。

2. 配置 GCOptimize 属性

该 struct 必须配置 GCOptimize 属性。对于常用的 UnityEngine 值类型(Vector 系列、Quaternion、Color 等)xLua 已经内置配置好了,用户无需重复配置;用户自定义 struct 则需要自行配置。属性可以通过配置文件或 C# Attribute 两种方式实现(详见下文)。

3. 加入生成代码列表

使用到该 struct 的地方(struct 自身、相关 delegate、相关数组等)需要添加到生成代码列表,并执行代码生成,让 xLua 为该类型生成专门的 GC 优化包装代码。

如何配置:三种方式详解

xLua 的所有配置都支持三种方式:打标签(Attribute)、静态列表、动态列表。GCOptimize 同样遵循这一规则。下面逐一展开。

方式一:C# Attribute 打标签

直接为 struct 类型打上 GCOptimizeAttribute(同时建议打上 LuaCallCSharp)。仓库示例 05_NoGc/NoGc.cs 中的自定义 struct 就是这样配置的:

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;
}

注意 MyStruct 嵌套了同样打了 GCOptimize 标签的 Pedding,且所有字段均为值类型,完全符合前述字段类型约束。从属性定义源码 GenAttributes.cs 可以看到 GCOptimizeAttribute 还带一个可选的 OptimizeFlag 参数,默认值为 OptimizeFlag.Default:

[Flags]
public enum OptimizeFlag
{
    Default = 0,
    PackAsTable = 1
}

public class GCOptimizeAttribute : Attribute
{
    OptimizeFlag flag;
    public OptimizeFlag Flag
    {
        get { return flag; }
    }

    public GCOptimizeAttribute(OptimizeFlag flag = OptimizeFlag.Default)
    {
        this.flag = flag;
    }
}

方式二:静态列表配置

有时我们无法直接给一个类型打标签(例如系统 API、没有源码的第三方库中的类型),这时可以在一个 静态类 里声明一个静态字段,字段类型只要实现了 IEnumerable<Type> 即可(BlackList 和 AdditionalProperties 例外),再为这个字段打上 GCOptimize 标签:

using System;
using System.Collections.Generic;
using XLua;

public static class MyGCOptimizeConfig
{
    [GCOptimize]
    public static List<Type> gc_optimize_list = new List<Type>()
    {
        typeof(MyNamespace.MyStruct),
        typeof(MyNamespace.MyStruct2),
    };
}

字段需要放在静态类中,且建议放在 Editor 目录下。这也是 xLua 对几个 UnityEngine 值类型的官方内置配置方式。在 GenAttributes.cs 的 SysGenConfig 类中可以看到:

public static class SysGenConfig
{
    [GCOptimize]
    static List<Type> GCOptimize
    {
        get
        {
            return new List<Type>()
            {
                typeof(UnityEngine.Vector2),
                typeof(UnityEngine.Vector3),
                typeof(UnityEngine.Vector4),
                typeof(UnityEngine.Color),
                typeof(UnityEngine.Quaternion),
                typeof(UnityEngine.Ray),
                typeof(UnityEngine.Bounds),
                typeof(UnityEngine.Ray2D),
            };
        }
    }
    ...
}

因此 Vector2/Vector3/Vector4、Color、Quaternion、Ray、Bounds、Ray2D 这些常用值类型无需任何额外配置即可享受 GC 优化,直接满足原文档中"UnityEngine 的几个 struct 已经配置了该属性"的说明。

方式三:动态列表配置

如果希望按名字空间、按程序集等规则动态收集类型,可以声明一个打了相应标签的静态属性。GCOptimize 同样支持这种方式:

using System;
using System.Collections.Generic;
using System.Linq;
using XLua;

public static class MyGCOptimizeConfig
{
    [GCOptimize]
    public static IEnumerable<Type> by_property
    {
        get
        {
            return from type in Assembly.Load("Assembly-CSharp").GetTypes()
                   where type.Namespace == "MyGame.Data" && type.IsValueType
                   select type;
        }
    }
}

Getter 是代码,可以实现很多效果,比如按名字空间配置、按程序集配置等等。该属性同样需要放到静态类中,建议放到 Editor 目录。

配置通用建议(来自 xLua 的配置):列表方式均必须是 static 的字段/属性,且必须放到一个 static 类;建议尽量不用标签方式(il2cpp 下会增加不少代码量),建议列表方式配置放在 Editor 目录。

使用到该 struct 的地方也要加入生成列表

除了 struct 本身,凡是在 Lua 与 C# 之间传递该 struct 的入口,也需要加入生成代码列表,才能让无 GC 路径真正生效。这些入口包括:

  • 携带 struct 参数的 delegate(C# 调用 Lua 函数时)——需要打 CSharpCallLua;
  • 携带 struct 参数的 C# 方法——所在的类型需要打 LuaCallCSharp;
  • struct 数组——数组访问路径同样走 GC 优化代码。

仓库 05_NoGc/NoGc.cs 给出了完整示例,把 struct、枚举、delegate、数组全部串联起来:

[GCOptimize]
[LuaCallCSharp]
public struct MyStruct { ... }

[LuaCallCSharp]
public enum MyEnum
{
    E1,
    E2
}

[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);

配置完成后,需要执行 XLua/Generate Code 菜单生成代码。生成器源码 Generator.cs 中的逻辑印证了收集流程:GCOptimizeList 收集所有被 GCOptimizeAttribute 标注的类型(包括 SysGenConfig 静态列表里的内置类型),随后通过 GenPackUnpack(types, save_path) 为这些类型生成解包/打包代码,最终输出到 Assets/XLua/Gen/ 目录(路径可通过 CSObjectWrapEditor.GenPath 配置修改)。

无 GC 场景与示例验证

完成配置后,以下场景中的值类型传递均不产生(C#)GC 分配,来自 FAQ 与官方无 GC 示例的验证:

  1. 所有基本值类型(所有整数、所有浮点数、decimal)——本身无 GC;
  2. 所有枚举类型——需要把该枚举加入 GCOptimize;
  3. 字段只包含值类型的 struct(可嵌套其它只包含值类型的 struct)——需要把该 struct 加入 GCOptimize。

使用方式包括:

  • 用 delegate 调用 Lua 函数、传值类型参数;
  • 用 LuaTable、LuaFunction 的无 GC 接口(如 Func<T1, T2, TResult>)传值类型;
  • 传递值类型数组(如 Vector3[]、自定义 struct 数组)并允许 Lua 侧直接读写数组元素;
  • 通过 interface(CSharpCallLua)让 Lua 实现回调并交换数组内容;
  • 通过 LuaEnv.Global.Set/Get 在 Lua 与 C# 之间传递 struct 值。

NoGc 示例的运行时验证代码展示了几条关键路径(NoGc.cs):

// C# 通过 delegate 调用 Lua,传 Vector3 / 自定义 struct / 枚举,均无 GC
f2(new Vector3(1, 2, 3));
MyStruct mystruct1 = new MyStruct(5, 6);
f3(mystruct1);
f4(MyEnum.E1);

// Lua 访问 C# 值类型数组,无 GC(含 primitive / Vector3 / 自定义 struct / enum / decimal 数组)
farr(a1);
farr(a2);
farr(a3);
farr(a4);
farr(a5);

// Lua 侧也可用 table 构造 struct 参数传入 C#,例如
-- local r = monoBehaviour:Vector3ParamMethod({x = 1, y = 2, z = rnd})
-- local r = monoBehaviour:StructParamMethod({a = 1, b = rnd, e = {c = rnd}})

Lua 侧同样可以无 GC 地读写 C# 的 struct 数组元素,例如交换 Vector3[] 中两个元素:

function array_exchange(arr)
    arr[0], arr[1] = arr[1], arr[0]
end

进阶:AdditionalProperties 处理私有字段

默认情况下,GCOptimize 只对 public 的 field 打解包。如果某些 struct 把字段做成私有、通过 property 对外访问,就需要用到 XLua.AdditionalProperties 这一扩展配置。

标签方式比较简单(在对应属性上加标签即可);配置方式要求是 Dictionary<Type, List<string>> 类型,Key 是要生效的类型,Value 是属性名列表。可参考 xLua 对几个 UnityEngine 值类型的内置配置 SysGenConfig 类(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、Ray2D、Bounds 这类 struct 虽然底层字段是私有(如 m_Origin、m_Direction、m_Center、m_Extents),但通过 AdditionalProperties 把 origin、direction、center、extents 属性纳入 GC 优化的解包范围,从而让这些类型的完整属性访问也做到无 GC。

实操清单与注意事项

在微信小游戏 Unity 工程中落地 struct GC 优化,按以下步骤执行即可:

  1. 确认字段约束:struct(含嵌套 struct)只包含 byte/sbyte/short/ushort/int/uint/long/ulong/float/double 字段;
  2. 配置 GCOptimize:自定义 struct 打 [GCOptimize] 标签,或在 Editor 目录的静态类中通过静态列表/动态列表配置;UnityEngine 的 Vector 系列、Quaternion、Color 等已内置配置,无需处理;
  3. 登记使用入口:把携带该 struct 的 delegate/interface 加入 CSharpCallLua,把暴露该 struct 方法的 C# 类型加入 LuaCallCSharp;
  4. 执行代码生成:在 Unity 菜单执行 XLua/Generate Code,生成代码默认输出到 Assets/XLua/Gen/;
  5. 按无 GC 接口调用:优先使用 delegate 调用、LuaTable/LuaFunction 无 GC 接口与数组访问路径;
  6. 私有字段场景:如 struct 通过 property 暴露私有字段,用 AdditionalProperties 补充配置。

需要注意的前提与限制:

  • 字段包含 string、引用类型等不符合约束的 struct 无法启用 GC 优化,会退回默认的引用传递(boxing + 释放)路径;
  • GC 优化依赖生成代码,配置变更后必须重新执行 Generate Code;
  • 该方案解决的是 C# 侧 GC 分配,Lua 侧自身的 GC 策略(如 LuaEnv.FullGc、GcPause、GcStepmul 调优)属于另一个话题,可参考 FAQ 中的相关说明;
  • 本仓库的 xLua 位于微信小游戏 WebGL 工程 Demo/xLua_WebGL 中,上述配置与生成流程在 Unity WebGL / 微信小游戏导出目标下同样适用。
登录后查看全文
minigame-unity-webgl-transform