Lua篇 — Lua介绍

Posted by Xun on Saturday, October 23, 2021

随着游戏行业的发展,热更已经成为游戏开发不可或缺的功能。热更有很多方案,lua热更是使用较广泛,也相对稳定的方案。因此,需要对lua有一定程度的了解,对于lua热更框架才能理解得更加透彻。

简介

  • lua是一种弱语言类型,可在运行时加载执行,不需要提前编译,所以在游戏开发中,常用来实现线上修复更新,下面会介绍一些lua中常用的概念。(参考 http://cloudwu.github.io/lua53doc/manual.html

值与类型

  • Lua 中有八种基本类型: nil、boolean、number、string、function、userdata、 thread 和 table。
    • number:包括整数和浮点数。
    • userdata:将 C 中的数据保存在 Lua 变量中,用户数据类型的值是一个内存块,有两种用户数据:
      • fulluserdata:完全用户数据 ,指一块由 Lua 管理的内存对应的对象。
      • lightuserdata:轻量用户数据 ,一个简单的 C 指针,需要用户自行管理内存。
    • table:表内可以包含任何类型的值(nil 除外)。 任何键的值若为 nil 就不会被记入表结构内部。 换言之,对于表内不存在的键,都对应着值 nil 。

环境与全局环境

  • 每个被编译的 Lua 代码块都会有一个外部的局部变量叫 _ENV ,_ENV 这个名字永远都不会成为一个代码块中的自由名字。被 _ENV 用于值的那张表被称为 环境。
  • Lua 保有一个被称为 全局环境 特别环境。它被保存在 C 注册表 (LUA_REGISTRYINDEX)的一个特别索引下。 在 Lua 中,全局变量 _G 被初始化为这个值。 (_G 不被内部任何地方使用。)
  • 当 Lua 加载一个代码块,_ENV 这个 upvalue 的默认值就是这个全局环境。

错误处理

  • Lua 代码中调用 error 函数可以显式地抛出一个错误。
  • 如果你需要在 Lua 中捕获这些错误,可以使用 pcall 或 xpcall 在保护模式下调用一个函数。

元表及元方法

  • Lua 中的每个值都可以有一个元表。 这个元表就是一个普通的 Lua 表, 它用于定义原始值在特定操作下的行为。

  • 用 getmetatable 函数来获取任何值的元表,用 setmetatable 来设置一张表的元表。

  • 元方法,即元表中以"__“作为前缀的方法,元表中常用的元方法有:

    • __index
      • 查找 table[key] 的值,当 table 不是表或是表 table 中不存在 key 这个键时,会触发这个元方法。这个元方法可以是一个函数也可以是一张表,如果它是一个函数,则以 table 和 key 作为参数调用它。如果它是一张表,最终的结果就是以 key 取索引这张表的结果。
    • __newindex
      • 设置 table[key] = value,,当 table 不是表或是表 table 中不存在 key 这个键时,会触发这个元方法。这个元方法可以是一个函数也可以是一张表,如果是一个函数,则以 table、 key、以及 value 为参数传入。如果是一张表,Lua 对这张表做索引赋值操作。
    • __call
      • 函数调用操作 func(args)。当 Lua 尝试调用一个非函数的值的时候会触发这个事件(即 func 不是一个函数)。查找 func 的元方法,如果找得到,就调用这个元方法,func 作为第一个参数传入原来调用的参数(args)后依次排在后面。
    • __gc
      • 垃圾收集的元方法,被称为终结器。当一个被标记的对象成为了垃圾后,垃圾收集器并不会立刻回收它。取而代之的是,Lua 会将其置入一个链表。在收集完成后,Lua 将遍历这个链表。Lua 会检查每个链表中的对象的 __gc 元方法,如果是一个函数,那么就以对象为唯一参数调用它,否则直接忽略它。

    垃圾收集

    • Lua 采用了自动内存管理。 Lua 运行了一个垃圾收集器来收集所有死对象(即在 Lua 中不可能再访问到的对象)来完成自动内存管理的工作。Lua 中所有用到的内存,如:字符串、表、用户数据、函数、线程、内部结构等,都服从自动管理。
    • Lua 实现了一个增量标记-扫描收集器。它使用这两个数字来控制垃圾收集循环(使用百分数为单位,例如:值 100 在内部表示 1 )
    • 垃圾收集器间歇率:
      • 控制着收集器需要在开启新的循环前要等待多久。增大这个值会减少收集器的积极性。当这个值比 100 小的时候,收集器在开启新的循环前不会有等待。设置这个值为 200 就会让收集器等到总内存使用量达到之前的两倍时才开始新的循环。
      • 使用 collectgarbage(“setpause”) 设置。
    • 垃圾收集器步进倍率:
      • 控制着收集器运作速度相对于内存分配速度的倍率。增大这个值不仅会让收集器更加积极,还会增加每个增量步骤的长度。不要把这个值设得小于 100 ,那样的话收集器就工作的太慢了以至于永远都干不完一个循环。默认值是 200 ,这表示收集器以内存分配的"两倍"速工作。
      • 使用 collectgarbage(“setstepmul”) 设置。

弱表

  • 弱表指内部元素为弱引用的表。垃圾收集器会忽略掉弱引用。换句话说,如果一个对象只被弱引用引用到,垃圾收集器就会回收这个对象。
  • 一张弱表可以有弱键或是弱值,也可以键值都是弱引用。含有弱值的表允许收集器回收它的值,但会阻止收集器回收它的键。若一张表的键值均为弱引用,那么收集器可以回收其中的任意键和值。
  • 任何情况下,只要键或值的任意一项被回收, 相关联的键值对都会从表中移除。
  • 一张表的元表中的 __mode 域控制着这张表的弱属性。
    • 当 __mode = ‘k’ 时,这张表的所有键皆为弱引用。
    • 当 __mode = ‘v’ 时,这张表的所有值皆为弱引用。
  • 属性为弱键强值的表也被称为暂时表。对于一张暂时表,它的值是否可达仅取决于其对应键是否可达。特别注意,如果表内的一个键仅仅被其值所关联引用,这个键值对将被表内移除。

协程

  • coroutine.create
    • 可创建一个协程。其唯一的参数是该协程的主函数。 create 函数只负责新建一个协程并返回其句柄(一个 thread 类型的对象),而不会启动该协程。
  • coroutine.resume
    • 执行一个协程。第一次调用 coroutine.resume 时,第一个参数应传入 coroutine.create 返回的线程对象,然后协程从其主函数的第一行开始执行。传递给 coroutine.resume 的其他参数将作为协程主函数的参数传入。协程启动之后,将一直运行到它终止或让出。
  • coroutine.yield
    • 使协程暂停执行,让出执行权。协程让出时,对应的最近 coroutine.resume 函数会立刻返回,即使该让出操作发生在内嵌函数调用中(即不在主函数,但在主函数直接或间接调用的函数内部)。在协程让出的情况下, coroutine.resume 也会返回 true,并加上传给 coroutine.yield 的参数。当下次重启同一个协程时,协程会接着从让出点继续执行。此时,此前让出点处对 coroutine.yield 的调用会返回,返回值为传给 coroutine.resume 的第一个参数之外的其他参数。
  • coroutine.wrap
    • 与 coroutine.create 类似,创建一个协程。不同之处在于,它不返回协程本身,而是返回一个函数。调用这个函数将启动该协程。传递给该函数的任何参数均当作 coroutine.resume 的额外参数。 coroutine.wrap 返回 coroutine.resume 的所有返回值,除了第一个返回值(bool的错误码)。和 coroutine.resume 不同, coroutine.wrap 不会捕获错误,而是将任何错误都传播给调用者。

闭包

  • 当 C 函数被创建出来,我们有可能会把一些值关联在一起,也就是创建一个 C 闭包(参见 lua_pushcclosure)。 这些被关联起来的值被叫做 upvalue,它们可以在函数被调用的时候访问的到。
  • 无论何时去调用 C 函数,函数的 upvalue 都可以用伪索引定位。可以用 lua_upvalueindex 这个宏来生成这些伪索引。第一个关联到函数的值放在 lua_upvalueindex(1) 位置处,依此类推。使用 lua_upvalueindex(n) 时,若 n 大于当前函数的总 upvalue 个数(但不可以大于 256)会产生一个可接受的但无效的索引。

注册表

  • Lua 提供了一个注册表,这是一个预定义出来的表,可以用来保存任何C代码想保存的Lua值,即 LUA_REGISTRYINDEX 。
  • 注册表中的整数键用于引用机制(参见 luaL_ref),以及一些预定义的值。因此,整数键不要用于别的目的。
  • 当创建了一个新的 Lua 状态机, 其中的注册表内就预定义好了几个值。 这些预定义值可以用整数索引到,这些整数以常数形式定义在 lua.h 中。有下列常数:
    • LUA_RIDX_MAINTHREAD: 注册表中这个索引下是状态机的主线程。(主线程和状态机同时被创建出来。)
    • LUA_RIDX_GLOBALS: 注册表的这个索引下是全局环境。

栈索引

  • Lua 的操作都是在栈上进行,一般方法都会需要传指定索引 index,有两种情况:
    • index > 0,表示从栈底数起,第几个位置。
    • index < 0,表示从栈顶数起,第几个位置。

常用方法

  • lua中的一些常用方法,对于阅读lua框架有一定帮助。
  • lua_absindex
    • int lua_absindex (lua_State *L, int idx);
    • 将一个可接受的索引 idx 转换为绝对索引 ,即一个不依赖栈顶在哪的值。
  • lua_call
    • void lua_call (lua_State *L, int nargs, int nresults);
    • 要调用一个函数请遵循以下协议:
      • 要调用的函数应该被压入栈。
      • 把需要传递给这个函数的参数按正序压栈,这是指第一个参数首先压栈。
      • 调用一下 lua_call,nargs 是你压入栈的参数个数。
      • 当函数调用完毕后,所有的参数以及函数本身都会出栈,而函数的返回值这时则被压栈。返回值的个数将被调整为 nresults 个,除非 nresults 被设置成 LUA_MULTRET。在这种情况下,所有的返回值都被压入堆栈中。
      • Lua 会保证返回值都放入栈空间中。函数返回值将按正序压栈(第一个返回值首先压栈),因此在调用结束后,最后一个返回值将被放在栈顶。
  • lua_gc
    • int lua_gc (lua_State *L, int what, int data);
    • 控制垃圾收集器,根据其参数 what 发起几种不同的任务:
      • LUA_GCSTOP: 停止垃圾收集器。
      • LUA_GCRESTART: 重启垃圾收集器。
      • LUA_GCCOLLECT: 发起一次完整的垃圾收集循环。
      • LUA_GCCOUNT: 返回 Lua 使用的内存总量(以 K 字节为单位)。
      • LUA_GCCOUNTB: 返回当前内存使用量除以 1024 的余数。
      • LUA_GCSTEP: 发起一步增量垃圾收集。
      • LUA_GCSETPAUSE: 把 data 设为垃圾收集器间歇率,并返回之前设置的值。
      • LUA_GCSETSTEPMUL: 把 data 设为垃圾收集器步进倍率,并返回之前设置的值。
      • LUA_GCISRUNNING: 返回收集器是否在运行(即没有停止)。
  • lua_getglobal
    • int lua_getglobal (lua_State *L, const char *name);
    • 把全局变量 name 里的值压栈,返回该值的类型。
  • lua_gettable
    • int lua_gettable (lua_State *L, int index);
    • 把 t[k] 的值压栈,这里的 t 是指索引指向的值 而 k 则是栈顶放的值。
  • lua_gettop
    • int lua_gettop (lua_State *L);
    • 返回栈顶元素的索引。因为索引是从 1 开始编号的,所以这个结果等于栈上的元素个数,0 表示栈为空。
  • lua_newuserdata
    • void *lua_newuserdata (lua_State *L, size_t size);
    • 这个函数分配一块指定大小的内存块,把内存块地址作为一个完全用户数据压栈,并返回这个地址。宿主程序可以随意使用这块内存。
  • lua_pop
    • void lua_pop (lua_State *L, int n);
    • 从栈中弹出 n 个元素。
  • lua_pushboolean
    • void lua_pushboolean (lua_State *L, int b);
    • 把 b 作为一个布尔量压栈。
  • lua_pushcclosure
    • void lua_pushcclosure (lua_State *L, lua_CFunction fn, int n);
    • 把一个新的 C 闭包压栈。
  • lua_pushinteger
    • void lua_pushinteger (lua_State *L, lua_Integer n);
    • 把值为 n 的整数压栈。
  • lua_pushnil
    • void lua_pushnil (lua_State *L);
    • 将空值压栈。
  • lua_pushnumber
    • void lua_pushnumber (lua_State *L, lua_Number n);
    • 把一个值为 n 的浮点数压栈。
  • lua_pushstring
    • const char *lua_pushstring (lua_State *L, const char *s);
    • 将指针 s 指向的零结尾的字符串压栈。Lua 对这个字符串做一个内部副本(或是复用一个副本),因此 s 处的内存在函数返回后,可以释放掉或是立刻重用于其它用途。返回内部副本的指针。如果 s 为 NULL,将 nil 压栈并返回 NULL。
  • lua_pushvalue
    • void lua_pushvalue (lua_State *L, int index);
    • 把栈上给定索引处的元素作一个副本压栈。
  • lua_setfield
    • void lua_setfield (lua_State *L, int index, const char *k);
    • 做一个等价于 t[k] = v 的操作, 这里 t 是给出的索引处的值, 而 v 是栈顶的那个值。
    • 这个函数将把这个值弹出栈。跟在 Lua 中一样,这个函数可能触发一个 __newindex 事件的元方法。
  • lua_setglobal
    • void lua_setglobal (lua_State *L, const char *name);
    • 从堆栈上弹出一个值,并将其设为全局变量 name 的新值。
  • lua_settable
    • void lua_settable (lua_State *L, int index);
    • 做一个等价于 t[k] = v 的操作,这里 t 是给出的索引处的值, v 是栈顶的那个值, k 是栈顶之下的值。
    • 这个函数会将键和值都弹出栈。跟在 Lua 中一样,这个函数可能触发一个 __newindex 事件的元方法。
  • lua_settop
    • void lua_settop (lua_State *L, int index);
    • 参数允许传入任何索引以及 0 。它将把堆栈的栈顶设为这个索引。如果新的栈顶比原来的大,超出部分的新元素将被填为 nil 。如果 index 为 0 ,把栈上所有元素移除。
  • lua_rawget
    • int lua_rawget (lua_State *L, int index);
    • 类似于 lua_gettable ,但是作一次直接访问(不触发元方法)。
  • lua_rawgeti
    • int lua_rawgeti (lua_State *L, int index, lua_Integer n);
    • 把 t[n] 的值压栈,这里的 t 是指给定索引处的表。这是一次直接访问,就是说,它不会触发元方法。
  • lua_rawset
    • void lua_rawset (lua_State *L, int index);
    • 类似于 lua_settable ,但是是做一次直接赋值(不触发元方法)。
  • lua_rawseti
    • void lua_rawseti (lua_State *L, int index, lua_Integer i);
    • 等价于 t[i] = v , 这里的 t 是指给定索引处的表, 而 v 是栈顶的值。
    • 这个函数会将值弹出栈。 赋值是直接的,即不会触发元方法。
  • lua_type
    • int lua_type (lua_State *L, int index);
    • 返回给定有效索引处值的类型,当索引无效(或无法访问)时则返回 LUA_TNONE。 lua_type 返回的类型被编码为一些个在 lua.h 中定义的常量:
      • LUA_TNIL
      • LUA_TNUMBER
      • LUA_TBOOLEAN
      • LUA_TSTRING
      • LUA_TTABLE
      • LUA_TFUNCTION
      • LUA_TUSERDATA
      • LUA_TTHREAD
      • LUA_TLIGHTUSERDATA
    • lua_load
      • int lua_load (lua_State *L, lua_Reader reader, void *data, const char *chunkname, const char *mode);
      • 加载一段 Lua 代码块,但不运行它。
    • luaL_dofile
      • int luaL_dofile (lua_State *L, const char *filename);
      • 加载并运行指定的文件。
    • luaL_dostring
      • int luaL_dostring (lua_State *L, const char *str);
      • 加载并运行指定的字符串。
    • luaL_ref
      • int luaL_ref (lua_State *L, int t);
      • 针对栈顶的对象,创建并返回一个在索引 t 指向的表中的引用(最后会弹出栈顶对象)。
    • luaL_unref
      • void luaL_unref (lua_State *L, int t, int ref);
      • 释放索引 t 处表的 ref 引用对象 (参见 luaL_ref )。 此条目会从表中移除以让其引用的对象可被垃圾收集。而引用 ref 也被回收再次使用。如果 ref 为 LUA_NOREF 或 LUA_REFNIL, luaL_unref 什么也不做。