unity热更新Lua
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 | __add(a, b) --加法 |
- Lua的表元素查找机制
1 | father = { |
假设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 | meta={} |
这里我myTable.val=1 访问了myTable表中没有的属性val
然后引发了寻找_newinde表
而_newindex指向了空表 { }
则输出nil
- _rawset与**_rawget**的区别
元方法 | 描述 | 用途 |
---|---|---|
_rawget | 访问table中的元素时,直接获取元素的值,不经过**_index**元方法。 | 当不想通过**_index元方法查询值,而是直接获取table中元素的原始值时,使用_rawget**。 |
_rawset | 更新table中的元素时,直接设置新值,不执行**_newindex**元方法。 | 当不想执行**_newindex元方法,而是直接设置table中元素的新值时,使用_rawset**。 |
- 怎么实现一个只读表
1 | local function readonly(t) |
遍历:pairs 和 ipairs区别??
- 自定义索引
1 | a={[0]=1,2,3,[-1]=4,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 | local t = {[1]=1,2,[3]=3,4,[5]=5,[6]=6} |
这边存在一点问题,对于[1]=1,2 这个怎么处理
输出:
1 | ipairs |
所以当ipairs遍历table时,从键值对索引值[1]开始连续递增,当键值对索引值[ ]断开或遇到nil时退出,所以上面的例子中ipairs遍历出的结果是2,4,3。
而pairs遍历时,会遍历表中的所有键值对,先按照索引值输出数组,在输出其它键值对,且元素是根据哈希算法来排序的,得到的不一定是连续的,所以pairs遍历出的结果是2,4,3,6,5。
- 对于数字和表混合的内容俩者又有什么区别呢
1 | local testTab ={1,2,3,4,5}; |
输出结果:
1 | ipairs testTab |
Lua系列–pairs和ipairs_lua pairs-CSDN博客
Lua类与对象
Lua中 点和冒号区别
点 :无法传递自身,需要显示传递
冒号 :隐式传递自身
类与对象写法
1 | Object = {} |
1 | ChildClass =Object:subClass("ChildClass") |
封装
1 | -- Class.lua |
继承
1 | -- SubClass.lua |
多态
1 | -- main.lua |
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 |
|
- Lua调用C
调用之前需要注册,将函数地址告知Lua
LuaFramework的框架中Lua要调用Unity自带的API或者我们自己写的脚本之前要先生成对应的XXXWrap文件,就是如上面例子一样,需要在lua里进行注册。
首先我们在C++中定义一个方法,该方法必须以Lua_State作为参数,返回值类型为int,表示要返回的值的数目。
1 | -- Lua函数:l_ff |
这些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 | local numTest1=5 |
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 | // 初始化阶段 |
总结
Lua通过借助grey链表,依次利用reallymarkobject对对象进行了颜色的标记,之后通过遍历alloc链表,依次利用sweeplist清除需要回收的对象。