热更新的原理

为什么使用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到底可以做什么呢?在《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中的require会阻止多次加载相同的模块。
    • 所以当需要更新系统的时候,要卸载掉响应的模块。
    • (把package.loaded里对应模块名下设置为nil,以保证下次require重新加载)并把全局表中的对应的模块表置nil。
    • 同时把数据记录在专用的全局表下,并用 local 去引用它。
    • 初始化这些数据的时候,首先应该检查他们是否被初始化过了。
    • 这样来保证数据不被更新过程重置。
1
2
3
4
5
6
function reloadup(module_name)
-- 强制清除已加载标记
package.loaded[module_name] = nil
-- 重新加载模块
require(module_name)
end
  • 第二种 正确解法
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
function reloadup(module_name)
-- 获取旧模块引用
local old_module = _G[module_name]

-- 清除加载标记并重新加载
package.loaded[module_name] = nil
require(module_name)

-- 获取新模块内容
local new_module = _G[module_name]

-- 保留旧表引用,更新字段内容
for k, v in pairs(new_module) do
old_module[k] = v -- 用新值覆盖旧表字段
end

-- 还原加载标记
package.loaded[module_name] = old_module
-- 恢复全局表引用
_G[module_name] = old_module
end
  • 为什么第一种不行
  1. 旧引用未更新,局部变量仍指向旧表。
  2. 数据重置导致状态丢失。
  3. 元表未继承的问题。
  4. 内存泄漏,旧表未被正确替换,导致多份实例存在

Lua互相调用

C#与Lua互调过程及性能分析

  • C#调用Lua的过程
  1. C#生成Bridge文件:C#代码生成一个Bridge文件,作为与Lua交互的桥梁。
  2. Bridge调用dll文件:Bridge文件调用由C语言编写的dll文件。
  3. dll文件执行Lua代码:dll文件加载并执行Lua代码。

总结:C# -> Bridge -> dll -> Lua

  • Lua调用C#的过程
  1. 生成Wrap文件:先生成一个Wrap文件(中间文件/配置文件)。
  2. 注册字段和方法:Wrap文件将C#的字段和方法注册到Lua虚拟机中(解释器如LuaJIT)。
  3. **Lua通过Wrap调用C#**:Lua代码通过Wrap文件调用C#的字段和方法。

总结:Lua -> Wrap -> C#

性能分析

Lua调用C#时,性能较慢的原因主要有两点:

  1. 对象获取消耗:Lua调用C#时,需要在ObjectTranslator中获取C#对象,这一步操作有一定的性能消耗。
  2. Userdata类型设置元表:获取对象后,Lua需要将Userdata类型的对象设置元表(Setmetatable),以便在Lua层中访问具体的属性和方法(如localPosition),这一步也有较大的性能消耗。

总结:Lua调用C#的性能瓶颈主要在于对象获取和元表设置的过程。

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去调用这个模块的函数~

从内存方面解释:

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

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

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

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)

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博客

table 补充

1
2
3
4
5
6
7
8
9
10
11
12
13
typedef union Value {
GCObject *gc; /* collectable objects */
void *p; /* light userdata */
int b; /* booleans */
lua_CFunction f; /* light C functions */
lua_Integer i; /* integer numbers */
lua_Number n; /* float numbers */
} Value;

struct lua_TValue {
Value value_;
int tt_;
} TValue;

img

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

只读表

1
2
3
4
5
6
7
8
9
10
11
function GetReadOnlyTable(t)
local meta = {
__index = t,
__newindex = function ()
error("Cannot modify read-only table!")
end
}
local newTable = {}
setmetatable(newTable, meta)
return newTable
end
  • index:索引 table[key]。当 table 不是表或是表 table 中不存在 key 这个键时,这个事件被触发。此时,会读出 table 相应的元方法。这个事件的元方法其实可以是一个函数也可以是一张表。如果它是一个函数,则以 table 和 key 作为参数调用它。如果它是一张表,最终的结果就是以 key 取索引这张表的结果。(这个索引过程是走常规的流程,而不是直接索引,所以这次索引有可能引发另一次元方法。)
  • newindex:索引赋值 table[key] = value。和索引事件类似,它发生在 table 不是表或是表 table 中不存在 key 这个键的时候。此时,会读出 table 相应的元方法。同索引过程那样,这个事件的元方法即可以是函数,也可以是一张表。如果是一个函数,则以 table、key 和 value 为参数传入。如果是一张表,Lua 对这张表做索引赋值操作。(这个索引过程是走常规的流程,而不是直接索引赋值,所以这次索引赋值有可能引发另一次元方法。)一旦有了newindex元方法,Lua 就不再做最初的赋值操作。(如果有必要,在元方法内部可以调用 rawset 来做赋值。)

多人开发避免全局变量泛滥 –> _G表只读

1
2
3
4
5
6
7
8
9
10
-- 设置全局表的元方法,限制全局变量操作
setmetatable(_G, {
__newindex = function(_, key)
error("禁止创建新全局变量,非法键: " .. key)
end,

__index = function(_, key)
error("禁止访问未定义的全局变量,非法键: " .. key)
end
})

_rawset与**_rawget**的区别

元方法 描述 用途
_rawget 访问table中的元素时,直接获取元素的值,不经过**_index**元方法。 当不想通过**_index元方法查询值,而是直接获取table中元素的原始值时,使用_rawget**。
_rawset 更新table中的元素时,直接设置新值,不执行**_newindex**元方法。 当不想执行**_newindex元方法,而是直接设置table中元素的新值时,使用_rawset**。

遍历: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深拷贝和浅拷贝

  • 如何实现浅拷贝
    使用 = 运算符进行浅拷贝
    分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博客

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
function deepCopy(object)
local lookup_table = {} -- 用于记录已拷贝的表(解决循环引用)

local function _copy(obj)
-- 基础类型直接返回
if type(obj) ~= "table" then
return obj

-- 已拷贝过的对象直接返回
elseif lookup_table[obj] then
return lookup_table[obj]

-- 处理新表
else
local new_table = {}
lookup_table[obj] = new_table -- 记录已拷贝的表

-- 递归拷贝键值对(需同时处理 key 和 value
for key, value in pairs(obj) do
new_table[_copy(key)] = _copy(value)
end

-- 继承原表的元表(保留特殊行为)
return setmetatable(new_table, getmetatable(obj))
end
end

return _copy(object)
end

Upvalue

为下一节的闭包做准备:

  • 在lua中,会生成一个全局栈,所有的upvalue都会指向该栈中的值,若对应的参数离开的作用域,栈中的值也会被释放,upvalue的指针会指向自己,等待被gc
  • 闭包运行时,会通过创建指向upvalue的指针,并循环upvalue linked list,找到所需要的外部变量进行运行
  • 一个upvalue有两种状态: open和closed。当一个upvalue被创建时,它是open的,并且它的指针指向Lua栈中对应的变量。当Lua关闭了一个upvalue, upvalue指向的值被复制到upvalue结构内部,并且指针也相应进行调整

一、核心概念

  1. Upvalue 定义

    • 闭包引用的外部局部变量(非函数参数或内部局部变量)。
    • 当函数引用其外部作用域的局部变量时,该变量成为闭包的 Upvalue。
  2. Upvalue 状态

    • Open 状态

      • 初始状态,直接指向 Lua 全局栈中的变量值。

      • 变量存活期间,Upvalue 通过指针访问栈数据。

    • Closed 状态

      • 外部变量离开作用域时触发状态转换。
      • 值从栈复制到 Upvalue 内部存储,指针调整为指向内部数据。
      • 等待垃圾回收(GC)释放内存。
  3. 存储机制

    • Lua 维护全局栈管理局部变量,Upvalue 初始指向栈中位置。
    • 闭包运行时通过循环 Upvalue 链表定位所需外部变量。

二、代码示例

1
2
3
4
5
6
7
8
9
10
11
12
function outer()
local x = 10 -- x 成为 inner 的 Upvalue
function inner()
print(x) -- 访问 Upvalue
x = x + 1 -- 修改 Upvalue
end
return inner
end

local myInner = outer() -- outer 执行结束,x 离开作用域
myInner() -- 输出 10(Closed Upvalue 内部存储值)
myInner() -- 输出 11(修改 Closed Upvalue 的值)

三、关键过程解析

  1. Upvalue 捕获
    • innerouter 内部定义时,自动捕获外部变量 x 作为 Upvalue。
    • 初始阶段,Upvalue 处于 Open 状态,直接指向 outer 栈帧中的 x
  2. 状态转换
    • 当 outer 执行完毕,其栈帧销毁:
      • Upvalue x 转为 Closed 状态,值从栈复制到内部存储。
      • 后续闭包调用通过 Closed Upvalue 访问数据。
  3. 闭包持久性
    • 即使 outer 已结束,闭包仍通过 Closed Upvalue 维持 x 的生命周期。
    • 多次调用闭包可跨作用域读写同一 Upvalue 存储值。

四、机制总结

阶段 行为 结果
闭包定义时 创建 Open Upvalue 指向栈变量 变量存活期内直接引用栈数据
外部作用域结束 Upvalue 转为 Closed,复制值到内部存储 解除栈依赖,独立维护数据
闭包多次调用 通过 Closed Upvalue 读写内部存储 跨调用保持状态一致性

此机制使 Lua 闭包能高效管理外部变量,平衡内存安全与灵活性。

lua闭包

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

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

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

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


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

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

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


闭包总结闭包的主要作用有两个,

一是简洁,不需要在不使用时生成对象,也不需要函数名;

二是捕获外部变量形成不同的调用环境

闭包原理概述:

闭包(函数)编译时会生成原型(prototype) ,包含参数、调试信息、虚拟机指令等一系列该闭包的源信息,其中在递归编辑内层函数时,会为内层函数生成指令,同时为该内层函数需要的所有upvalue创建表,以便之后调用时进行upvalue值搜索

在lua中,会生成一个全局栈,所有的upvalue都会指向该栈中的值,若对应的参数离开的作用域,栈中的值也会被释放,upvalue的指针会指向自己,等待被gc

闭包运行时,会通过创建指向upvalue的指针,并循环upvalue linked list,找到所需要的外部变量进行运行

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)

lua协程

coroutine.create() 创建 coroutine,返回 coroutine, 参数是一个函数,当和 resume 配合使用的时候就唤醒函数调用
coroutine.resume() 重启 coroutine,和 create 配合使用
coroutine.yield() 挂起 coroutine,将 coroutine 设置为挂起状态,这个和 resume 配合使用能有很多有用的效果
coroutine.status() 查看 coroutine 的状态 注:coroutine 的状态有三种:dead,suspended,running,具体什么时候有这样的状态请参考下面的程序
coroutine.wrap() 创建 coroutine,返回一个函数,一旦你调用这个函数,就进入 coroutine,和 create 功能重复
coroutine.running() 返回正在跑的 coroutine,一个 coroutine 就是一个线程,当使用running的时候,就是返回一个 coroutine 的线程号
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
-- coroutine_test.lua 文件
-- 创建了一个新的协同程序对象 co,其中协同程序函数打印传入的参数 i
co = coroutine.create(
function(i)
print(i);
end
)
-- 使用 coroutine.resume 启动协同程序 co 的执行,并传入参数 1。协同程序开始执行,打印输出为 1
coroutine.resume(co, 1) -- 1

-- 通过 coroutine.status 检查协同程序 co 的状态,输出为 dead,表示协同程序已经执行完毕
print(coroutine.status(co)) -- dead

print("----------")

-- 使用 coroutine.wrap 创建了一个协同程序包装器,将协同程序函数转换为一个可直接调用的函数对象
co = coroutine.wrap(
function(i)
print(i);
end
)

co(1)

print("----------")
-- 创建了另一个协同程序对象 co2,其中的协同程序函数通过循环打印数字 1 到 10,在循环到 3 的时候输出当前协同程序的状态和正在运行的线程
co2 = coroutine.create(
function()
for i=1,10 do
print(i)
if i == 3 then
print(coroutine.status(co2)) --running
print(coroutine.running()) --thread:XXXXXX
end
coroutine.yield()
end
end
)

-- 连续调用 coroutine.resume 启动协同程序 co2 的执行
coroutine.resume(co2) --1
coroutine.resume(co2) --2
coroutine.resume(co2) --3

-- 通过 coroutine.status 检查协同程序 co2 的状态,输出为 suspended,表示协同程序暂停执行
print(coroutine.status(co2)) -- suspended
print(coroutine.running())

print("----------")