0. 前言

有段时间没有写博客了,主要原因是事情有点多,一件接着一件,没有太多整理总结的机会。游戏开发逐渐进入铺量制作的忙碌阶段,趣味性没那么多,新鲜感也少了,虽然还是有很多可供记录的点,但大多比较琐碎,难成系统,又或者可能暂时没有结果,不便于分享。

这几天花了一些时间在Lua层的内存检查和性能优化与检查方面,对比并尝试集成了一些方案,也踩了一些坑,整理记录在这里,给需要的同学提供参考。

1. ToLua#的编译

之前的博客有提到过,我们使用的是ToLua#作为Unity引擎和Lua之间的桥接工具,本文记录的集成工具都是在C层进行的,因此要编译自己的ToLua#。

ToLua#的源码地址是:https://github.com/topameng/tolua_runtime,编译流程可以参考其wiki文档,不过这部分的过程记录的不太详细,本部分基于wiki文档和自己在Windows以及Mac OS上的编译过程进行一些整理,记录整个过程和遇到的问题如下:

安装msys2-x86_64-20161025.exe工具,Web地址:http://msys2.github.io/。

为msys2安装gcc,由于原始的下载地址我本地下载非常慢而且出错,建议添加国内的镜像地址:

编辑 /etc/pacman.d/mirrorlist.mingw32 ,在文件开头添加:Server = http://mirrors.ustc.edu.cn/msys2/REPOS/MINGW/i686

编辑 /etc/pacman.d/mirrorlist.mingw64 ,在文件开头添加:

Server = http://mirrors.ustc.edu.cn/msys2/REPOS/MINGW/x86_64

编辑 /etc/pacman.d/mirrorlist.msys ,在文件开头添加:

Server = http://mirrors.ustc.edu.cn/msys2/REPOS/MSYS2/$arch

然后执行 pacman -Sy 刷新软件包数据即可。

打开mingw的控制台,输入如下命令进行gcc相关工具的安装:

pacman -S mingw-w64-i686-gcc

pacman -S mingw-w64-x86_64-gcc

pacman -S mingw-w64-i686-make

pacman -S mingw-w64-x86_64-make

pacman -S make

安装完毕之后,执行tolua_runtime下的对应sh文件进行编译。

编译Android版本需要安装Android SDK,下载Android NDK r10e,并配置Android NDK r10e的目录到PATH环境变量中,配置ANDROID_NDK_PATH环境变量。需要注意几个配置:

sh文件里的NDKABI变量,定义了NDK的版本,在msys64\etc\profiles里设置环境变量。

如果你使用的MinGW-w64 Win64 Shell来编译32位版本的时候会报找不到dll的错误:

F:/msys64/mingw64/bin/../lib/gcc/x86_64-w64-mingw32/6.2.0/../../../../x86_64-w64-mingw32/bin/ld.exe: skipping incompatible

F:/msys64/mingw64/bin/../lib/gcc/x86_64-w64-mingw32/6.2.0/../../../../x86_64-w64-mingw32/lib/libm.a when searching for -lm

F:/msys64/mingw64/bin/../lib/gcc/x86_64-w64-mingw32/6.2.0/../../../../x86_64-w64-mingw32/bin/ld.exe: skipping incompatible

F:/msys64/mingw64/bin/../lib/gcc/x86_64-w64-mingw32/6.2.0/../../../../x86_64-w64-mingw32/lib\libm.a when searching for -lm

F:/msys64/mingw64/bin/../lib/gcc/x86_64-w64-mingw32/6.2.0/../../../../x86_64-w64-mingw32/bin/ld.exe: skipping incompatible

F:/msys64/mingw64/bin/../lib/gcc/x86_64-w64-mingw32/6.2.0/../../../../x86_64-w64-mingw32/lib/libm.a when searching for -lm

F:/msys64/mingw64/bin/../lib/gcc/x86_64-w64-mingw32/6.2.0/../../../../x86_64-w64-mingw32/bin/ld.exe: cannot find -lm

我纠结了半天,按照路径检查发现它用的还是64位的库,在msys64下发现有两个exe,一个叫做mingw64.exe,一个叫做mingw32.exe,使用32位的那个来编译对应的32版本就可以正常编译了。

iOS的编译脚本里设置了 ISDKVER=iPhoneOS10.2.sdk,这里要跟随SDK的版本升级进行更新,否则LuaJit就编译不过,报错信息为"string.h"文件找不到。

这样,使用不同的编译脚本就可以编译出对应平台的ToLua.dll文件了,拷贝文件覆盖之前Unity的Plugins目录下对应平台的dll文件即可实现ToLua#的更新。

注意: 在覆盖的时候要关闭对应工程的Unity进程,否则会提示dll被占用无法覆盖。

2. 内存检查工具

Unity引擎中有自己的内存检查工具,但是无法查看集成的Lua部分的内存情况。Lua的内存管理由Lua虚拟机负责,Lua 5.1版本的垃圾回收使用的是双白色标记清除(Mark-sweep)算法,5.2版本引入了分代的策略,具体的实现原理可以参考Lua的源代码。从根本上说,由于有垃圾回收功能的存在,即使存在循环引用的情况,也可以在GC的过程中对不再使用的内存进行释放,不存在严格意义上的“内存泄露”,然而,在游戏运行过程中,无论是C#层的频繁GC还是Lua层的频繁GC,都会导致卡顿的问题,因此要尽量减少内存的无谓分配,从而减少GC的执行频率。当然,由于开发过程中存在C#和Lua的互相引用,可能会出现由于释放过程存在问题导致C#和Lua的对象互相引用然后都GC不掉的情况,这个可能产生更加严重的内存问题。因此,我们需要的内存检查工具最少应当可以针对上述这两种情况进行检查。

通常进行内存排查的原理比较相似,大都是基于两份内存快照之间的差异来进行人工的对比和分析,对于Lua 5.1来说,大部分的资源都是在_G这样一个变量,因此一次常见的思路是从这个_G开始来遍历出所有的Lua对象,当然,如果不想遗漏数据,更加好的遍历起始应当是从debug.getregistry()开始。编写的代码不太复杂,逐一处理好metatable等相关的内容即可,我尝试了git上一个在Lua层的工具:lua_memkeak,有一些问题,原因是我们自己在Lua层Hook了_G的访问机制来避免不小心写出的全局变量。(多说几句,在Lua中不声明local的变量都会作为全局变量,或者更严格地说,函数中的变量在不声明local的情况下,会被放在函数的env中,只是默认所有函数的env都是_G,所以才造成了不声明local的变量会被放置在_G中的现象。不经意的全局变量可能会导致意料之外的数据修改从而产生难以排查的bug,同事导致部分内存无法被正确地释放,因此我们项目中Lua的所有全局变量必须由一个函数来进行声明。)

因此我更倾向于找一个C层的实现,云风作为Lua的倡导者,在他的博客中提供了一个Lua内存分析工具:Snapshot,对应的Git地址在这里。集成到ToLua#中的过程也比较简单,把snapshot.c文件拷贝到ToLua_Runtime目录下,修改一下build脚本,将snapshot.c加入到编译代码中。由于原始的snapshot.c文件目标是编译为dll供Lua虚拟机调用,这里为了方便ToLua#使用,修改了一下最后的接口导出:

static const struct luaL_Reg snapshot_funcs[] = {

{ "snapshot", b_snapshot },

{ NULL, NULL }

};

LUALIB_API int luaopen_snapshot(lua_State *L) {

luaL_checkversion(L);

#if LUA_VERSION_NUM < 502

luaL_register(L, "snapshot", snapshot_funcs);

#else

luaL_newlib(L, snapshot_funcs);

#endif

return 1;

}

按照第一步重新编译ToLua#的dll文件,更新之后,添加对应导出的C#接口,然后在Lua代码中仿照例子编写一个初步的内存查看函数:

-- Lua内存记录功能

local preLuaSnapshot = nil

local function snapshotLuaMemory(sender, menu, value)

-- 首先统计Lua内存占用的情况

print("GC前, Lua内存为:", collectgarbage("count"))

-- collectgarbage()

-- print("GC后, Lua内存为:", collectgarbage("count"))

local snapshot = require "snapshot"

local curLuaSnapshot = snapshot.snapshot()

local ret = {}

local count = 0

if preLuaSnapshot ~= nil then

for k,v in pairs(curLuaSnapshot) do

if preLuaSnapshot[k] == nil then

count = count + 1

ret[k] = v

end

end

end

for k, v in pairs(ret) do

print(k)

print(v)

end

print ("Lua snapshot diff object count is " .. count)

preLuaSnapshot = curLuaSnapshot

end

使用方法非常简单,制作了一个按钮,触发上述的函数,点击一次会做一个内存快照记录在preLuaSnapShot中,过一段时间,再点击一次按钮,就会在控制台输出内存的diff情况。我们主要针对两块内容进行了初步检查:

角色在场景内只做移动等简单操作,查看是否有网络、游戏简单的tick逻辑导致的内存分配。这种情况下更多是不进行手动GC,着重检查不必要的内存分配。

进出战斗之后查看前后快照的diff,检查是否有内存泄露的情况。这种情况下会进行一次手动GC,来回收那些战斗中的临时数据,着重检查由于各种引用关系导致无法被释放的内存对象。

我们初步发现了之前代码中的一些问题,包括逻辑代码中可以优化的table创建过程,角色移动过程中不断的回调用的Slot对象创建,ToLua#中协程实现的时候每次wait都会创建一个Timer对象等问题,并逐一进行了修复。

注意:在使用云风这个Snapshot工具的时候,它好用的地方是可以查看到对象的类型、变量名称和文件行数,但是可能由于某些对象引用在ToLua#内部或者C#层,抑或是我们自己编写的Lua Class机制,导致一些条目无法像云风博客中说的看到那么多细致的内容,只能看到变量名称和类型,通过全局搜索来判定对象被引用的位置。时间关系没有去查看源代码进行优化,之后有时间可以再仔细看下,如果有朋友知道如何解决也希望不吝赐教~

3. Profiler的集成

由于我们放置了大量的逻辑在Lua层,因此也需要对Lua的部分进行Profiler来定位可以进行优化的点。由于内存部分使用了云风的Snapshot,因此自然想看看云风的git上是否有Profiler的工具,果然很快找到了——LuaProfiler。结构也很简单,就一个profiler.c文件需要集成,因此很开心地下载下来尝试集成到游戏中,但是编译的时候各种错误。

仔细看了一下代码,原来用到的很多函数都是Lua 5.2和Lua 5.3版本之后才有的函数,尝试翻找snapshot.c中的代码进行一些5.1版本中的实现,花费了半天时间编译通过了但是试用了下会Crash。对于Lua的代码部分不是非常熟悉,因此觉得再在这个地方花费时间可能是个无底洞,因此又想去找找别的方法。

Lua-users上有专门的Profiling Lua Code专题,第一个是LuaProfiler,看了下是支持5.1版本的,但是git上面上次更新是08年的事情了。。。看着有点虚,又搜罗了一圈,其他基于Lua层自己做Profiler的工具感觉对于Lua的运行可能会有比较大的性能影响,因此不太想去尝试。最后还是觉得先试试这个接近10年前的产品。

集成的过程还算顺利,以win64为例,只需要添加如下部分在sh文件中即可:

luaprofiler/stack.c \

luaprofiler/clocks.c \

luaprofiler/function_meter.c \

luaprofiler/core_profiler.c \

luaprofiler/lua50_profiler.c \

编译也较为顺利,但是一旦在游戏中开启之后,ToLua#就会一直报错。对于Lua调用C#的接口,都会报错在这个地方:

public static void CheckArgsCount(IntPtr L, int count)

{

int c = LuaDLL.lua_gettop(L);

if (c != count)

{

throw new LuaException(string.Format("no overload for method takes '{0}' arguments", c));

}

}

添加断点看了下,这里Lua虚拟机的堆栈中的数据c的值比期望的参数个数count大1。利用一个接口查看了下具体的参数类型和数据,前面的都正确,只是最后多一个而已。一开始的想法是LuaProfiler底层的代码为了方便记录数据,在每次函数调用的地方都添加了一个变量来进行数据存储。于是我想只能通过修改ToLua#的生成代码,让之前严格的参数个数必须相等的判断修改为大于等于就通过的判定,这样可以避免误报LuaException,但是仔细思考之后,觉得这样修改太过于麻烦,让ToLua#生成的代码可能不够严谨,于是想从C层看看有没有修改的可能。

其实,无论是云风的方式还是这个LuaProfiler,抑或是其他的基于Lua层的性能检查工具,其根本原理是基于lua_sethook这样一个功能。

lua_sethook

int lua_sethook (lua_State *L, lua_Hook f, int mask, int count);

Sets the debugging hook function.

Argument f is the hook function. mask specifies on which events the hook will be called: it is formed by a bitwise or of the constants LUA_MASKCALL, LUA_MASKRET, LUA_MASKLINE, and LUA_MASKCOUNT. The count argument is only meaningful when the mask includes LUA_MASKCOUNT. For each event, the hook is called as explained below:

The call hook: is called when the interpreter calls a function. The hook is called just after Lua enters the new function, before the function gets its arguments.

The return hook: is called when the interpreter returns from a function. The hook is called just before Lua leaves the function. You have no access to the values to be returned by the function.

The line hook: is called when the interpreter is about to start the execution of a new line of code, or when it jumps back in the code (even to the same line). (This event only happens while Lua is executing a Lua function.)

The count hook: is called after the interpreter executes every count instructions. (This event only happens while Lua is executing a Lua function.)

A hook is disabled by setting mask to zero.

云风的方式是间隔采样的方式,hook LUA_MASKCOUNT,按照一定的间隔进行代码采样,这种方式不太能精确统计每个函数的运行时间,但是对于运行的程序影响较小,从整体消耗百分比的角度分析瓶颈更加准确。

lua_sethook(cL, profiler_hook, LUA_MASKCOUNT, interval);

LuaProfiler的方式是Hook每个函数的调用和Return逻辑,可以拿到每个函数精确的运行时间,但是这个过程中也就增加了运行消耗。这跟量子力学的理论有那么点相似——你想要观察对象,就会对被观察的对象产生影响。LuaProfiler通过暂停计时的方式让统计的时间更加准确,但是运行时的消耗无法减少。

lua_sethook(L, (lua_Hook)callhook, LUA_MASKCALL | LUA_MASKRET, 0);

仔细阅读了一下LuaProfiler的代码,对于一些不太了解的函数也逐一进行了搜索,最后发现其在hook的函数处理中逻辑上并不需要在Lua的栈中添加数据,它用于记录时间消耗的数据在自己组织的一块内存的栈结构中。

最后发现,在callback函数中的lua_gettable操作用来获取profile的状态信息指针,但是把这个数据遗漏在了栈中没有pop出来。我尝试在最后添加了lua_pop (L, 1);操作,编译测试之后没有遇到问题,也解决了ToLua#的报错。

/* called by Lua (via the callhook mechanism) */

static void callhook(lua_State *L, lua_Debug *ar) {

int currentline;

lua_Debug previous_ar;

lprofP_STATE* S;

lua_pushlightuserdata(L, &profstate_id);

lua_gettable(L, LUA_REGISTRYINDEX);

S = (lprofP_STATE*)lua_touserdata(L, -1);

if (lua_getstack(L, 1, &previous_ar) == 0) {

currentline = -1;

} else {

lua_getinfo(L, "l", &previous_ar);

currentline = previous_ar.currentline;

}

lua_getinfo(L, "nS", ar);

if (!ar->event) {

/* entering a function */

lprofP_callhookIN(S, (char *)ar->name,

(char *)ar->source, ar->linedefined,

currentline);

}

else { /* ar->event == "return" */

lprofP_callhookOUT(S);

}

lua_pop (L, 1); /* lua_gettable operation left a value in the lua stack, which makes the tolua param check failed! */

}

我依然有些担心LuaProfiler的作者将这个信息遗漏在栈内是否是有意为之,只是目前这个工具能够正常工作,我就先当作自己fix了一个不过。

这里说一个插曲,在UWA群中我去问了一下LuaProfiler的情况,有个朋友说他们使用SLua+LuaProfiler没有遇到问题,我还专门有去看了下SLua的Warp函数,感觉其对于参数个数的检查和ToLua差别不大,也是基于相等来做的判定。时间关系,我没有去尝试在SLua中集成来进行测试,有使用的朋友可以自己试下,有结论也期望反馈给我。

集成之后的LuaProfiler的使用可以参考Using LuaProfiler的描述,简单来说使用它提供的summary.lua,结合Excel就可以进行比较好的性能分析。使用-v参数可以统计出包括执行次数、平均时长、总时间消耗在内的更多信息。

4. 总结

要在Unity中用好Lua需要注意很多东西,脚本语言本身的性能就比静态语言要差一些,如果写得人不够专业,就可能会造成很多问题,包括内存泄露和性能瓶颈。通过这几个工具的集成,可以让项目组的其他同学方便地进行内存检查和性能测试,越早地抓出问题,就可以让后续编写的代码更好。对于我个人来说,这也是对于Lua进行C扩展的一个入门练习,通过阅读代码和尝试修改bug,了解了一些基本函数的意义和使用方法。

后续有时间,我会按照项目的需求对这两个工具进行一些改造。目前它们在信息输出方面还有一些缺失,LuaProfiler由于在运行时会记录很多数据从而导致严重影响游戏的帧率,最后统计的结果也没有调用关系的内容,届时再在博客中和大家分享。

2017年4月20日于杭州家中

tolua unity 报错_Unity手游开发札记——ToLua#集成内存泄露检查和性能检测工具相关推荐

  1. unity 如何运行demo_Unity手游开发札记——Unity线性空间下移动设备上烘焙变暗问题处理笔记...

    说明:该问题只适用于unity 5.x的版本,在2017+的版本中官方已经修复. 0. 写在之前 其实针对这个问题已经写了一篇很简单的填坑笔记了,但是UWA说希望那篇文章稍微扩充一下放到USpark系 ...

  2. easyui表格编辑事件_Unity手游开发札记——从Odin插件聊基于元数据的编辑器实现

    Metadata is data that provides information about other data. 最近一个多月的时间在全力做新项目的Demo,由于程序暂时还只有我一个人,所以从 ...

  3. Unity手游开发札记——我们是如何使用Lua来开发大型游戏的?(上)

    0. 照旧的碎碎念 转眼间已经三月了,2月份的博客因为过年的懒惰和开年之后的忙碌而没有写--第二个月就打破了去年总结时对于2018年的愿望,真是羞耻呢-- 年后在准备新的测试版本,断断续续做了一些优化 ...

  4. Unity手游开发札记——移动平台的天气系统实现

    0. 牢骚 我发现,每个月的20+号是我有精力写博客的时间-- 这次项目算是经历的第一次严格意义上的渠道测试,更换了正式名称,见了更多玩家,开发组也经历的更多通宵--评价和数据如何暂时还未揭晓,趁着没 ...

  5. tolua unity 报错_Unity3D热更新之LuaFramework篇[01]--从零开始

    解压刚刚下载好的压缩包,发现里面是一个Unity工程(如图2-1),于是用 unity打开此工程. 图2-1 我使用的Unity版本为5.5.5f1,会提示需要升级,是否备份,点"Go He ...

  6. iPhone/iPad高级应用与手游开发学习笔记:多点触摸与手势检测(三:UIPinchGestureRecognizer和UIRotationGestureRecognizer)

    先说一件不幸的事情,本人中午打篮球,不慎脚拐了......悲催啊,愚人节,但是这件事情绝不愚人. 言归正传,上一篇学习了捏合手势,这一篇中我们学习旋转手势,并且使用旋转和捏合做一个操作图片的例子 使用 ...

  7. Unity lua内存泄漏与性能检测

    上周UWA发表了一片博文Lua性能优化-Lua内存优化作者分享了在unity中lua使用的不少干货,文中提到两个lua的小插件,一个是内存检查工具Snapshot,一个是性能分析工具LuaProfil ...

  8. Unity 报错之 ToLua打包:Unable to find tolua DllNotFoundException: tolua

    Unity 报错之 ToLua打包:Unable to find tolua DllNotFoundException: tolua 最近在学习使用LuaFramework框架,使用其打出的安卓包运行 ...

  9. unity手游之聊天SDK集成与使用一

    unity手游之聊天SDK集成与使用一 手游中都有聊天功能,比如公会,私聊,世界聊天,那么找一个好用,功能强大的SDK的可以节省很多精力,帮助我们提高开发速度与游戏质量. 写本篇博文是为了方便使用这个 ...

  10. [视频教程] KBEngine mmo手游开发系列(三) - 角色技能与怪物系统

    KBEngine mmo手游开发系列(三)-角色技能与怪物系统 课程链接:https://edu.51cto.com/sd/21044 本课程为KBEngine mmo手游开发系列的第三个课程,本课程 ...

最新文章

  1. python检查目录是否存在,如果不存在则创建
  2. eclipse maven 项目发布到tomcat 报错 Failed to scan JAR [file:/C:/xxxxx.jar] from WEB-INF/lib
  3. 你每隔多久使用计算机上网查找资料英文,牛津英语8B Unit3导学案
  4. fastai学习:06_multicat Questionnarie
  5. java5 ReadWriteLock用法--读写锁实现
  6. Could not resolve this reference. Could not locate the assembly
  7. 本人原创,如何应用firebug突破新浪ishare下载限制
  8. 移动滑块改变使用容量
  9. matlab lu分解求线性方程组_线性代数10——矩阵的LU分解
  10. 可视化全埋点系列文章之功能介绍篇
  11. 网页游戏的项目设计方案分享
  12. Pr 入门教程:如何更改素材属性?
  13. 测试窗体的FormBorderStyle属性,不同属性所对应的窗体边框显示情况
  14. 计算机的用户终端,计算机终端、客户端、服务端都是什么概念,他们之间的区别是什么?谢谢,大家,小弟是菜鸟...
  15. 越豪华越危险 家装豪华程度与环境污染成正比
  16. unicloud云开发---uniapp云开发(四)---本机手机号一键登录以及第三方登陆
  17. Dijkstra算法 详细讲解
  18. 一文说透安全沙箱技术
  19. Maven命令行窗口指定settings.xml
  20. 第 5 届 FEDAY 前端大会的完整 PPT 内容已出炉-站在大牛的肩膀上学习

热门文章

  1. 遭遇nat.exe,socks.exe,USP10.dll,BOSC.dll,kb080387.CNT,~ctwxw.txt等1
  2. J2EE是什么(二)
  3. 时序报告要看哪些指标
  4. 力扣第39题dfsdfs(respathtarget-candidates[1]i)#调用递归,组成目标的 i 可以重复用,不用i+1,def dfsdfs(resres,pathtarget,ind
  5. am3352 软时钟老是漂移 rx-8025时钟 rx-8025SA时钟
  6. sl400上面安装ubuntu
  7. linux vrrp 配置命令,华为交换机VRRP配置实例收集(转)
  8. xposed+justTrustme使用与分析
  9. LCD液晶拼接屏优势凸显受市场欢迎
  10. 拼接图像亮度均匀调整_华邦瀛微色差液晶拼接屏系统解决方案