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#交互时的装箱开销