一、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语言 内存泄漏_lua语言 内存泄漏


lua引用C#对象导致mono内存泄露


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

lua语言 内存泄漏_弱引用_02

三、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);
    }