一、Lua性能优化
- table的频繁查找,尽可能使用local变量,不要在循环中做大量表的查找,例如不要在循环中用全局函数,不要使用math.floor这种,而是将它的值提前存储在局部变量,因为即使是hash表的查找也会比较耗时;
- 频繁的创建新的对象,不要在循环中频繁创建新表,如a = {}这种操作等,要有缓存池的概念;
- 配置表的格式优化,配置表通常在游戏中占比很大,所以配表的优化也很重要;
二、Lua垃圾回收与内存泄漏
垃圾回收
Lua会动态分配其数据结构,这些结构会根据需要增长,并最终缩小或释放;当关闭一个Lua状态时,Lua会显式释放所有的内存。所有Lua中的对象都是垃圾收集的目标,包括table、字符串、函数等;
Lua的内存机制可以适用于大多数应用程序,但是一些特殊的应用程序则需要定制功能;例如,在内存受限环境中运行的程序,或者需要将垃圾收集运行时间减至最小的情况;
Lua不会为重用而缓存内存块,也不会刻意避免内存碎片;研究表明,内存碎片一般是由于不当的分配策略引起的,而非程序行为所致;
垃圾收集器
5.0及以前的版本采用简单的标记并清除的垃圾收集器,而且在GC时,也就是一个垃圾收集周期内会暂停与主程序的交互,每个阶段都由四个阶段组成:标记、整理、清除、收尾;
- 标记,将根集合中的对象标记为活跃,根集合中的对象就是Lua可以直接访问的对象,它们是注册表中的对象和主线程对象;然后可通过根集合对象访问到的对象也标记为活跃,从而使所有可达到对象标记为活跃(这种方式避免了循环应用无法被GC的问题);
- 整理,主要包含两个部分,userdata和弱引用;lua会遍历所有的userdata,找出所有未被标记并具有__gc元方法的userdata,然后将这些userdata标记为活跃,并放入一个单独列表中;lua还会遍历所有弱引用table,并根据弱引用设置删除其中未被标记的key和value;
- 清除,遍历所有对象,如果遍历到的未标记,就收集它,否则就清除标记,为下一个收集周期做准备;
- 收尾,根据整理阶段中生成的userdata列表来调用它们的终结函数,为了简化错误处理;
增量式收集器
5.1开始使用了增量式收集器,这种新的收集器做了与原收集器一样的步骤,但是他运行时不会暂停整个程序的响应;它会和解释器一起工作,每当解释器分配了一些固定量的内存后,收集器就会运行一小步,当收集器工作时,解释器仍然可以改变对一个对象的引用关系,但是为了确保解释器能正常工作,解释器中的有些操作还会检测危险的修改,并纠正所涉及对象的标记。
增量式收集器会以原子的方式来完成某些操作,即某些的操作是无法打断的,Lua仍会在一个原子操作中暂停和主程序的交互;如果一个院子操作需要很长时间才能完成,它就可能会影响程序的计时,主要的原子操作是table的遍历和整理阶段。
原子的table遍历表示收集器在遍历一个table时是不会停止的,只有当一个程序中具有一个极大的table的时候,才会成为一个问题;
原子的整理阶段以为垃圾收集器在一轮运行中收集所有需要清理的userdata,并清理所有弱引用table,只有当程序中具有特别多的userdata,或者弱引用table有特别多的条目时,才会成为一个问题;
内存泄漏
在了解了内存泄漏时,了解到了弱表的概念,弱表就是添加了元表,并在元表中设置了{__mode = "kv"}元方法,然后就将该表标记为弱表,该表的引用为弱引用,在lua进行GC时则不会考虑弱引用;
lua的内存泄漏主要是没有释放掉引用,从而导致无法被GC自动回收内存;

lua引用C#对象导致mono内存泄露
lua需要能通过gc来通知中间层维护的C#对象和lua对象的dictionary表来移除引用,如果lua中的对象没有被gc,那么就会造成C#对象无法释放;

三、Lua热重载
理念
热更新让我们在不中断进程运行的情况下,把要修改的代码放入到进程中,随后就会运行新的代码;lua语言的热更主要得益于lua中的函数是第一类值,替换代码只需要让相应的变量指向新的function即可;
在lua中函数是值类型,table是引用类型,所以更函数要特别注意,如果在其它地方赋值过某个函数,那么热更时也需要修改引用处,当然这是个策略问题(也不是必须要做);
热更新的关键是,找到更新的代码模块和内存中运行的代码模块,比较差异,处理差异;
在热更新之前要确定哪些是需要进行热更新的,哪些是可以忽略的;
实践
Lua的热重载主要是利用了package.loaded表,它记录了所有已经加载的模块,热重载的基本逻辑是加载新的模块,module也是一个表,这其实就是根据新表的内容对旧表的内容进行更新;
进行热重载的时候需要注意的事项有如下几个
- 函数的替换,lua中的函数是一个闭包,它有自己的upvalue,所以函数的替换除了替换函数的逻辑,也要替换函数的upvalue值,保证替换之后的数据准确;这里需要先通过getupvalue拿到所有旧模块的upvalue,并根据name和value放入一个表中,然后遍历新函数的upvalue,将旧表的值使用setupvalue给闭包重新赋值;
- 在函数替换中,有一种类型需要考虑,就是虽然替换了函数的逻辑,但是有一种情况就是如果旧表已经被赋值过怎么办,如何来更新呢,这个时候就需要对相应的变量来重新赋值了;这一部分取决于不同项目的lua框架来自己做,其实综合来看,lua的热重载的原理是相同的,但是具体的实现要依赖各自的项目来定制;
- table的替换,table的替换思路就和table的深拷贝类似,遍历表及所有的元表做处理;
- 是否对全局变量做修改,对全局变量的修改也需要先对比差异再处理差异;
运行时自动检测Lua文件的修改
public delegate void ReloadDelegate(string path);
private static ReloadDelegate ReloadFunction;
static void OnPostprocessAllAssets(string[] importedAssets, string[] deletedAssets, string[] movedAssets, string[] movedFromAssetPaths)
{
// 做运行时的热重载
if (!EditorApplication.isPlaying){
return;
}
List<string> luaPathList = new List<string>();
foreach (string str in importedAssets)
{
Regex luaSuffix = new Regex(".lua$");
if (luaSuffix.IsMatch(str)){
luaPathList.Add(LuaFileOnChanged(str));
}
}
if (luaPathList.Count == 0)
return;
foreach (var file in luaPathList)
{
Reload(file);
}
// AssetDatabase.SaveAssets();
// AssetDatabase.Refresh();
}
private static string LuaFileOnChanged(string filePath)
{
var fullPath = filePath;
var luaFolderName = "Lua";
var requirePath = fullPath.Replace(".lua", "");
var luaScriptIndex = requirePath.IndexOf(luaFolderName) + luaFolderName.Length + 1;
requirePath = requirePath.Substring(luaScriptIndex);
requirePath = requirePath.Replace('/','.');
return requirePath;
}
private static void Reload(string file)
{
if(ReloadFunction == null){
LuaEnv luaEnv = LuaManager.Instance.GetLuaVM();
ReloadFunction = luaEnv.Global.Get<ReloadDelegate>("ReloadLua");
}
ReloadFunction(file);
Debug.Log("Auto Reload:" + file);
}
















