1.介绍一下闭包

什么是闭包:子函数可以使用父函数中的局部变量

闭包的主要作用有两个:

一是简洁,在不需要使用时,就不生成对象,也不需要函数名;

二是捕获外部变量,形成不同的调用环境。

闭包的原理

闭包(函数)编译时会生成原型(prototype),包含参数、调试信息、虚拟机指令等一系列该闭包的源信息,其中在递归编辑内层函数时,会为内层函数生成指令,同时为该内层函数需要的所有upvalue创建表,以便之后调用时进行upvalue值搜索

在lua中,会生成一个全局栈,所有的upvalue都会指向该栈中的值,若对应的参数离开的作用域,栈中的值也会被释放,upvalue的指针会指向自己,等待被gc

闭包运行时,会通过创建指向upvalue的指针,并循环upvalue linked list,找到所需要的外部变量进行运行

2. Lua的内存管理机制


Lua的内存管理机制是基于垃圾回收(garbage collection)的原理,并采用了标记-清除(mark and sweep)算法。

对象分配:

当Lua程序需要分配内存来存储对象时,它会使用自己的内存分配器。Lua的内存分配器是基于固定大小的内存块(blocks)的,这些内存块被称为"内存块列表"(block list)。当程序需要分配一个对象时,内存分配器会从内存块列表中选择一个合适的内存块来存储该对象。

垃圾回收器:

Lua的垃圾回收器周期性地运行,以检测并清除不再被引用的对象。垃圾回收器分为两个阶段:标记阶段和清除阶段。

标记阶段:

垃圾回收器从一个称为"根集"(root set)的地方开始,逐步遍历所有可达的对象,并将它们标记为活动对象。根集包括全局变量、当前执行的函数中的局部变量以及C函数中的一些特殊数据结构。

清除阶段:

在标记阶段完成后,垃圾回收器会扫描所有的对象,并清除那些没有被标记的对象(不在活动状态)。这些未被标记的对象被认为是不再被引用的对象,可以被安全地释放内存。

分代回收:

Lua的垃圾回收器采用了分代回收的策略。它将内存对象分为几代(generation),每一代都有不同的优先级。新分配的对象通常被放置在第一代中,而较老的对象则会逐渐晋升到更高的代。这种分代的机制可以提高垃圾回收的效率,因为新分配的对象通常具有较短的生命周期。

手动内存管理:

Lua还提供了一些手动管理内存的机制。其中一个是collectgarbage函数,它可以手动触发垃圾回收过程。通过调整collectgarbage函数的参数,可以对垃圾回收器的行为进行一些控制,例如设置不同的垃圾回收阈值、关闭垃圾回收等。此外,Lua还提供了weak table来帮助管理对象之间的引用关系,以避免循环引用导致的内存泄漏。

3.多人开发如何避免全局变量泛滥

_G 是 Lua 中的一个特殊表,表示全局环境,所有全局变量实际上都保存在这个表中。通过元表(metatable)机制,我们可以拦截对全局变量 _G 的访问和赋值操作,以帮助开发者调试代码中未定义或意外使用的全局变量。

setmetatable(_G, {...})_G 设置一个元表,并定义了两个元方法:

  • __newindex:拦截对未定义全局变量的赋值操作;
  • __index:拦截对未定义全局变量的访问操作。

代码示例:

-- 设置 _G(全局环境表)的元表
setmetatable(
  _G,
  {
    -- 当尝试给不存在的全局变量赋值时触发(如:foo = 123,但 foo 没有事先定义)
    __newindex = function(_, key)
      print("attempt to add a new value to global, key: " .. key)
    end,

    -- 当尝试访问一个未定义的全局变量时触发(如:print(bar),但 bar 没有定义)
    __index = function(_, key)
      print("attempt to index a global value, key: " .. key)
    end
  }
)

4.热更新的原理

前置1:lua中的require是什么

Lua 中的 require 是一个用于模块加载的函数,其作用是:

  • 加载并运行指定的 Lua 模块(文件)
  • 只加载一次:同一个模块在整个运行期间只会被加载和执行一次(除非显式清除缓存)。
  • 通常用来组织代码、拆分功能模块。
-- 前提:我们有my_module.lua文件
local myModule = require("my_module")
myModule.functionInModule()

前置2:package

package.loaded 是一个LUA内置的表,Lua 内部用它来记录所有已加载的模块

print(package.loaded)       --  --> table: 0x...
print(package.path)         --  --> 当前模块搜索路径
print(package.preload)      --  --> 手动注册的模块表

热更新的代码

我们要把新模块的内容,注入进游戏逻辑原本引用的旧模块表中,这样才会在运行中的游戏中生效(不能仅靠require来工作)。


-- 定义热更新函数,参数为模块名
function reloadUp(module_name)
  -- 从全局环境_G中获取旧的模块表
  local old_module = _G[module_name]
  -- 将package.loaded中对应模块标记为未加载,强制重新加载
  package.loaded[module_name] = nil
  -- 重新加载模块,实际上是执行了模块的文件,加载新的内容到内存
  require(module_name)
  -- 从全局环境_G中获取刚加载的新模块表
  local new_module = _G[module_name]
  -- 遍历新模块表中的所有字段
  for k, v in pairs(new_module) do
    -- 将新模块表中的字段复制到旧模块表中,实现更新
    old_module[k] = v
  end
  -- 将更新后的旧模块表重新放回package.loaded,表示模块已被加载(起到标记的作用,避免再调用函数时重复加载)
  package.loaded[module_name] = old_module
end

最终得到的结果的仍是旧的模块表,只是内容已经被热更新。因此,函数中的新模块表只是是通过重新 require(module_name) 加载模块文件生成的一个“临时的新模块表”,而旧表 old_module 是外界(其他模块或代码)已经引用过的模块表,old_module 保存的是对旧模块表的引用,我们要保留这个引用。

require() 是加载动作,for k,v in pairs(new_module) 是迁移动作,package.loaded[...] = old_module 是注册动作

5.C#中,值类型传递为什么会产生GC? XLua的解决办法是什么?

通常来说,只要堆分配了内存,也就是实例化引用对象,在对象使用完时,就会被GC。所以值类型时从栈上分配的,原则上不会产生GC,但是在lua和C#交互过程中,值传递会因为装箱拆箱产生GC。

以下面的MyStruct为例:

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

以下代码展示了[GCOptimize]如何通过生成特化代码,绕过.NET装箱机制,避免装箱:

// XLuaTest.MyStruct生成代码
[MonoPInvokeCallbackAttribute(typeof(LuaCSFunction))]
static int __CreateInstance(RealStatePtr L)
{
    try
    {
        ObjectTranslator translator = ObjectTranslatorPool.Instance.Find(L);
        if (LuaAPI.lua_gettop(L) == 3 && LuaTypes.LUA_TNUMBER == LuaAPI.lua_type(L, 2) && LuaTypes.LUA_TNUMBER == LuaAPI.lua_type(L, 3))
        {
            int _p1 = LuaAPI.xlua_tointeger(L, 2);
            int _p2 = LuaAPI.xlua_tointeger(L, 3);

            var gen_ret = new XLuaTest.MyStruct(_p1, _p2);
            // 无[GCOptimize]标签
            // translator.Push(L, gen_ret);
            // 有[GCOptimize]标签
            translator.PushXLuaTestMyStruct(L, gen_ret);

            return 1;
        }

        if (LuaAPI.lua_gettop(L) == 1)
        {
            // 无[GCOptimize]标签
            // translator.Push(L, default(XLuaTest.MyStruct));
            // 有[GCOptimize]标签
            translator.PushXLuaTestMyStruct(L, default(XLuaTest.MyStruct));
            return 1;
        }
    }
    catch (System.Exception gen_e)
    {
        return LuaAPI.luaL_error(L, "c# exception:" + gen_e);   // 捕获C#异常,转换成LUA异常
    }
    return LuaAPI.luaL_error(L, "invalid arguments to XLuaTest.MyStruct constructor!");  // 参数不匹配,报错
}

(1. 结构体构造的Lua绑定机制

  • 通过[MonoPInvokeCallback]标记将C#方法注册为Lua可调用的函数
  • 函数名__CreateInstance表明这是Lua中调用XLuaTest.MyStruct.new()时触发的构造函数

(2. 参数处理逻辑

  • 带参数的构造
    当检测到3个参数(含隐含的元表参数)且后两个为number类型时:
    • new XLuaTest.MyStruct(LuaAPI.xlua_tointeger(L, 2), LuaAPI.xlua_tointeger(L, 3))
  • 默认构造
    当只有1个参数(仅元表)时创建默认结构体:
    • default(XLuaTest.MyStruct)

(3. [GCOptimize]标签的核心作用

  • 存在标签时:使用特化的值类型传递方法
    • translator.PushXLuaTestMyStruct(L, gen_ret); // 避免装箱
  • 无标签时:使用通用object传递
    • translator.Push(L, gen_ret); // 会导致结构体装箱

这验证了[GCOptimize]的核心价值:消除值类型在Lua-C#交互时的装箱开销

6.什么是弱引用?

此作者没有提供个人介绍。
最后更新于 2025-05-28