游戏测试 Lua 优化:破解全局变量的使用困局

侑虎科技 · April 12, 2021 · 1351 hits

之前,我们从 C# 代码的角度,为大家介绍了 CPU 耗时和堆内存的知识点,详见文末的知识点汇总。本期,我们将开启对本地资源检测中 LuaCheck 功能的解读,并结合简单的代码实例来讲解 Lua 检测中的具体情况,力图以浅显易懂的表达,让职场萌新或优化萌新能够深入理解。

Lua 中的全局变量实现

全局变量在大多数语言中都属于 “双刃剑”:全局变量可以表达程序中的全局概念,但是全局变量的使用会带来很多隐患。对于 Lua 这样的嵌入式语言,它是由宿主应用通过调用代码段(Chunk)来实现功能的。但 “程序” 概念并不明确,所以 Lua 语言为了解决这个问题,选择不使用全局变量,转而对全局变量进行模拟,把所有的全局变量保存在一个称为全局环境(global environment)的普通表中。

Lua 5.2 版本中修改了 environment 的概念,新增使用一个名为_ENV、以语法糖形式存在的预定义上值来模拟全局变量。例如一个代码段:

local z=10
x=y+z 

因为 Lua 语言把所有的代码段都当作匿名函数进行处理,实际上编译器会将代码段编译成如下形式:

local _ENV = smoe value
return function (...)
    local z = 10
    _ENV.x = _ENV.y + z
end 

当_ENV 的值是全局环境(global environment)表的时候,Lua 便模拟出 “x、y 是全局变量” 的情况。但实际上 Lua 语言是没有全局变量概念的,关于_ENV 知识的详细解读可以阅读云风大神的 Blog


Lua 中全局变量的危害

1)在程序开发过程中,Lua 语言中的全局变量不需要声明就可以使用,因此随着功能模块的增多,全局变量就会变得非常不稳定,稍有不慎便会重定义覆盖全局变量,产生各种不易查找的灾难性 Bug。

2)对于不是局部变量的访问,Lua 会重定向到全局环境表_G。而局部变量始终具有优先访问权,因此如果一个全局变量和一个局部变量同名且同时存在,那么访问该变量时,得到的始终是局部变量的值。

3)如果没有人为设置,Lua 中通过模拟得到的全局变量,均会被_G 表引用。但如果该全局变量引用了 Unity 中的 Object 对象,就会导致 Unity 中的 Object 对象被 Destroy 之后,_G 表中的全局变量依然引用着该 Object 对象,引发泄露问题。更多关于 Lua 造成的堆内存泄露问题可以阅读本篇文章。

4)开发过程中多个虚拟机同时操作一个变量时,会导致线程不安全。


报告检测规则解读

考虑到以上全局变量的使用隐患,因此我们在使用 Lua 语言开发时,应当尽量少用全局变量。

UWA 本地资源检测服务中,是否允许全局变量的下拉框中有四个选项:

Not:表示不允许使用全局变量,任何地方出现的全局变量都将被视为不通过。

Allow-Defined:表示允许在 Main-Chunk 中声明全局变量后,在函数体等地方使用。

Main-Chunk指的是代码主流程块,例如下边的代码,foo 变量位于 Main-Chunk 中,而 qu 变量在函数体中,不位于 Main-Chunk 中。

foo = 4
print(foo)

function f()
   qu = 4
   print(qu)
end 

Allow-Defined-Top:表示允许在任何地方声明全局变量。

Add-Custom:选择该选项后,会弹出两个输入框(如下图),表示允许维护白名单,“自定义全局变量” 表示白名单中的全局变量允许被声明、使用和修改;“自定义只读全局变量或字段” 表示白名单中的全局变量及其字段允许被声明和使用,不允许被修改。这里的只读全局变量来源于用户自己的设定,可以维护一些不会被更改的数据表。


1、设置一个未定义的全局变量

本条规则下,检测的代码结果会和我们在 UWA Scan Setting 内的设置紧密相连。例如,在 Main-Chunk 中写如下代码:

function f()
   baz = 5
end 

选择 Not 模式,上述声明的全局变量(f、baz)会被视为不通过;

选择 Allow-Defined-Top 模式,在非 Main-Chunk 的代码段中声明的全局变量(baz)会被视为不通过;

选择 Add-Custom 模式,声明的全局变量(f、baz)如果不在白名单中,则会被视为不通过。


2、更改一个未定义的全局变量

例如,在项目中写如下代码:

server.foo = "bar" 

在没有声明全局变量 server 的情况下,我们对全局变量 server 进行了更改,赋值了一个键值对。

改正方式:在赋值之前进行声明:

server={}
server.foo = "bar" 

3、访问一个未定义的全局变量

同样,如果在没有声明全局变量 server 的情况下,我们写了如下代码:

server.sessions["hey"] = "you" 

那实际上是在没有进行声明的前提下,访问了全局变量 server 的 “sessions” 域。
改正方式:在访问之前进行声明。


4、试图设置一个只读的全局变量

如果选择 Add-Custom 模式,那么在 “自定义只读全局变量或字段” 白名单中存在的全局变量理应保持为 “只读” 的属性。

例如:设定全局变量 string 是只读全局变量,但出现如下代码:

string = "foo" 

这就意味着应当保持 “只读” 属性的 string,它的内容却被更改了。本条规则检测的就是这些发生变动的 “只读” 全局变量,开发团队需要去检查那些发生修改操作的代码,判断具体的使用情况是否符合初衷。


5、试图设置一个全局变量的只读域

function fun()
    bar.ff={}
end 

这就导致只读全局变量 bar 的域 ff 被更改,在使用上和 “只读” 发生了冲突,需要进一步的排查。


6、未被使用的隐式定义的全局变量

此处指的是那些被定义了、但从未被使用过的全局变量。例如:声明了全局变量 fun,但在之后的代码中从未使用过它:

function fun()
   baz = 5
end 

开发团队在经过必要的确认后即可删除这些多余的全局变量。


7、试图设置一个全局变量的未定义域

一种可能的情况如下:

function table.clone()
    print('huh')
end 

在没有声明 table 这个全局变量的情况下,我们给全局变量 table 赋值了一个表并添加了一个函数作为 value。在这种情况下,开发团队就要检查相关全局变量和对应域的具体使用。


8、试图访问一个全局变量的未定义域

类似的:

function table.clone()
    print('huh')
end

table.clone() 

类比上一条,在没有声明 table 这个全局变量的情况下,访问了 table.clone()。在经过进一步的检查后,开发团队再决定是否进行声明的补充或者删除访问。


希望以上这些知识点能在实际的开发过程中为大家带来帮助。需要说明的是,每一项检测规则的阈值都可以由开发团队依据自身项目的实际需求设置合适的阈值范围,这也是本地资源检测的一大特点。同时,也欢迎大家来使用 UWA 推出的本地资源检测服务,可帮助大家尽早对项目建立科学的检测规范。

万行代码屹立不倒,全靠基础掌握得好!

相关推荐
《C# 代码优化:斩断伸向堆内存的 “黑手”》
《C# 代码优化:拯救你的 CPU 耗时》
《场景检测:面片、光影和物理属性》
《场景检测:Audio Listener、RigidBody 和 Prefab 连接》
《场景检测:雾效、Canvas 和碰撞体》
《特效优化 2:效果与性能的博弈》
《特效优化:发现绚丽背后的质朴》

性能黑榜相关阅读

《那些年给性能埋过的坑,你跳了吗?》
《那些年给性能埋过的坑,你跳了吗?(第二弹)》
《掌握了这些规则,你已经战胜了 80% 的对手!》

No Reply at the moment.
需要 Sign In 后方可回复, 如果你还没有账号请点击这里 Sign Up