Lua

热更新的原理

为什么使用Lua作为热更新语言,不用C#

​ 热更新本身对于资源热更新是非常容易的,Unity自带的AB包就可以轻松解决,难的是代码热更新,因为Unity中的C#是编译型语言,Unity在打包后,会将C#编译成一种中间代码,再由Mono虚拟机编译成汇编代码供各个平台执行,它打包以后就变成了二进制了,会跟着程序同时启动,就无法进行任何修改了。

​ LUA是解释型语言,并不需要事先编译成块,而是运行时动态解释执行的。这样LUA就和普通的游戏资源如图片,文本没有区别,因此可以在运行时直接从WEB服务器上下载到持久化目录并被其它LUA文件调用。

不用C#热更的原因

​ 准确的说,C#在安卓上可以实现热更新,但在苹果上却不能。

​ 那C#为什么不做成解释型语言呢?因为C#的定位是一个追求效率且功能强大的编译型语言。在安卓上可以通过C#的语言特性-反射机制实现动态代码加载从而实现热更新。

​ 具体做法是:将需要频繁更改的逻辑部分独立出来做成DLL,在主模块调用这些DLL,主模块代码是不修改的,只有作为业务(逻辑)模块的DLL部分需要修改。游戏运行时通过反射机制加载这些DLL就实现了热更新。

​ 但苹果对反射机制有限制,不能实现这样的热更。为什么限制反射机制?安全起见,不能给程序太强的能力,因为反射机制实在太过强大,会给系统带来安全隐患。

谁偷了我的热更新?Mono,JIT,iOS - 慕容小匹夫 - 博客园 (cnblogs.com)

Lua数据结构

Lua表格(Table)

Table的简单组成:
1.哈希表 用来存储Key-Value 键值对,当哈希表上有冲突的时候,会通过链表的方式组织冲突元素
2.数组 用来存储 数据(包括数字,表等)

  • 数组部分:从1开始作为整数数字索引,这种设计使得数组能够提供紧凑且高效的随机访问。数组的存储位置位于 TValue *array 中,而数组的长度信息则存储在 int sizearray 中。

  • 哈希表:存储在 Node *node,哈希表的大小用 lu_byte lsizenode 表示,lsizenode表示的是2的几次幂,而不是实际大小,因为哈希表的大小一定是 2 的整数次幂。哈希冲突后,采取开放定址法,应对 hash 碰撞

每个 Table 结构最多由三块连续内存构成:

  • 一个 table 结构本身
  • 一块存放了连续整数索引的数组
  • 以及一块大小为2的整数次幂的哈希表

在 Lua 中,table 会将部分整形 key 作为下标放在数组中,而其余的整形 key 和其他类型的 key 则都放在 hash 表中。

table中的hash表的实现结合了以上两种方法的一些特性:

  • table 中的 hash 表实现结合了链地址法(拉链法)和开放定址法的特性。
  • 它的查找和插入操作的复杂度与链地址法相当,而内存开销则近似于开放定址法。

语法相关:

  • table 是 Lua 的一种数据结构,用于帮助我们创建不同的数据类型,如:数组、字典等;
  • table 是一个关联型数组,你可以用任意类型的值来作数组的索引,但这个值不能是 nil,所有索引值都需要用 “[“和”]” 括起来;如果是字符串,还可以去掉引号和中括号; 即如果没有[]括起,则认为是字符串索引,Lua table 是不固定大小的,你可以根据自己需要进行扩容;
  • table 的默认初始索引一般以 1 开始,如果不写索引,则索引就会被认为是数字,并按顺序自动从1往后编;
  • table 的变量只是一个地址引用,对 table 的操作不会产生数据影响;
  • table 不会固定长度大小,有新数据插入时长度会自动增长;
  • table 里保存数据可以是任何类型,包括function和table;
  • table所有元素之间,总是用逗号 “,” 隔开;

【Lua 5.3源码】table实现分析_lua解析table-CSDN博客

Lua元表 (Metatable)

  • 什么是元表

在Lua table中我们可以访问对应的key来得到value值,但是却无法对两个table进行操作。因此Lua 提供了元表(Metatable),允许我们改变table的行为,每个行为关联了对应的元方法。通俗来说,元表就像是一个“操作指南”,里面包含了一系列操作的解决方案,例如 _index方法就是定义了这个表在索引失败的情况下该怎么办,**_add方法就是告诉table在相加的时候应该怎么做。这里面的_index_add就是元方法**。

  • 有两个很重要的函数来处理元表:

**setmetatable(table,metatable):**对指定table设置元表(metatable),如果元表(metatable)中存在__metatable键值,setmetatable会失败 。

**getmetatable(table):**返回对象的元表(metatable)。

  • 什么是元方法

很多人对Lua中的元表和元方法都会有一个这样的误解:“如果A的元表是B,那么如果访问了一个A中不存在的成员,就会访问查找B中有没有这个成员”。如果说这样去理解的话,就大错特错了,实际上即使将A的元表设置为B,而且B中也确实有这个成员,返回结果仍然会是nil,原因就是B的**_index元方法没有赋值。别忘了我们之前说过的:“元表是一个操作指南”,定义了元表,只是有了操作指南,但不应该在操作指南里面去查找元素,而_index方法则是“操作指南”的“索引失败时该怎么办**。

下面是一些Lua表中可以重新定义的元方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
__add(a, b) --加法
__sub(a, b) --减法
__mul(a, b) --乘法
__div(a, b) --除法
__mod(a, b) --取模
__pow(a, b) --乘幂
__unm(a) --相反数
__concat(a, b) --连接
__len(a) --长度
__eq(a, b) --相等
__lt(a, b) --小于
__le(a, b) --小于等于
__index(a, b) --索引查询
__newindex(a, b, c) --索引更新(PS:不懂的话,后面会有讲)
__call(a, ...) --执行方法调用
__tostring(a) --字符串输出
__metatable --保护元表
  • Lua的表元素查找机制
1
2
3
4
5
6
7
8
9
10
father = {  
prop1=1
}
father.__index = father -- 把father的__index方法指向它本身
son = {
prop2=1
}
setmetatable(son, father) --把son的metatable设置为father
print (son.prop1)
-- 输出为1

假设father.__index = father 这句话不存在的话执行结果为nil,

这正印证了上面所说的,只设置元表是不管用的

  • 在上面的例子中,当访问son.prop1时,son中是没有prop1这个成员的。接着Lua解释器发现son设置了元表:father
  • 需要注意的是:此时Lua并不是直接在fahter中找到名为prop1的成员,而是先调用father的__index方法
  • 如果fahter的**_index方法为nil,则直接返回nil(也就是father.__index = father** 这句话不存在)
  • 但是如果**_index指向了一张表(上面的例子中father的_index**指向了自己本身也就是 father.__index = father
  • 那么就会到**_index**方法所指向的这个表中去查找名为prop1的成员,最终,我们在father表中找到了prop1成员
  • 这里的**_index方法除了可以是一个表,也可以是一个函数,如果是函数的话,_index**方法被调用时会返回该函数的返回值

总结 : Lua查找一个表元素的规则可以归纳为如下几个步骤:

  • Step1:在表自身中查找,如果找到了就返回该元素,如果没找到则执行Step2;
  • Step2:判断该表是否有元表(操作指南),如果没有元表,则直接返回nil,如果有元表则继续执行Step3;
  • Step3:判断元表是否设置了有关索引失败的指南(**_index元方法),如果没有(_index为nil),则直接返回nil;如果有_index方法是一张表,则重复执行Step1->Step2->Step3;如果_index方法**是一个函数,则返回该函数的返回值

【游戏开发】小白学Lua——从Lua查找表元素的过程看元表、元方法 - 马三小伙儿 - 博客园 (cnblogs.com)

_index 与 _newindex的区别

__newindex用于表的更新,__index用于表的查询。

如果访问不存在的数据,由**_index提供最终结果
如果对不存在的数据赋值,由
_newindex**对数据进行赋值

  • _index 元方法可以是一个函数,Lua语言就会以【表】和【不存在键】为参数调用该函数

  • _index元方法也可以是一个表,Lua语言就访问这个元表

  • 对表中不存在的值进行赋值的时候,解释器会查找**_newindex**

  • _newindex 元方法如果是一个表,Lua语言就对这个元表的字段进行赋值

1
2
3
4
5
6
7
8
meta={}
meta.__newindex={}
myTable={}
setmetatable(myTable,meta)
myTable.val=1
print(myTable.val)

-- 输出 nil

这里我myTable.val=1 访问了myTable表中没有的属性val

然后引发了寻找_newinde表

而_newindex指向了空表 { }

则输出nil

  • _rawset与**_rawget**的区别
元方法 描述 用途
_rawget 访问table中的元素时,直接获取元素的值,不经过**_index**元方法。 当不想通过**_index元方法查询值,而是直接获取table中元素的原始值时,使用_rawget**。
_rawset 更新table中的元素时,直接设置新值,不执行**_newindex**元方法。 当不想执行**_newindex元方法,而是直接设置table中元素的新值时,使用_rawset**。
  • 怎么实现一个只读表
1
2
3
4
5
6
7
8
9
10
11
12
13
14
local function readonly(t)  
local newT = {}
local mt = {
__index = t,
__newindex = function()
error("别修改我!我是只读的!")
end
}
setmetatable(newT, mt)
return newT
end

local days = readonly({"星期一", "星期二", "星期日"})
days[2] = "星期三哪去了啊?" -- 这行将会触发错误

遍历:pairs 和 ipairs区别??

  • 自定义索引
1
2
3
4
5
6
a={[0]=1,2,3,[-1]=4,5}
print(a[-1]) --4
print(a[0]) --1
print(a[1]) --2
print(a[2]) --3
print(a[3]) --5

因为底层实现的原因 导致索引就处理一位,其他的按照顺序放入数组中

也就是:

索引 数值 注释
-1 4 自定义索引-1
0 1 自定义索引0
1 2 第一个自定义索引0额外的属性2
2 3 第一个自定义索引0额外的属性3
3 5 第二个自定义索引-1额外的属性5
关键字 简介 详细
pairs 迭代 table,可以遍历表中所有的 key 可以返回 nil pairs会遍历所有key,对于key的类型没有要求,遇到nil时可以跳过,不会影响后面的遍历,既可以遍历数组部分,又能遍历哈希部分。
ipairs 迭代数组,不能返回 nil,如果遇到 nil 则退出 ipairs只会从1开始,步进1,只能遍历数组部分, 中间不是数字的key忽略, 到第一个不连续的数字为止(不含),遍历时只能取key为整数值,遇到nil时终止遍历。
1
2
3
4
5
6
7
8
9
10
11
local t = {[1]=1,2,[3]=3,4,[5]=5,[6]=6}
print('ipairs')
for index, value in ipairs(t) do
print(index.."_"..value)
end
print('pairs')
for key, value in pairs(t) do
print(key.."_"..value)
end
--答案是ipairs [2 4 3] , pairs [2 4 3 6 5] 无序

这边存在一点问题,对于[1]=1,2 这个怎么处理

输出:

1
2
3
4
5
6
7
8
9
10
ipairs
1_2
2_4
3_3
pairs
1_2
3_3
5_5
6_6
2_4

所以当ipairs遍历table时,从键值对索引值[1]开始连续递增,当键值对索引值[ ]断开或遇到nil时退出,所以上面的例子中ipairs遍历出的结果是2,4,3。

而pairs遍历时,会遍历表中的所有键值对,先按照索引值输出数组,在输出其它键值对,且元素是根据哈希算法来排序的,得到的不一定是连续的,所以pairs遍历出的结果是2,4,3,6,5。

  • 对于数字和表混合的内容俩者又有什么区别呢
1
2
3
4
5
6
7
local testTab ={1,2,3,4,5};
-- '纯表'
local testTab1 = {a = 1, b = 2, c =3};
-- '杂表1'
local testTab2 = {"zi",a = 5,b = 10, c = 15,"miao","chumo"};
-- '杂表2'
local testTab3 = {"zi",a = 5,b = 10, c = 15,"miao",nil,"chumo"};

输出结果:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
ipairs testTab 
1
2
3
4
5
pairs testTab
1
2
3
4
5
--------------------------
ipairs testTab1
pairs testTab1
1
3
2
--------------------------
ipairs testTab2
zi
miao
chumo
pairs testTab2
zi
miao
chumo
5
15
10
--------------------------
ipairs testTab3
zi
miao
pairs testTab3
zi
miao
chumo
5
15
10
--------------------------

Lua系列–pairs和ipairs_lua pairs-CSDN博客

Lua类与对象

Lua中 点和冒号区别

点 :无法传递自身,需要显示传递

冒号 :隐式传递自身

类与对象写法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
Object = {}

function Object:new()
local obj = {}
self.__index = self
setmetatable(obj, self)
return obj
end

function Object:subClass(className)
_G[className] = {}
local obj = _G[className]

obj.base = self

self.__index = self

setmetatable(obj, self)

return obj
end

-- 实现方法
funciton ChildClass:DoSomething()
print("Object DoSomething")
end

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
ChildClass =Object:subClass("ChildClass")

ChildClass.x=nil;

ChildClass.vector={}

funciton ChildClass:OnInit()

end

-- 重定义父类的方法,相当于override
funciton ChildClass:DoSomething()
print("ChildClass DoSomething")
self.OnInit()
self.vector:OtherClassFun()
end

封装

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
-- Class.lua

--类:属性,构造函数,成员,属性等等
--类的声明,属性,初始值
class = {x = 0, y = 0}
--设置元表的索引,要模拟类,这一步最重要
class.__index = class --表的元表索引是自己
--构造方法,习惯性命名new()
function class:new(x, y)
local t = {} --初始化t,如果没有这一句,会导致类所建立的对象一旦发生改变,其它对象都会改变
setmetatable(t, class) --设置t的元表为class , 把class设为t的原型
t.x = x --属性值初始化
t.y = y
return t --返回自身
end
--这里定义类的其它方法,self标识是非常重要的核心点,冒号:隐藏了self参数
function class:test()
print(self.x, self.y)
end
function class:plus()
self.x = self.x + 1
self.y = self.y + 1
end

继承

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
-- SubClass.lua

require("Class")
--类的继承
SubClass = {z = 0}
--声明新的属性Z
--两步完成类Class的继承
setmetatable(SubClass, Class)
--设置类型是class
SubClass.__index = SubClass --设置表的索引为自身
--构造方法,习惯性命名new()
function SubClass:new(x, y, z)
local t = {} --初始化对象自身
t = Class:new(x, y) --将对象自身设定为父类,相当于C#里的base
setmetatable(t, SubClass) --将对象自身元表设定为SubClass
t.z = z --新属性赋值
return t
end
--定义一个新的方法
function SubClass:go()
self.x = self.x + 10
end

--重定义父类的方法,相当于override
function SubClass:test()
print(self.x, self.y, self.z)
end



多态

1
2
3
4
5
6
7
8
9
10
11
12
13
-- main.lua

require("Class")
require("SubClass")
local a = Class:new() -- 首先实例化父类,并调用父类方法
a:plus()
a:test()

a=SubClass:new() -- 然后实例化子类对象
a:plus() --重写
a:go() --新增
a:test() --重写

Lua互相调用

C和lua的互相调用

  • 如果我们想要理解Lua语言与其它语言交互的实质,我们首先就要理解Lua堆栈。

  • 简单来说,Lua语言之所以能和C/C++进行交互,主要是因为存在这样一个无处不在的虚拟栈。

  • 栈的特点是先进后出,在Lua语言中,Lua堆栈是一种索引可以是正数或者负数的结构,并规定正数1永远表示栈底,负数-1永远表示栈顶。

  • 换句话说呢,在不知道栈大小的情况下,我们可以通过索引-1取得栈底元素、通过索引1取得栈顶元素。

  • C和Lua之间的差异
    1.Lua有垃圾回收机制,C需要显示释放内存
    2.Lua是动态类型,弱类型语言【运行时确认】,C是静态类型,强类型语言。【编译时确认】

  • C与Lua的通信使用了虚拟栈结构!!!
    以下是简单的虚拟栈概念!
    将2个数据压入虚拟栈

当使用正数索引时,表示从栈底开始,一直到栈顶 ,使用负数索引时表示从栈顶开始,一直到栈底。
通过指定索引来出栈和入栈

  • C#调用Lua

    是依靠C作为中间语言,通过C#调用C,C再调用Lua实现的 而框架中的tolua.dll等也就是借助LuaInterface封装的C语言动态库

使用C++调用Lua时我们可以直接利用C++中的Lua环境来直接Lua脚本,例如我们在外部定义了一个lua脚本文件,我们现在需要使用C++来访问这个脚本该怎么做呢?在这里我们可以使用luaL_loadfile()、luaL_dofile()这两个方法个方法来实现,其区别是前者仅加载脚本文件而后者会在加载的同时调用脚本文件。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
#include <stdlib.h>  
#include <stdio.h>
#include <string.h>
#include "lua.h"
#include "lualib.h"
#include "lauxlib.h"

// 初始化Lua状态机并加载基础库
void init_lua(lua_State* L)
{
luaL_openlibs(L); // 打开所有标准库
// 以下函数是Lua 5.1及之前版本的,但在Lua 5.2及之后版本已被废弃,因为luaL_openlibs已经包括了这些库
// luaopen_base(L); // 打开基础库(已废弃)
// luaopen_table(L); // 打开表库(已废弃)
// luaopen_string(L); // 打开字符串库(已废弃)
// luaopen_math(L); // 打开数学库(已废弃)
}

// C函数:加法
int c_add(lua_State* L)
{
int a = lua_tonumber(L, -2); // 获取栈顶第二个元素(即第一个参数)
int b = lua_tonumber(L, -1); // 获取栈顶元素(即第二个参数)
int c = a + b;
lua_pushnumber(L, c); // 将结果压入栈顶
return 1; // 返回给Lua一个结果
}

// C函数:自增
int c_step(lua_State* L)
{
int a = lua_tonumber(L, -1); // 获取栈顶元素
int c = a + 1;
lua_pushnumber(L, c); // 将结果压入栈顶
return 1; // 返回给Lua一个结果
}

// 注册到Lua的C函数列表
luaL_Reg mylib[] =
{
{"c_add", c_add},
{"c_step", c_step},
{NULL, NULL} // 列表结束标志
};

int main()
{
lua_State *L = lua_open(); // 创建一个新的Lua状态机
init_lua(L); // 初始化Lua状态机并加载基础库

// 加载Lua脚本文件
if (luaL_loadfile(L, "test.lua") != 0) {
printf("加载Lua文件失败\n");
return 0;
}

// 运行加载的Lua脚本
if (lua_pcall(L, 0, 0, 0) != 0) {
printf("运行Lua脚本失败: %s\n", lua_tostring(L, -1));
return 0;
}

// 注册C函数到Lua的"mylib"表中
luaL_register(L, "mylib", mylib);

// C调用Lua函数
lua_getglobal(L, "l_ff"); // 获取全局变量"l_ff"(假设是Lua中定义的函数)
lua_pushnumber(L, 2); // 压入第一个参数
lua_pushnumber(L, 3); // 压入第二个参数
if (lua_pcall(L, 2, 1, 0) != 0) { // 调用Lua函数,传入2个参数,期望返回1个结果
printf("调用Lua函数失败: %s\n", lua_tostring(L, -1));
return 0;
}
int res = lua_tonumber(L, -1); // 从栈顶获取Lua函数返回的结果
lua_pop(L, 1); // 弹出栈顶元素,清理栈

printf("在C中得到的结果: %d\n", res);

lua_close(L); // 关闭Lua状态机

return 0;
}
  • Lua调用C
    调用之前需要注册,将函数地址告知Lua
    LuaFramework的框架中Lua要调用Unity自带的API或者我们自己写的脚本之前要先生成对应的XXXWrap文件,就是如上面例子一样,需要在lua里进行注册。

首先我们在C++中定义一个方法,该方法必须以Lua_State作为参数,返回值类型为int,表示要返回的值的数目。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
-- Lua函数:l_ff  
-- 接受两个参数a和b
function l_ff(a, b)
-- 调用C库中的c_add函数,将a和b相加,结果加1后赋值给局部变量c
local c = mylib.c_add(a, b) + 1
-- 打印变量c的值
print("在Lua中: ", c)

-- 调用C库中的c_step函数,将变量c的值加1后赋值给局部变量d
local d = mylib.c_step(c)
-- 打印变量d的值
print("在Lua中: ", d)

-- 返回变量d的值给调用者
return d
end

这些api的名字很怪异,常常没法从名字知道这个函数是做什么的。

lua_getglobal是从lua脚本里面取一个全局变量放到堆栈上(c和lua之间是通过虚拟的堆栈来互相沟通的)。

lua_pushnumber是把一个数字放到堆栈上。

lua_pcall是从当前堆栈进行函数调用。

lua_tonumber这个是把堆栈中的某个值作为int取出来(因为l_ff有返回值,因此堆栈最顶上就是函数的返回值)

在函数c_add里面,lua_pushnumber才是lua调用的返回值(在lua里面,同样是把把栈最顶上的位置当作返回值)

总结:

  • Lua和C++是通过一个虚拟栈来交互的。
  • C++调用Lua实际上是:由C++先把数据放入栈中,由Lua去栈中取数据,然后返回数据对应的值到栈顶,再由栈顶返回C++。
  • Lua调C++也一样:先编写自己的C模块,然后注册函数到Lua解释器中,然后由Lua去调用这个模块的函数。

C和lua的互相调用_conky lua 互相调用-CSDN博客

Lua和C++交互详细总结 - Bill Yuan - 博客园 (cnblogs.com)

[整理]Unity3D游戏开发之Lua - Ming明、 - 博客园 (cnblogs.com)

C#与Lua交互过程和原理?

Wrap文件:每一个Wrap文件都是对一个C#类的包装。

  • 交互过程

C# Call Lua交互过程
C#文件先调用Lua的解析器底层的dll库(C语言编写),再由DLL文件执行相应的Lua文件

Lua Call C# 交互过程
1.Wrap方式:首先生成C#源文件对应的Wrap文件,Lua文件会调用生成的Wrap文件,再由Wrap文件去调用C#文件。
2.反射方式:当索引系统API、DLL库或者第三方库,如果无法将代码具体实现进行代码生成,可通过反射来获取,执行效率较低。

  • 交互原理

C#与Lua交互原理:虚拟栈!!!
交互通过虚拟栈实现,栈的索引分为正数和负数,如果索引是正数,则1表示栈底,如果索引是负数,则-1表示在栈顶

C# Call Lua交互原理
C#先将数据放入栈中,然后Lua去栈中获取数据,然后返回数据对应的值到栈顶,再由栈顶返回至C#

Lua Call C#交互原理
C#源文件生成Wrap文件、或C#源文件生成C模块,将Wrap文件和C模块注册到Lua的解析器中,最后再由Lua去调用这个模块的函数~

  • 代码文件方面

lua调用C#过程:
lua->wrap->C#
先生成Wrap文件(中间文件/适配文件),wrap文件把字段方法,注册到lua虚拟机中(解释器luajit),然后lua通过wrap就可以调C#了、或者在config文件中添加相应类型也可以

C#调用Lua过程:
C#生成Bridge文件,Bridge调dll文件(dll是用C写的库),先调用lua中dll文件,由dll文件执行lua代码
C#->Bridge->dll->Lua 或 C#->dll->Lua

从内存方面解释:

说白了就是对栈进行操作
C# Call Lua:C#把请求或数据放在栈顶,然后lua从栈顶取出该数据,在lua中做出相应处理(查询,改变),然后把处理结果放回栈顶,最后C#再从栈顶取出lua处理完的数据,完成交互。

Lua与C#的相互调用(xLua)_c# lua-CSDN博客

C#与Lua交互过程及原理_lua和c#如何交互-CSDN博客

Lua热更

大家或许会想,Lua到底可以做什么呢?在《Lua游戏开发》一书中作者已经告诉了我们答案:

1、编辑游戏的用户界面
2、定义、存储和管理基础游戏数据
3、管理实时游戏事件
4、创建和维护开发者友好的游戏存储和载入系统
5、编写游戏的人工智能系统
6、创建功能原型,可以之后用高性能语言移植

lua一大作用就是提供代码热更新

那么怎么实现热更新呢?

  • 导出函数require(mode_name)

  • 查询全局缓存表package.loaded

  • 通过package.searchers查找加载器

  • package.loaded
    存储已经被加载的模块:当require一个mode_name模块得到的结果不为假时,require返回这个存储的值。require从package.loader中获得的值仅仅是对那张表(模块)的引用,改变这个值并不会改变require使用的表(模块)。

  • package.preload
    保存一些特殊模块的加载器:这里面的值仅仅是对那张表(模块)的引用,改变这个值并不会改变require使用的表(模块)。

  • package.searchers
    require查找加载器的表:这个表内的每一项都是一个查找器函数。当加载一个模块时,require按次序调用这些查找器,传入modname作为唯一参数。此方法会返回一个函数(模块的加载器)和一个传给这个加载器的参数。或返回一个描述为什么没有找到这个模块的字符串或者nil。

https://blog.csdn.net/xufeng0991/article/details/52473602

Lua内存分布

Lua深拷贝和浅拷贝

  • 如何实现浅拷贝
    使用 = 运算符进行浅拷贝
    分2种情况

    • 拷贝对象是string、number、bool基本类型。拷贝的过程就是复制黏贴!修改新拷贝出来的对象,不会影响原先对象的值,两者互不干涉
    • 拷贝对象是table表,拷贝出来的对象和原先对象时同一个对象,占用同一个对象,只是一个人两个名字,类似C#引用地址,指向同一个堆里的数据~,两者任意改变都会影响对方.
  • 如何实现深拷贝

    复制对象的基本类型,也复制源对象中的对象

    常常需用对Table表进行深拷贝,赋值一个全新的一模一样的对象,但不是同一个表

    Lua没有实现,封装一个函数,递归拷贝table中所有元素,以及设置metetable元表

    如果key和value都不包含table属性,那么每次在泛型for内调用的Func就直接由if判断返回具体的key和value

    如果有包含多重table属性,那么这段if判断就是用来解开下一层table的,最后层层递归返回。

    核心逻辑:使用递归遍历表中的所有元素。

    • 先看copy方法中的代码,如果这个类型不是表的话,就没有遍历的必要,可以直接作为返回值赋值;
    • 当前传入的变量是表,就新建一个表来存储老表中的数据,下面就是遍历老表,并分别将k,v赋值给新建的这个表,完成赋值后,将老表的元表赋值给新表。
    • 在对k,v进行赋值时,同样要调用copy方法来判断一下是不是表,如果是表就要创建一个新表来接收表中的数据,以此类推并接近无限递归。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
local numTest1=5
local numTest2=numTest1 --使用 == 进行浅拷贝
local numTest2=10 --修改numTest2,不会改变numTest1
print(numTest1)
--答案 5
print(numTest2)
--答案 10

local tab={}
tab["好好学习"]="游戏开发"
tab["热更"]="Xlua"
for key, value in pairs(tab) do
print(key.."对应"..value)
end

local temp = tab
tab["好好学习"]="热更"
tab["热更"]="好好学习"
for key, value in pairs(tab) do
print(key.."对应"..value)
end
--输出答案,tab和temp都发生了改变
--热更对应Xlua
--好好学习对应游戏开发

t={name="asd",hp=100,table1={table={na="aaaaaaaa"}}};

--实现深拷贝的函数
function copy_Table(obj)

function copy(obj)
if type(obj) ~= "table" then --对应代码梳理“1” (代码梳理在下面)
return obj;
end
local newTable={}; --对应代码梳理“2”

for k,v in pairs(obj) do
newTable[copy(k)]=copy(v); --对应代码梳理“3”
end
return setmetatable(newTable,getmetatable(obj));
end

return copy(obj)
end

a=copy_Table(t);

for k,v in pairs(a) do
print(k,v);
end

--1.先看copy方法中的代码,如果这个类型不是表的话,就没有遍历的必要,可以直接作为返回值赋值;
--2.当前传入的变量是表,就新建一个表来存储老表中的数据,下面就是遍历老表,并分别将k,v赋值给新建的这个表,完成赋值后,将老表的元表赋值给新表。
--3.在对k,v进行赋值时,同样要调用copy方法来判断一下是不是表,如果是表就要创建一个新表来接收表中的数据,以此类推并接近无限递归。

Lua:实现和理解深拷贝_lua复制文件代码-CSDN博客

lua闭包

闭包=函数+引用环境
子函数可以使用父函数中的局部变量,这种行为可以理解为闭包!

1、闭包的数据隔离
不同实例上的两个不同闭包,闭包中的upvalue变量各自独立,从而实现数据隔离

2、闭包的数据共享
两个闭包共享一份变量upvalue,引用的是更外部函数的局部变量(即Upvlaue),变量是同一个,引用也指向同一个地方,从而实现对共享数据进行访问和修改。

3、利用闭包实现简单的迭代器
迭代器只是一个生成器,他自己本身不带循环。我们还需要在循环里面去调用它才行。
1)while…do循环,每次调用迭代器都会产生一个新的闭包,闭包内部包括了upvalue(t,i,n),闭包根据上一次的记录,返回下一个元素,实现迭代
2)for…in循环,只会产生一个闭包函数,后面每一次迭代都是使用该闭包函数。内部保存迭代函数、状态常量、控制变量。


闭包:通过调用含有一个内部函数加上该外部函数持有的外部局部变量(upvalue)的外部函数(就是工厂)产生的一个实例函数

闭包组成:外部函数+外部函数创建的upvalue+内部函数(闭包函数)

Lua的闭包详解(终于搞懂了) - 风雨缠舟 - 博客园 (cnblogs.com)

lua的GC算法

1、Lua的GC垃圾回收机制算法
Lua的GC使用了标记清除算法Mark and Sweep

标记:每一次执行GC前,从根节点开始遍历每一个相关节点,进行标记
清除:标记完成后,遍历对象链表,然后对需要执行清除标记的对象,进行清除

使用三色法:白,灰,黑,作为对象的三种状态
新白:可以回收的对象;新创建的对象,初始状态是新白,但不会被清除
旧白:可以回收的对象;lua只会清除旧白,GC后,会更新新白
灰色:等待回收的对象:该对象已被GC访问过,但该对象引用的其它对象还未标记
黑色:不可回收的对象

简单流程:
1.根对象开始标记,将白色对象重置为灰色对象,加入灰色链表
2.如果灰色链表不为空,取出一个对象,重置为黑色,并遍历相关引用的对象,重置为黑色
3.如果灰色链表为空,清除一次灰色链表
4.根据不同类型对象分布回收,类型的存储表
5.判断是否遍历到链表尾
6.判断对象是否为白色
7.将对象重置为白色
8.释放资源

伪代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
// 初始化阶段  
将所有对象颜色设置为白色
创建一个灰色对象列表(List)

while 遍历root节点及其所有子对象:
if 如果对象颜色为白色:
将对象颜色设置为灰色
将对象添加到灰色对象列表的尾部

// 标记阶段
while 当灰色对象列表不为空时:
从灰色对象列表中取出一个对象
将对象颜色设置为黑色

while 遍历该对象的所有引用对象:
if 如果引用对象颜色为白色:
将引用对象颜色设置为灰色
将引用对象添加到灰色对象链表(insert to head)


// 回收阶段
遍历所有对象:
if 如果对象颜色为白色:
该对象没有被引用,执行回收操作(释放内存等)
else:
那么重新塞入到对象链表中,等待下一轮GC

总结

Lua通过借助grey链表,依次利用reallymarkobject对对象进行了颜色的标记,之后通过遍历alloc链表,依次利用sweeplist清除需要回收的对象。

lua的GC原理_lua 那些炒作会触发gc-CSDN博客

Lua设计与实现–GC篇 - 知乎 (zhihu.com)