在线时间:8:00-16:00
迪恩网络APP
随时随地掌握行业动态
扫描二维码
关注迪恩网络微信公众号
飞书文档:https://idreamsky.feishu.cn/docs/doccnjZ7tfpP5AFnSWGnlaUDm1h 一、需要注意的数据类型 1. 表table Lua 实现表的算法颇为巧妙。每个表包含两部分:数组(array)部分和哈希(hash)部分,数组部分保存的项(entry)以整数为键(key),从 1 到某个特定的 n,所有其他的项(包括整数键超出范围的)则保存在哈希部分。 哈希部分使用哈希算法来保存和查找键值。它使用的是开放寻址(open address)的表,意味着所有的项都直接存在哈希数组里。键值的主索引由哈希函数给出;如果发生冲突(两个键值哈希到相同的位置),这些键值就串成一个链表,链表的每个元素占用数组的一项。 当 Lua 想在表中插入一个新的键值而哈希数组已满时,Lua 会做一次重新哈希(rehash)。重新哈希的第一步是决定新的数组部分和哈希部分的大小。所以 Lua 遍历所有的项,并加以计数和分类,然后取一个使数组部分用量过半的最大的 2 的指数值,作为数组部分的大小。而哈希部分的大小则是一个容得下剩余项(即那些不适合放在数组部分的项)的最小的 2 的指数值。重新hash的性能消耗还是比较大的。要减少重新hash次数,可以创建大的表格替代多个小的表格或者复用表格。
每次新建一张table,都会产生堆内存,都会导致GC遍历的时候多一个判断节点。因此,Lua的GC优化,重点关注table和c的userdata。 在频繁更新或者使用的代码部分,不要反复申请table,这会使得虚拟机不断的去进行内存分配。
1. 追加一个元素到一个array的结尾的三种写法。其中使用本地计数器的第三种写法性能最好。 追加一个元素到一个array的结尾的三种写法。其中使用本地计数器的第三种写法性能最好。 1. t[#t + 1] = 123 2. talbe.insert(t, 123) 3. local counter = 1 for i = 1, 10000 od t[counter] = i counter = counter + 1 end 配置表优化:见下方。 服务端数据:例如背包中的每个道具数据可以在本地保存一个table,服务端更新时只需要更新对对应道具的table数据而不用每次创建新的表。 其它临时数据:减少在定时器(帧、秒)或update方法里开辟新的空间(引用全局变量、obj.transform等.操作、创建新的表),可以在循环开始前定义一个local变量做缓存。 2. 字符串string 因为Lua的String是内部复用的,当我们创建字符串的时候,Lua首先会检查内部是否已经有相同的字符串了,如果有直接返回一个引用,如果没有才创建。这使得Lua中String的比较和赋值非常地快速,因为只要比较引用是否相等、或者直接赋值引用就可以了。
连接方式:多个字符串连接时使用table.concat代替..的字符串连接。table.concat只会创建一块buffer,然后在此拼接所有的字符串,实际上是在用table模拟buffer。而..则每次拼接都会产生一串新的字符串,开辟一块新的buffer。聊天需要更注重这块内容。 3. 结构体如Vector3 为什么结构体单独说呢,因为结构体会带来很严重的性能问题,具体原因可以参考:https://www.jianshu.com/p/07dc38e85923以及https://www.gameres.com/700911.html 简而言之,就是是boxing(装箱)和unboxing(拆箱)。Vector3(栈)转为object类型需要boxing(堆内存中),object转回Vector3需要unboxing,使用后释放该object引用,这个堆内存被gc检测到已经没引用,释放该堆内存,产生一个gc内存。
tolua\slua 将Vector3等类型实现为纯lua代码,Vector3就是一个{x,y,z}的table,这样在lua中使用就快了。因为以上结构体,都是table的方式,所以,如果使用频繁的话,就容易产生大量的堆内存,必要的时候还是用对象池复用,例如坐标系统的点坐标。 使用c#原生的vector,建议在c#端进行封装,传值时使用x,y,z进行传递,在c#层包装成vector使用。直接在函数中传递三个float,要比传递Vector3要更快。 例如void SetPos(GameObject obj, Vector3pos)改为void SetPos(GameObject obj, float x, floaty, float z) 二、lua测优化 参考:https://www.lua.org/gems/sample.pdf 1.首先,我们需要了解类的实现,如下,核心的代码是setmetatable(cls, {__index = super})这句,访问 cls 中任何不存在的字段时,都会尝试到 super 中查找,这里的 super 就相当于父类,而 cls 则相当于是类 super 的子类。 local function __class(classname, super) local superType = type(super) local cls if superType ~= "function" and superType ~= "table" then superType = nil super = nil end if superType == "function" or (super and super.__ctype == 1) then -- inherited from native C++ Object else -- inherited from Lua Object if super then cls = {} setmetatable(cls, {__index = super}) cls.super = super else cls = {ctor = function() end} end cls.__cname = classname cls.__ctype = 2 -- lua cls.__index = cls function cls.ToString(self) return self.__cname end function cls.new(...) local instance = setmetatable({}, cls) instance.class = cls instance:ctor(...) return instance end end return cls end 元表:当访问表中不存在的字段时,元表中的 __index 元方法会被调用,并返回该方法返回的值,该值可以是一个函数或者表。注意,这边是访问不了元表内的属性的,而是去获取__index属性的返回值,如果返回值是函数则调用,返回表则在表内查找字段。测试如下,man无法访问Person类的isMan字段,但可以访问__index内的字段name。这就解释了继承为什么是setmetatable(cls, {__index = super}),而不是setmetatable(cls, super)。 local Person = { isMan = true, __index = { name = "jadeshu", age = 28, sex = 0, } } --表 local man = {} --表 setmetatable(man,Person) --设置元表 --man的元表是Person --测试 printWJF(man.name) --显示 jadeshu printWJF(man.isMan,"_",Person.isMan) --显示 nil_true 2.local变量和_G全局变量、self变量 _G:一张表,保存了lua所用的所有全局函数和全局变量,在默认情况,Lua在全局环境_G中添加了标准库比如math、函数比如pairs等。如_G.print("你好")=print("你好")。 全局变量不需要声明,没被 local 修饰的变量都是全局变量。我们应该减少全局变量的定义,可以把一些全局的属性放在一个全局表里,在通过这个表访问。 local:局部变量只在被声明的那个代码块内有效。(代码块:指的是一个控制结构内,一个函数体,或者一个chunk(变量被声明的那个文件或者文本串)),无法通过继承、元表访问,类似于c#的private变量。 需要注意的是:使用function声明的函数为全局函数,在被引用时不会因为声明的顺序而找不到 ,使用local function声明的函数为局部函数,在引用的时候必须要在声明的函数后面。 local和_g的优劣见:http://lua-users.org/wiki/OptimisingUsingLocalVariables 1. Local variables are very fast as they reside in virtual machine registers, and are accessed directly by index. Global variables on the other hand, reside in a lua table and as such are accessed by a hash lookup. 所以尽量使用local变量。local变量包括属性以及方法,一些经常或在循环用到的全局函数,可以申明为local局部变量,这样可以提升效率。例如表插入操作local TINSERT = table.insert。
self:代表当前表(模块),可以理解成c#的this,子类可以访问父类的self属性,不能访问local属性。如下,module作为父类或被require加载出来后,lParam 不能在模块外部访问,他们并不在最后return的module表里。constant和constant1做为module表里的内容可以被外部访问。 self.xxx定义的变量访问速度比local较慢,因为self查找会走元表,如果多重嵌套,效率肯定是比不上local的,但是self变量可以被外部模块访问,一些需要提供给外部的数据比较方便,当然你也可以把local封装一个Get方法。 -- 文件名为 module.lua -- 定义一个名为 module 的模块 module = {} local lParam = "这是一个局部变量" -- 定义一个常量 module.constant = "这是一个公共变量" -- 定义一个函数 function module:func1() self.constant1 = "这也是一个公共变量" end local function func2() print("这是一个私有函数!") end return module 3.配置表 缓存:使用时动态加载。 缓存处理一般有:1.常驻内存,加载后不销毁;2.定时清理,加载后一定时间内未使用则清理,使用则刷新时间;3.一次性,不缓存;4.跟随场景,只在切换场景时清除配置表; 优化:参考https://blog.uwa4d.com/archives/1490.html。核心点是 1.通过工具将excel表转为lua文件,通过table的方式访问表格。 2.提取配置表中大量重复的默认值、表格、数组等作为表的元表,减少重复变量尤其是重复的空表。 3.对配置表中只在客户端、服务端单项使用的字段进行分离,也就是说只有服务端用到的字段不导出到客户端的表格。 4.字符串处理,例如说明字段、标题等配置在多语言的表格里,在使用key值索引到对应的多语言项,多语言配置最好一个语言一张表,当前游戏使用哪个语种就加载哪个配置文件。 最终结构类似于:ARENA下的每一条数据的元表设置成默认值,当在数据里找不到指定key,会在元表(也就是默认值defaultValues)里查找默认值。这边的设置_index实际上相当于设置父类,当前表里查不到对象时,会在_index对象内查找,具体可以看源码里lua class的实现。 local defaultValues = { robotName = "des_3115", } local ARENA = { [1] = { rank = { 1, 1, }, robotGroupId = 5000, }, [2] = { rank = { 2, 2, }, robotGroupId = 4999, }, [3] = { rank = { 3, 3, }, robotGroupId = 4998, }, [4] = { rank = { 4, 4, }, robotGroupId = 4997, }, [5] = { rank = { 5, 5, }, robotGroupId = 4996, }, [6] = { rank = { 6, 6, }, robotGroupId = 4995, }, [7] = { rank = { 7, 7, }, robotGroupId = 4994, }, } do local base = { __index = defaultValues, --基类,默认值存取 __newindex = function() --禁止写入新的键值 error("Attempt to modify read-only table") end } for k, v in pairs(ARENA) do setmetatable(v, base) end base.__metatable = false --不让外面获取到元表,防止被无意修改 end return ARENA 4.不要在for循环中创建表和闭包 local t = {1,2,3,'hi'} for i=1,n do --执行逻辑,但t不更改 ... end 5.建议在场景切换时主动调用一次GC,包括lua、c#的gc方法。 三、与c#的交互优化 1. 交互优化 参考:https://gameinstitute.qq.com/community/detail/125117 gameobj.transform.position = pos调用栈如下:
调用函数:Lua中如果要调用一次C#的函数,至少有几个步骤: 1、在Lua层面,找到C#这个函数的Wrapper的C指针 2、C#层面,进行参数个数,参数类型的验证 3、不同类型的参数校验成本又是不一样的 Number类型,调用LuaDLL.luaL_checknumber 进行一次验证即可 String类型,需要先LuaDLL.lua_type 获取类型,根据不同类型再调用一次LuaDLL的对应tostring接口 Struct类型,如Vector3等,需要调用LuaDLL.tolua_getvec3获取结构体的值,再new一个Vector3 4、返回值处理
优化建议: 尽量减少不需要的交互,能在lua完成的就在lua完成。 lua端减少长串的点号操作,例如child.parent.tranfrom.localposition,建议在c#封装SetParentLocalPosition方法。 lua端减少对结构体(如vector)的直接操作,频繁使用的可以用tolua等封装的组件,非频繁的可以在c#额外封装方法,示例如下。 public static DateTime GetLocalServerTime() { return com.geargames.common.utils.Utils.ServerDateTimeNow().GGToLocalTime(); } public static void SetLocalPositionEx(this Component cmpt, float x, float y, float z) { cmpt.transform.localPosition = new Vector3(x, y, z); } 运行效率测试脚本如下,访问次数为100000次: 例1: 1、local pos = me.Root.transform.position 2、local pos = me.Root:GetLocalPosition() 3、local x,y,z = me.Root:GetLocalPositionEx() 测试结果(单位秒): 0.22617602348328 0.1167140007019 0.052457094192505 例2: local y = me.Root.transform.localPosition.y local y = me.Root:GetLocalPositionY() 测试结果: 0.2229311466217 0.052457094192505 测试代码: public static float GetLocalPositionY(this Component cmpt) { return cmpt.transform.localPosition.y; } public static void GetLocalPositionEx(this Component cmpt, out float x, out float y, out float z) { Transform trans = cmpt.transform; x = trans.localPosition.x; y = trans.localPosition.y; z = trans.localPosition.z; } local pos = Vector3(0,0,0) for i=1,100000 do me.Root:SetLocalPosition(pos) --0.058831930160522 --me.Root:SetLocalPositionEx(0,0,0) -- 0.063822984695435 --me.Root:SetLocalPosition(Vector3(0,0,0)) --0.13435101509094 End lua端减少频繁获取unity组件(如GetComponent\Find等方法),频繁使用的组件建议缓存到本地。建议使用导出工具获取需要操作的对象,而不是find的方法,避免忘记释放导致的泄露问题。 减少在循环里通过.获取c#对象的属性,如果可以,请缓存它们。
封装方法注意点: 1. lua和c#之间传参、返回时,尽可能不要传递以下类型: 2. 严重类: Vector3/Quaternion等unity值类型,数组 3. 次严重类:bool string 各种object 4. 建议传递:int float double 5. 频繁调用的函数,参数的数量要控制,无论是lua的pushint/checkint,还是c到c#的参数传递,参数转换都是最主要的消耗,而且是逐个参数进行的,因此,lua调用c#的性能,除了跟参数类型相关外,也跟参数个数有很大关系。 6. 优先使用static函数导出,减少使用成员方法导出 7. 合理利用out关键字返回复杂的返回值 2. 精简lua导出 网上已经有非常多IL2CPP导致包体积激增的抱怨,而基于lua静态导出后,由于生成了大量的导出代码。这个问题又更加严重。 鉴于目前ios必须使用IL2CPP发布64bit版本,所以这个问题必须要重视,否则不但你的包体积会激增,binary是要加载到内存的,你的内存也会因为大量可能用不上的lua导出而变得吃紧。 移除你不必要的导出,尤其是unityengine的导出。如果只是为了导出整个类的一两个函数或者字段,重新写一个util类来导出这些函数,而不是整个类进行导出。也可以使用[notolua]属性来标记不导出。例如我们只用到Animation的Play方法,不需要整个导出Animation类,只需要导出对应方法,或封装一个方法导出。 如果有把握,可以修改自动导出的实现,自动或者手动过滤掉不必要导出的东西。 3. 引用移除 两端保存的引用及时清除,例如lua持有的c#数据结构。 c# object返回给lua,是通过dictionary将lua的userdata和c# object关联起来,只要lua中的userdata没回收,c# object也就会被这个dictionary拿着引用,导致无法回收。 最常见的就是gameobject和component,如果lua里头引用了他们,即使你进行了Destroy,也会发现他们还残留在mono堆里。 不过,因为这个dictionary是lua跟c#的唯一关联,所以要发现这个问题也并不难,遍历一下这个dictionary就很容易发现。ulua下这个dictionary在ObjectTranslator类、slua则在ObjectCache类。 四、内存优化工具 Lua 提供了以下函数collectgarbage ([opt [, arg]])用来控制自动内存管理:
如何监测Lua的编程产生内存泄露: 1. 针对会产生泄露的函数,先调用collectgarbage("collect")和collectgarbage("count"),取得最初的内存使用情况。 2. 函数调用后, collectgarbage("collect")进行收集, 并使用collectgarbage("count")再取得当前内存, 最后记录两次的使用差。
可以保存函数调用前后的_G到本地文件,然后使用软件比较前后两次的_G的内容差,可以获取到泄漏的具体内容。文件差异对比软件:https://blog.csdn.net/liuyukuan/article/details/5980591
当然,推荐使用现成的工具lua profile,下载及文档链接:https://github.com/ElPsyCongree/LuaProfiler-For-Unity#zh |
请发表评论