关于在lua中读取excel表的优化

最近在做项目的优化时发现,我们项目中有个锻造的UI界面打开特别慢而且使用内存分析工具发现每次打开这个界面都会导致内存暴涨,在手动GC后内存被回收,可确定是临时内存暴涨,最后发现这个界面每次打开都会去读取一个很大的表数据,造成临时内存的上涨。

如果在lua中去读取一个很大的表,比如策划配置的excel表,假如这个表有4000多行,而且数据项比较多,那么会导致读取时间变长,而且会使临时内存暴涨

就算这个表已经被加载到lua内存中,当你再次去读取这张表并且把它保存在局部变量时也会使临时内存暴涨 ,因为这个是值数据需要开辟临时内存空间

所以在读取表数据时应该避免去读取整张表的数据,而是根据索引去读取单个数据或者是某些数据。如果表结构不满足条件就应该和策划商量把表拆开来配置,这样才不会导致临时内存暴涨。

实在不行也不要每次打开界面都去读取这么大的表,应该在首次打开的时候把这个表数据保存起来,下次打开时避免再次读取。

在lua中使用os.time的时区问题

最近做项目发现在Lua中使用os.time把服务器时间转换为时间戳时,如果修改小时的话会导致在U3D中打出的包如安卓、PC上时区和服务器时区对不上。

比如服务器时间是2020年3月22日12时30分,服务器使用的是中国北京时区, serverTime 传到客户端后,假如客户端时区调整为韩国首尔的时区,如果使用lua的 local a =os.date(“!*t”, serverTime) 转换为世界日期,然后再使用a.hour = 5,修改小时后,再使用 os.time(a) 转换为时间戳,此时的时间戳在安卓包和PC包上将不会是 世界时间的时间戳,而是本地时间的时间戳,因为你中间修改过小时数,但是在U3D编辑器中运行却是正常的。如果你中间没有修改过小时数,那么转换是正常的,这是一个很诡异的问题。

XLua中数字签名的使用

数字签名是一种非对称加密算法,首先生成一对密钥,一个公钥,一个私钥。假如A要给B发送数据,那么A首先用自己生成的密钥对里的私钥(这个 私钥只有A自己知道)对数据进行加密(相当于对数据进行签名),然后发送给B,B收到数据后使用A的公钥验证签名的真实性。

使用数字签名主要有以下的作用:

(1)防冒充(伪造),因为A的私钥只有A自己知道,没有A私钥加密的数据用A的公钥验证必然不通过。

(2)可鉴别身份,如果是经过A签名的数据,那么一定可以用A的公钥验证。

(3)防篡改(防破坏信息的完整性),如果数据修改了,用A的公钥验证不通过。

(4)防重放,如果数据有系列号那么可以防止重新发送数据。

(5)防抵赖。可以防止A不承认自己的数据,因为A的公钥验证了A的数据。

(6)机密性(保密性)。 没有A的公钥无法查看数据。

在XLua中用Tools/KeyPairsGen.exe生成公私钥对,key_ras文件保存的是私钥,key_ras.pub保存的是公钥。

用Tools/FilesSignature.exe对源代码进行签名。

签名后使用AddLoader方式加载签名后的lua文件:

  luaenv.AddLoader(new SignatureLoader("公钥", (ref string filepath) =>
            {
                filepath = Application.dataPath + "test.lua";
               
                if (File.Exists(filepath))
                {
                    return File.ReadAllBytes(filepath);
                }
                else
                {
                    return null;
                }
            }));

在SignatureLoader函数中传入生成的公钥和自己定义CustomLoader即可对签名后lua文件进行验证,SignatureLoader 函数中是具体的验证方式。

lua源码文件功能分析

从官网下载到lua5.2的源代码后 ,展开压缩包,源代码文件全部放在 src子目录下。这些文件根据功能的不同,可以分为大模块。

第一部分: 虚拟机运转的核心功能
lapi.c C语言接口
lctype.c C标准库中 ctype 相关实现
ldebug.c Debug 接口
ldo.c 函数调用以及栈管理
lfunc.c 函数原型及闭包管理
lgc.c 垃圾回收
lmem.c 内存管理接口
lobject.c 对象操作的一些函数
lopcodes.c 虚拟机的字节码定义
lstate.c 全局状态机 lstring.c 字符串池
ltable.c 表类型的相关操作
ltm.c 元方法
lvm.c 虚拟机
lzio.c 输入流接口

第二部分:源代码解析以及预编译字节码 lcode.c 代码生成器
ldump.c 序列化预编译的 Lua字节码
llex.c 词法分析器
lparser.c 解析器
lundump.c 还原预编译的字节码

第三部分:内嵌库 lauxlib.c 库编写用到的辅助函数库
lbaselib.c 基础库
lbitlib.c 位操作库
lcorolib.c 协程库
ldblib.c Debug 库
linit.c 内嵌库的初始化
liolib.c IO 库
lmathlib.c 数学库
loadlib.c 动态扩展库管理
loslib.c OS库
lstrlib.c 字符串库
ltablib.c 表处理库

第四部分:可执行的解析器,字节码编译器 lua.c 解释器
luac.c 字节码编译器

关于XLua与C#之间的通信分析

分析了一下XLua与C#之间的通信方式,发现和SLua,Ulua的区别不是很大。

Lua调用C#:

都是需要先生成一个个wrap文件,C#才能被lua调用。

wrap文件相当于一个接口,Lua先调用 wrap文件 然后 wrap 再调用C#,在 wrap 文件里面实际上是把C#的类函数,字段压入到lua虚拟机的虚拟栈上,再由lua虚拟机出栈后给lua调用的。

当索引系统API、dll库或者第三方库时,无法将代码的具体实现进行代码生成,采用C#的反射方式实现交互,缺点是执行效率低。

也就是说Lua调用C#其实就是:lua->wrap->C#

那么在XLua中C#又是如何调用Lua的呢?

看源码很容易知道,其实是使用如下函数:

 LuaEnv luaenv = new LuaEnv();//创建Lua虚拟机
 luaenv.DoString("CS.UnityEngine.Debug.Log('hello world')");执行lua代码

根据XLua的文档,DoString可以直接执行字符串代码,也可以加载lua文件执行,

分析源码发现DoString其实是调用的外部DLL中的xluaL_loadbuffer函数,如下 DoString 定义:

public object[] DoString(byte[] chunk, string chunkName = "chunk", LuaTable env = null)
        {
#if THREAD_SAFE || HOTFIX_ENABLE
            lock (luaEnvLock)
            {
#endif
                var _L = L;
                int oldTop = LuaAPI.lua_gettop(_L);
                int errFunc = LuaAPI.load_error_func(_L, errorFuncRef);
                if (LuaAPI.xluaL_loadbuffer(_L, chunk, chunk.Length, chunkName) == 0)
                {
                    if (env != null)
                    {
                        env.push(_L);
                        LuaAPI.lua_setfenv(_L, -2);
                    }

                    if (LuaAPI.lua_pcall(_L, 0, -1, errFunc) == 0)
                    {
                        LuaAPI.lua_remove(_L, errFunc);
                        return translator.popValues(_L, oldTop);
                    }
                    else
                        ThrowExceptionFromError(oldTop);
                }
                else
                    ThrowExceptionFromError(oldTop);

                return null;
#if THREAD_SAFE || HOTFIX_ENABLE
            }

而 xluaL_loadbuffer 外部引入声明如下:

 [DllImport(LUADLL, CallingConvention = CallingConvention.Cdecl)]
 public static extern int xluaL_loadbuffer(IntPtr L, byte[] buff, int size, string name);

也就说xluaL_loadbuffer的函数实现并不在源文件中,而是在外部DLL文件中实现的。

继续查找发现它其实是xlua.dll里面的函数,在 xlua.dll 源码xlua.c文件中发现如下定义:

LUALIB_API int xluaL_loadbuffer (lua_State *L, const char *buff, int size,
                                const char *name) {
	return luaL_loadbuffer(L, buff, size, name);
}

根据文件后缀,其实它就是C代码,而且调用的luaL_loadbuffer其实就是lua源码里面的函数。Xlua只是把它封装了一下而已。

继续查找Lua源码,分析Lua.5.3.3的源码发现xluaL_loadbuffer 其实是调用了 lua_load 函数,而 lua_load 实现如下:

LUA_API int lua_load (lua_State *L, lua_Reader reader, void *data,
                      const char *chunkname, const char *mode) {
  ZIO z;
  int status;
  lua_lock(L);
  if (!chunkname) chunkname = "?";
  luaZ_init(L, &z, reader, data);
  status = luaD_protectedparser(L, &z, chunkname, mode);
  if (status == LUA_OK) {  /* no errors? */
    LClosure *f = clLvalue(L->top - 1);  /* get newly created function */
    if (f->nupvalues >= 1) {  /* does it have an upvalue? */
      /* get global table from registry */
      Table *reg = hvalue(&G(L)->l_registry);
      const TValue *gt = luaH_getint(reg, LUA_RIDX_GLOBALS);
      /* set global table as 1st upvalue of 'f' (may be LUA_ENV) */
      setobj(L, f->upvals[0]->v, gt);
      luaC_upvalbarrier(L, f->upvals[0]);
    }
  }
  lua_unlock(L);
  return status;
}

上述代码中调用了luaD_protectedparser来进行parse过程, 在luaD_protectedparser中又调用了f_parser ,在f_parser中根据一些选择来分别处理不同的情况,这就是lua的词法语法语义分析过程。

从上述分析发现,其实C#调用lua,就是C#先调用C代码,然后C调用lua的过程,因为Lua的源码是C写的,lua的代码需要Lua虚拟机解释执行,也就是需要C代码来解析执行。



Lua实现ECS简单框架

 ECS模式全称就是Entity-Component-System模式.

– Entity是实例,作为承载组件的载体,也是框架中维护对象的实体. 
– Component只包含数据,具备这个组件便具有这个功能. 
– System作为逻辑维护,维护对应的组件执行相关操作.

面向对象:把共同的东西抽象出来然后写在基类里面,然后不断的继承它们.

面向组件:把共同的东西抽象成一个组件,然后组合成一个实体.

虽然ECS的开发思想很早就提出来了,但是我发现没有Lua版本的一个ECS框架,其他语言版本倒是挺多的,今天在github上看到触控的 quick-cocos2d-x 的源码发现了GameObject.lua,Registry.lua,Component.lua.  

这3个文件,觉得这就是ECS的思想啊! GameObject.lua 代表了 实体也就是Entity,Component.lua代表了Component,

Registry.lua 相当于一个注册类,缓存了所有的Component.而里面的具体业务逻辑代码构成了System.

只需要把新的组件继承自Component类,实体继承自GameObject类,然后用实体的addComponent 方法添加需要的组件即可.而Component里

的 exportMethods_ 函数是把Component的里的方法导出到实体中,可以在实体对象中直接访问组件里的方法,

这是一种非常巧妙的设计,我觉的这3个文件完全可以抽出来放当我们的项目中去.


Github地址:

https://github.com/chukong/quick-cocos2d-x

rawget和rawset的作用

在Lua中有时候会遇到rawget  和 rawset 这2个函数,实际这2个函数的意思就是对表的访问和赋值 不使用元表的元方法.如下代码:

Class = {}
Class.mt = {}

function Class.New(o)
	local instance = o or {}
	setmetatable(instance,Class.mt)
	return instance
end

Class.mt.__index = function(t, key)
	return "huangyi.cc"
end

Class.mt.__newindex = function(t, key , value)
	if key == "fgreen" then
		rawset(t, key, value) --原始操作不使用元表方法
		--t.fgreen = value    --会使用元表__newindex方法
	end
end

w = Class.New({})
print(rawget(w,"fgreen")) --第一次打印输出
print(w.fgreen)
w.fgreen = "nVal"
print(w.fgreen)

输出结果:

image.png

可以看到第一次打印输出是nil 说明使用rawget获取表的值时没有去访问元表的__index 方法,而是直接访问了表w ,所以输出了nil.

而第二次打印的时候没有使用rawget方法获取表的值 则调用的了元表的__index方法去获取表的值输出了元方法的返回值.

当直接对w.fgreen赋值时,使用了元方法__newindex,元方法里面又使用了rawset 设置表的值,此时不会再去调用元方法__newindex了,所以设置成功了.

如果将rawset(t, key, value)换成t.fgreen = value的话将会造成死循环,因为t.fgreen = value的直接赋值会继续去调用元方法__newindex,造成堆栈溢出.

KSFramework

新公司,新项目,新的框架,这又将是一个挑战。

现在我们项目用的是 KSFramework+Xlua  ,KSFramework是一个Unity 5 Asset Bundle开发框架和工具集,专注于运行时热重载,本身是使用了SLua作为脚本引擎。但是我们项目中给它改成了XLua作为脚本引擎,因为xlua效率方面比slua似乎更高一点,而且在KSframework中把slua换成xlua也是非常方便的,即使换成ulua也是可以的。

        但是最近发现的是 在xlua里面只要出错 在手机端里游戏就会出现闪退,打出的PC包也是直接崩溃掉,之前用tolua的时候一般lua代码报错是不会导致游戏直接崩溃的。

这一点确实很蛋疼。