xLua知识点总结
前言
本篇整理了xLua的一些重要知识点,画了思维导图,方便记忆和回顾。
代码热更新
热更新是指在不需要重新编译打包游戏的情况下,在线更新游戏中的一些非核心代码和资源,比如活动运营和打补丁。代码热更新实际上也是把代码当成资源的一种热更新,代码热更主要包括Lua热更新、ILRuntime热更新和C#直接反射热更新等。目前大多采用更成熟的、没有平台限制的Lua热更新方案。而近年来,ILRuntime 和 HybridCLR 等基于 C# 的热更新方案逐渐流行。这些方案支持直接使用 C# 进行热更新,避免了 Lua 和 C# 混合开发的复杂性,同时提供了更好的性能和开发体验。
另外,因为本人实际开发中,热补丁的应用极少,且热补丁的使用存在相当的缺陷,因此本文并不会提及此模块。
为什么C#不能热更
C#是编译型语言,Unity在打包后,会将C#编译成一种中间代码,再由Mono虚拟机编译成汇编代码供各个平台执行,它打包以后就变成了二进制了,会跟着程序同时启动,就无法进行任何修改了。
C#的编译流程:写好的代码->编译成.dll扩展程序(UnityEditor完成)->运行于Unity
所以直接使用C#进行热更新显然是不可行的,但是也不是说一点办法也没有。在安卓上可以通过C#的语言特性-反射机制实现动态代码加载从而实现热更新。但苹果对反射机制有限制,不能实现这样的热更。为了安全起见,不能给程序太强的能力,因为反射机制实在太过强大,会给系统带来安全隐患。
使用Lua进行热更新方案
Lua 则是解释型语言,并不需要事先编译成块,而是运行时动态解释执行的。这样Lua就和普通的游戏资源如图片,文本没有区别,因此可以在运行时直接从WEB服务器上下载到持久化目录并被其它Lua文件调用。
Lua热更新解决方案是通过一个Lua热更新插件(如uLua、sLua、toLua、xLua等)来提供一个Lua的运行环境以及和C#进行交互。而xLua是腾讯开源的热更新插件,有大厂背书和专职人员维护,插件的稳定性和可持续性较强。
xLua的特性
- 支持各版本的Untiy
- 支持多种平台
- 互访技术。生成适配代码,反射。
- 易用性。解压即可用,不需要生成代码,更加简单的无GC API等。
- 性能优化。Lazyload技术,避免用不上的类型的开销等。
CSharp和Lua侧的交互
这里重点说一下Lua调用C#层面。
LuaCallCSharp
传递C#对象到Lua

性能优化
要说性能优化,首先便是要会导致性能问题的地方。这里可以两个方面来说—————调用方面和传参方面。
性能消耗
调用方面
通过上面的流程,我们其实可以看到调用的过程中,做了很多包括但不限于取值,入栈,缓存,类型转换等操作,这本身是一个很“昂贵”的操作,而这种操作如果如果过于频繁是会造成相当多的性能消耗。
好在我们知道Lua和C#之间的引用是通过索引来进行关联的,只要对象没有被gc,其实也就是查查缓存的事情,但如果已经释放掉了,便是要重新走一遍调用的流程。因此对应临时且多次调用而导致反复分配和gc的情况,便会出现性能问题。
比如:gameobj.transform就是一个巨大的陷阱,因为.transform只是临时返回一下,但是你后面根本没引用,又会很快被lua释放掉,导致你后面每次.transform一次,都可能意味着一次分配和gc。
传参方面
在lua和c#间传递unity独有的值类型(比如Vector3/Quaternion等)更加昂贵,因为这涉及到Lua和C#类型的类型转换,多个参数多次的入栈出栈,内存分配等操作。
虽然我们说的是lua和c#的传参,但是从传参这个角度讲,lua和c#中间其实还夹着一层c,lua、c、c#由于在很多数据类型的表示以及内存分配策略都不同,因此这些数据在三者间传递,往往需要进行转换(术语parameter mashalling),这个转换消耗根据不同的类型会有很大的不同。
例如c#将Vector3传给lua,整个流程如下:
- c#中拿到Vector3的x,y,z三个值
- push这3个float给lua栈
- 然后构造一个表,将表的x,y,z赋值
- 将这个表push到返回值里
一个简单的传参就要完成3次push参数、表内存分配、3次表插入,性能可想而知。
整理一下:
- 严重类: Vector3/Quaternion等unity值类型,数组
- 次严重类:bool string 各种object
- 建议传递:int float double
- bool string类型,涉及到c和c#交互的性能消耗,这两者在c和c#中的内容表示不一样,意味着从c传递到c#的时候需要进行类型转换,降低性能。同时string还要考虑内存的重新分配(将string的内存复制到托管堆,以及utf8和utf16的互相转换)。
- V3/Quaternion,数组甚至更加严重。先说数组,因为lua中只有一个table,和c#是完完全全的两码事,因此这两者的转换只能逐个复制。 而如果成员涉及到的object/string等相关类型,更是要逐个转换。
- int float double 类型是推荐的,因为不涉及到过分的类型转换。
优化方案
调用方面: 针对lua里面获取到的c#里面的object为例, 我们不再使用 “.” 去直接点出它的属性,而是通过访问静态方法执行此object的功能。当然,这需要我们对这个object书写静态拓展方法(之所以说拓展方法,毕竟我们引用的基本上是unity里面go的组件),或者通过统一静态类来管理这种object。 这里选这是前者,我们封装对于组件的拓展方法,然后把self和此操作需要的参数传过去,在此静态方法中执行我们想要的操作。如果当前需要更多的操作,那也只需要的写更多的拓展方法即可。
通过这种形式来减少对于go的组件的引用,特别是.transform这种临时引用。
传参方面: 减少Unity独有值类型的等相关的传参。建立在上面拓展方法的基础上,我们的实现方式就是写重载的拓展方法,允许多样的传参或者统一go上面组件获取的调用去实现功能。
示例演示
比如在lua侧想获取一个GO的transform,我们一般会直接调用 obj.transform;再比如我们想要获取游戏中的一个GO,然后控制其显示和隐藏,就可以使用SetActive这个接口。
1 | local GameObject = CS.UnityEngine.GameObject |
而如果使用拓展方法的话,就会使用这种写法:
1 | [ ] |
然后我们的操作也就变成了
1 | local GameObject = CS.UnityEngine.GameObject |
业务开发中,如果要求采用这种方式进行开发,往往会搭配列一个黑名单,禁止直接调用。
比如:
1 | [ ] |
blackList的目的是为了防止程序员自己又调用了内置接口,比如来新人或者有些人写着写着忘记了 因为这些函数已经封装了正确的调用函数 所以安全起见全部不容许导出xlua。