本篇不谈贴图等资源占用内存的情况;
我们项目客户端这块是使用了 tolua 热更方案,基本上所有的业务逻辑都是在 lua 实现,战斗相关的逻辑也是一样在 lua 实现的;
前阵子我们的测试同学在 Windows 上跑游戏测试验收各种功能时,运 1 个小时左右,在操作系统的性能显示表明 Unity 这个进程就占了将近 15 个 G !导致他们必须得 kill 掉进程后重新启动 Unity 后方可继续运行!
这很明显的是存在内存泄漏的问题,毕竟我们做的是手游,内存占用不会达到 pc 游戏那种级别,然后我就开始着手花时间去排查!
后面排查出来的结果倒不是说有真正意义上的泄漏,而是因为 gc 这块太严重,就是请求分配内存的速度快于 gc 的速度,到后面产生了非常多的内存碎片,当要申请一块较大内存时总是发现当前已保留的内存(即使本来没有用的内存总量远大于用户的请求)无法满足需求,只能继续向操作系统去申请,从而导致 Unity 占用的总内存越来越大!
而在排查内存问题时,我所使用的工具主要是 Unity 的 Profiler 以及 github 上借鉴别人的 lua 内存泄漏工具(查 lua 内存泄漏的一个/查 lua 内存分配过快的一个 / lua 内存及 CPU 时间统计一个),具体网址就不贴了,一搜就有!
然后导致这个问题的原因主要有以下几点:
1.C# 中驱动 lua 的一些逻辑脚本中的函数(比如 Update 、 OnRenderImage 等等),这些接口是以模块名拼接函数名的形式通过 tolua 调用过去的,但原来实现的人没考虑字符串拼接会产生 gc 的问题,导致每帧在 C# 都生成了很多新的字符串对象,从而导致了 mono gc,大概是每帧 3~4KB;
2.后处理特效脚本造成了很多的 mono gc ,大概是每帧6KB,因为我们的 MainCamera 上挂了 4、5 个处理后效的脚本
3.lua 中一些业务逻辑的计算使用了客户端的实时时间,这个是从 C# 那边通过 userdata 给过来的,但因为现在 tolua 是通过字符串的形式支持了 64 位整型值,每次使用的时候都会进行 tonumber(tostring(realTime)) 转换操作,从而导致了一旦调用频繁(比如在 Update 中调用),就会产生很多的 lua string(也是 gc 对象)
4.lua 中的战斗逻辑大量的向量计算(比如位置/方向/旋转),举个例子:
1 local function _init() 2 this.curPos = Vector3.New(0, 0, 0), 3 this.moveDirN = Vector3.New(1, 0, 0) 4 end 5 6 --... 7 8 local function _update() 10 local nextPos = this.curPos + this.moveDirN * 3 11 --... 12 end
这时在 _update 中计算 nextPos 的时候,每帧就会多产生两个 Vector3 table 出来——因为 tolua 在 lua 中封装了 Vector3 这个类(主要作用应该是避免频繁地和 C# 进行交互,就是多花了一点内存,节省下客观的 cpu 执行时间),重载数学运算符的实现,一次计算+-*/运算就会多一个 new Vector3,不过这也是合理的,本来实现上就该是这样,不然用户会产生疑惑的!
5.在事件(比如进入场景事件/主角升级事件等等)分发这块,原来的人使用了一个开源的事件库,其中用了 lua 协程来处理事件派发:每派发一个事件给不同监听者,都会创建单独的协程去处理,但处于战斗过程中,会有各种各样不同但事件需要派发,也就是说这个量还是挺大但,具体 gc 数值大概是每帧 1KB ,但按我们项目现有情况来说,派发事件这一操作没有异步的需求,都是需要所有监听者直接执行即可,也没有说某个事件的处理时间已经达到了不能容忍的程度,所以暂时是没有异步这一个需求,后续如果有再把协程加回来,并且封装多一层,也就是进行协程复用,不会频繁产生新的协程;
6.其他的也是跟上述第 4 点有相似之处,都是在调用频率非常频繁的函数不断的新创建 lua table,这块的量大概是有 8KB;
7.这一点我还没有细测,但猜测是有可能的,就是网络消息的收发这块,有些很频繁的消息,比如移动、技能等等,每次都是一个的 new,后续这块也可能是一个热点;
8.同 7 一样,处理视野范围内的所有实体的移动、技能的这些逻辑也有可能会导致比较多的 lua gc,后面再确认看看;
确认了上述几个大头原因之后,就用了相应的优化方案进行优化,这个就不细说了,因为原因都确认了,那么方案基本就是确认的:思路就是一个,怎么避免或者说最大程度减少 gc!现阶段我们游戏在 pc 上的内存占用基本是比较稳定的,持续战斗两个小时,多分配出来的内存占用基本在几十 M 这个级别,后续再在手机上进行进一步的确认测试!
写在最后:打开 Unity Profiler 在运行过程中,其本身也是占用 mono 内存的,所以这块得注意,别被误导了,比如说在 Memory Profiler 中会它自身产生的 gc 曲线,这块通过开关就可以开启/关闭它在 CPU Profiler 中的显示!
2020-08-21
本篇完,转载请注明出处——枫叶未红