【热更新实践】Lua和热更新
  EFTJ6596AiAP 2023年11月02日 41 0

Lua重点语法

数据结构

有八种:

  • 值类型:nil\string\boolean\number
  • 引用类型:function\thread\userdata\table

(1)基本概念
  • 本质上是键值对,下标从1开始;数组的索引可以是数组或者字符串。
  • table 不会固定长度大小,有新数据添加时 table 长度会自动增长,没初始的 table 都是 nil。

case 01:

a = {6,8,9,"a",x=123,pos = {x=90,y=89,z=20}}
a.z=156
print(a.z)
print(a.x)
print(a.pos)
print(a.pos.x)

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

for k,v in ipairs(a) do
print(k, ":", v)
end

输出效果:

【热更新实践】Lua和热更新_热更新

知识点:

  • table中nil不可以做为键
  • ipairs和pairs的区别
  • 两者都可以用来遍历,但是ipairs只能遍历数组部分,pairs可以遍历数组部分和hash部分
  • 对于nil的处理不同,pairs遇到nil会跳过之后继续执行,ipairs会停止遍历

case 02:

b={pos={2,6,9},8,90,"z",x=678,122,}
for k,v in pairs(b) do
print(k, ":", v)
end

for k,v in ipairs(b) do
print(k, ":", v)
end

输出效果:

【热更新实践】Lua和热更新_服务器_02

case 2:

b={pos={2,6,9},8,nil,90,"z",x=678,122,}
for k,v in pairs(b) do
print(k, ":", v)
end
print("------------------------------------")
for k,v in ipairs(b) do
print(k, ":", v)
end

输出效果:

【热更新实践】Lua和热更新_热更新_03

(2)底层实现

table的底层分为数组部分+哈希表部分

每个table结构最多由3块连续内存组成:Table结构+数组(存放了连续的整数索引)+哈希表(大小为2的整数次幂)

(3)表的索引

a = {6,8,9,"a",x=123,pos = {x=90,y=89,z=20}}
print(a[1])
print(a.pos,a.x)

输出效果:

【热更新实践】Lua和热更新_服务器_04

解读:有两种方式,[]可以实现所有的索引,.只能实现索引为字符的索引

string

(1)基本概念

lua中有8种基本的数据类型

nil, boolean, number, string, function, userdata, thread, and table

string有三种表示形式

a="abc"
b='hjk'
c= [[I'm a good man.]]
print(a,b,c)

输出效果:

【热更新实践】Lua和热更新_版本号_05

C#中字符char用单引号来表示,lua中没有char这种数据结构,单个字符的string就用来表示字符了。

(2)底层实现

string库中的function都是返回一个数据对象,而不是直接对原来的string进行操作。


变量和表达式

声明方法的方式

function foo(x)
print(x)
end

-- 等价于
foo = function(x)
print(x)
end

点和冒号的区别

深拷贝 & 浅拷贝

“=”表示浅拷贝,传递值或者地址

深拷贝表示复制对象的基本类型,也复制源对象中的对象。一般需要用到table来实现,通过循环将值赋给新表中的值,还需要将原来表的元表赋值给新表作为元表。


元表和元方法

元表的本质:一个普通的table+定义了特定事件下的值。由key(一个__开头的string)和metavalue(大多数是一个function,被叫做元方法)组成。

操作:

  • 获取:getmetatable
  • 改变:setmetatable

每一种类型的数据共享同一个metatable;除了string类型,其他类型都是默认没有元表的。

__index & __newindex

  • 在表中查找一个值,如果没找到,则判断其是否有元表
  • 如果没有元表,查找结束,返回nil;如果有元表,则调用__index去父脚本里查找

相关的key值

  • __rawget & __rawset
  • __rawget:不想从__index对应的元方法中查找值
  • __rawset:不想执行__newindex对应的元方法

代码示例:

t = {b=3,c=7}

mt = {
--[[__index = function (table, key)
return 123
end,]]

--[[__index = {
f=123,
e=456
}]]

__newindex = function (t,k,v)
rawset(t,k,v)
--t[k] = v
end
}
setmetatable(t, mt)

t.f = 123
t.e = 678
print(type(getmetatable(t).__newindex))
print(t["f"])
print(t["e"])

输出效果:

【热更新实践】Lua和热更新_服务器_06

知识点总结:

  • __index是用来做查询的,如果在一个table中没有找到,就去这个表的元表里面找;元表的value可以是function也可以是table;
  • __newindex是用来新创建表的值的,如果用这种赋值方式t[k] = v,会因为递归导致堆栈溢出,只能使用rawset

推崇的做法:

  • 在将一个表设置为另一个对象的元表之前,先将所需的所有元方法写好
  • 在对象创建之后立即为其设置元表


实现面向对象

要求:

  • 实现对象:利用table和function来实现类,利用元表来实现继承和多态
  • 实现继承:设置元表,然后要将子类的__index设置好

代码示例:

bag = {

}

bagmt = {

--__index = bagmt;

put = function (t, item)
table.insert(t.items, item)
end,

take = function (t)
return table.remove(t.items)
end,
list = function (t)
for k,v in pairs(t.items) do
print(k, ":", v)
end
end
}

bagmt.__index = bagmt

--实现的效果,当bagmt中检索不到的时候,就可以去mt里面查找

function bag.new()
local t ={
items = {}
}
setmetatable(t,bagmt)
return t
end

local b= bag.new()
b:put("an apple")
print(b.items[1])
print(b:take())
b:put("a pear")
b:put("a pineaple")
b:put("another apple")
b: list()

自己写的代码示例:

dish = {
putin = function (t,item)
table.insert(t.dishes, item)
end,

remove = function (t)
return table.remove(t.item)
end,

show = function (t)
print(table.concat(t.dishes, "-"))
end
}

dish.__index = dish

dinner = {
new =function ()
local t = {
dishes = {}
}
setmetatable(t,dish)
return t
end
}

local dinnertoday = dinner.new()
dinnertoday:putin("dumplings")
dinnertoday:putin("liz")
dinnertoday:show()

这里是一个思路:

【热更新实践】Lua和热更新_服务器_07

实现私有对象

实现一个只读的表

只读表需要满足的条件:

  • 禁止在表中创建新的值
  • 禁止改变已有的值
  • 子表也只是可读

代码实现:

参照了这篇博文,但是对于其中的一些写法做出了改进,应该是更好理解了。

​https://blog.csdn.net/Mr_Sun88/article/details/105648580?spm=1001.2101.3001.6650.4&utm_medium=distribute.pc_relevant.none-task-blog-2%7Edefault%7ECTRLIST%7ERate-4-105648580-blog-90167437.pc_relevant_default&depth_1-utm_source=distribute.pc_relevant.none-task-blog-2%7Edefault%7ECTRLIST%7ERate-4-105648580-blog-90167437.pc_relevant_default&utm_relevant_index=6​

local testTable = {}
testTable.width = 10
testTable.print = function ()
print("This is a readonly table.")
end
testTable.childtable = {2,5,8,9}

function setReadOnlyTable(t)
--local proxy = {}
--这一个表就是存储了__index和__newindex的表,可以被设置为元表
local meta = {
__index = t,
__newindex = function (t,k,v)
error("testTable is a readonly table.")
end
}

--[[ 设置元表方法1:博文中的用法,感觉不太直观,效果是一样的
setmetatable(proxy, meta)
return proxy
]]

--设置元表方法2:改进之后的写法,感觉比较直观
setmetatable(t,meta)
return t

end

local readonlyTable = setReadOnlyTable(testTable)
print("------------------------------------")
print(readonlyTable.width)
readonlyTable.print()
print("------------------------------------")
print(readonlyTable.childtable[3])
readonlyTable.childtable[3]=18
readonlyTable.x="abc"

输出效果:

【热更新实践】Lua和热更新_版本号_08

闭包

闭包 = 函数 + 引用环境

在Lua中function是一种first-class value,具有如下特点:

  • ​函数​可以​存储​​变量或table​中。
  • ②可以作为​实参​传递给其他函数。
  • ③可以作为其他函数的​返回值​
  • ④可以在​运行期间​被创建(C语言的函数就没有这个特点)。

【热更新实践】Lua和热更新_版本号_09

在运行时,每当Lua执行一个形如function…end这样的函数时,它就会创建一个新的数据对象,其中包含但不只限于相应函数原型的引用和一个由所有upvalue引用组成的数组,而这个数据对象就称为闭包。

函数是编译期概念,是静态的,而闭包是运行期概念,是动态的。

Upvalue

upvalue是让lua来模拟实现,类似c语言中,静态变量机制的一种机制。一个带上upvalue的函数,我们称之为闭包(closure)。


GC

参考:​​https://blog.csdn.net/fwb330198372/article/details/104263213​

lua有自动的内存管理,会有一个垃圾收集器来自动的回收垃圾。当一个对象被垃圾收集器确认不会再被访问到的时候,就会被认为是死对象。垃圾收集器有两种模式:incremental & generational。

GC的大致演变过程:

  • 两色增量标记清除法-三色增量标记清除法
  • 分步GC-分代GC(5.2版本的分代GC-5.4版本的分代GC)

三色增量标记清除法

采用三色增量标记清除法的优势:GC不需要额外的等待

两色增量标记清除法的问题:需要停下来等待GC

三色含义:

  • 白色:新建对象的初始状态。如果在一轮GC扫描结束后,对象还是为白色,该对象可以被进行回收了。
  • 灰色:表示对象已经被GC扫描过,但该对象引用的对象还没被扫描到。
  • 黑色:表示对象已经被GC扫描过,同时该对象引用的对象也被扫描过了。

采用三色增量标记清除法GC的流程:

  • 开始:将新创建的对象置为白色
  • 初始化:遍历根结点中引用的对象,从白色置为灰色,放到灰色节点列表中
  • 标记阶段:扫描判断这个对象引用的对象是否已经被扫描到
  • 回收阶段:遍历对象,如果还是白色,说明是没有被引用的对象,那么回收

分代GC

为了减少遍历,5.2中引入了分代GC,但是那时候只要存货过一轮就容易被标记为老年代,这样minorGC的时候就不会再扫描到。这使得局部变量需要等到majorGC的时候才会被扫描到。5.4的GC是需要存活过两轮之后才会被标记为老年代。

分代GC中有两个参数the minor multiplier & the the major multiplier。表示当内存增长到上一轮majorGC的x%之后,就进行下一代的GC。其中minor multiplier的范围是20-200,major multiplier的范围是100-1000。


协程

进程、线程、协程

  • 进程:
  • 操作系统中程序运行的基本单位
  • 每个进程都有自己独立的内存空间,所以进程间切换的开销比较大
  • 通信:不同的进程通过进程间通信
  • 优势:稳定;劣势:切换开销大
  • 线程:
  • 一个进程至少包含一个线程,可能有多个
  • 又叫轻量级进程,是CPU调度的最小单位
  • 通信:通过共享内存
  • 优势:切换快,开销小;劣势:没有进程稳定,容易丢失数据
  • 协程:
  • 一个线程可以有多个协程,是一种用户态的轻量级线程
  • 协程的调度是不被操作系统控制的,由程序来控制

Lua中的协程

协程中的状态:

  • running
  • suspended

Lua提供的是非对称协程(​asymmertric coroutine​),也就是说需要两个函数来控制协程的运行,一个用于挂起协程的执行,另一个用于恢复它的执行。yiled函数让一个运行中的协程挂起自己,然后通过resume函数让其恢复运行。

协程案例:

co = coroutine.create(
function ()
print("coroutine running")
for i=1,5 do
print("running,",i)
coroutine.yield()
end
print("after yield")

end
)
print(type(co))

--协程启动
coroutine.resume(co)
print("------------------")
print("after one round",coroutine.status(co))
coroutine.resume(co)
print("------------------")
coroutine.resume(co)
print("------------------")
coroutine.resume(co)
print("------------------")
coroutine.resume(co)
print("after five round",coroutine.status(co))
print("------------------")
coroutine.resume(co)
print("after 6 round",coroutine.status(co))
print("------------------")
coroutine.resume(co)
print("------------------")
coroutine.resume(co)
print("------------------")
coroutine.resume(co)
print("after 9 round",coroutine.status(co))

以下是输出效果:每一次yield之后协程都进入了suspended状态,最后协程运行完毕之后就进入了dead状态。

【热更新实践】Lua和热更新_版本号_10

排序算法

冒泡排序

代码:

a={6,9,10,3,2,8}

--排序方法
function bubbleSort(t)
local index = 0
for i=1, #t -1 do
for j=1, #t-i do
if t[j] > t[j+1] then
--交换
t[j], t[j+1] = t[j+1], t[j]
end

end
index = index+1
end
return t
end

--打印方法
function PrintTable(t)
for k,v in pairs(t) do
print(k,":",v)
end
end

--测试
PrintTable(a)
local b=bubbleSort(a)
print("------------排序后--------------")
PrintTable(b)

输出效果:

【热更新实践】Lua和热更新_热更新_11

快速排序

代码:

function quickSort(t, left, right)
if left >= right then return end

local low = left
local high = right
local base = t[low]--提取基准

while low < high do
while t[high]>= base and low <high do
high = high -1
end
t[low] = t[high]

while t[low]<= base and low < high do
low = low +1
end
t[high] = a[low]
end
t[low] =base

--接下来递归

quickSort(t,left,low-1)
quickSort(t, low+1,right)
end

--打印方法
function PrintTable(t)
for k,v in pairs(t) do
print(k,":",v)
end
end

print("------------quickSort--------------")
quickSort(a, 1, #a)
PrintTable(a)

输出效果:

【热更新实践】Lua和热更新_服务器_12

热更新

C#和Lua交互

底层:

  • C#调用Lua:C#文件先调用Lua解析器底层的dll库,然后由dll文件执行lua文件。
  • Lua调用C#:
  • Wrap方式:C#生成Wrap文件,Lua调用生成的Wrap文件,再由wrap文件去调用C#文件。
  • 反射方式:这种方式的执行效率比wrap模式低。

AssetBundle

含义:Unity可以在运行的时候加载的非代码资产。

Unity自带一个BuildPipeline.BuildAssetBundles()可以用来创造AB包。


Lua如何实现热更新

通过Lua的模块加载机制。热更新的核心就是替换package.loaded表中的模块。

  • 🔑导出函数require(mode_name)
  • 🔑查询全局缓存表package.loaded
  • 🔑通过package.searchers查找加载器


原理

Unity游戏热更新包含两个方面:资源+脚本

热更新的方案:AssetsBundle,AB包的本质是缺省的resources

涉及3个目录:

  • 游戏资源目录(仅可读):
  • windos:Application.dataPath + "/StreamingAsset"
  • ios: Application.dataPath + "/Raw"
  • android: jar:file://Application.dataPath + "!/assets"
  • 数据目录: 可读可写,安卓和ios上的游戏资源目录是仅可读的
  • 网络资源地址:服务器地址,用来存放游戏资源的网址

热更的步骤:

  • 第一次开启游戏:将游戏资源目录中的内容复制到数据目录
  • 游戏开启后:从网络资源地址下载更新的文件到数据目录中;过程中会先下载服务器上的files.txt,和本地的MD5做比较,更新有变化的文件
  • 游戏过程中:从数据目录中获取、解包。

数据目录中包含:不同版本的资源文件+用于版本控制的file.txt(包含:资源文件的名称+MD5)

热更新的过程

Unity3D 热更新的一般方法:

  • 导出热更资源
  • 打包md5信息
  • 上传热更ab到热更服务器
  • 上传版本信息到版本服务器
  • 游戏流程热更
  • 启动游戏
  • 根据版本号和平台号去版本服务器上检查是否有热更
  • 从热更服务器上下载MD5文件
  • 从热更服务器上下载需要热更的资源,解压到热更资源目录
  • 游戏运行加载资源,优先到热更目录,然后才是母包资源目录

MD5:通过MD5算法生成电子签名,类似于数字指纹;Unity热更中MD5文件存储的信息包括:AB路径、MD5值、未压缩文件大小、压缩文件大小


版本号

一般是四位:

  • 巨大版本号:有巨大的变化才会变
  • 整包更新版本号:走应用商店的大的版本迭代
  • 服务器协议版本号:需要更新商店的版本号
  • 编译/热更版本号:每次热更都+1


参考文献

​http://www.lua.org/manual/5.4/manual.html#2.5​


【版权声明】本文内容来自摩杜云社区用户原创、第三方投稿、转载,内容版权归原作者所有。本网站的目的在于传递更多信息,不拥有版权,亦不承担相应法律责任。如果您发现本社区中有涉嫌抄袭的内容,欢迎发送邮件进行举报,并提供相关证据,一经查实,本社区将立刻删除涉嫌侵权内容,举报邮箱: cloudbbs@moduyun.com

  1. 分享:
最后一次编辑于 2023年11月08日 0

暂无评论

EFTJ6596AiAP