前言

本篇整理了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,整个流程如下:

  1. c#中拿到Vector3的x,y,z三个值
  2. push这3个float给lua栈
  3. 然后构造一个表,将表的x,y,z赋值
  4. 将这个表push到返回值里
    一个简单的传参就要完成3次push参数、表内存分配、3次表插入,性能可想而知。

整理一下:

  • 严重类: Vector3/Quaternion等unity值类型,数组
  • 次严重类:bool string 各种object
  • 建议传递:int float double
  1. bool string类型,涉及到c和c#交互的性能消耗,这两者在c和c#中的内容表示不一样,意味着从c传递到c#的时候需要进行类型转换,降低性能。同时string还要考虑内存的重新分配(将string的内存复制到托管堆,以及utf8和utf16的互相转换)。
  2. V3/Quaternion,数组甚至更加严重。先说数组,因为lua中只有一个table,和c#是完完全全的两码事,因此这两者的转换只能逐个复制。 而如果成员涉及到的object/string等相关类型,更是要逐个转换。
  3. int float double 类型是推荐的,因为不涉及到过分的类型转换。
优化方案

调用方面: 针对lua里面获取到的c#里面的object为例, 我们不再使用 “.” 去直接点出它的属性,而是通过访问静态方法执行此object的功能。当然,这需要我们对这个object书写静态拓展方法(之所以说拓展方法,毕竟我们引用的基本上是unity里面go的组件),或者通过统一静态类来管理这种object。 这里选这是前者,我们封装对于组件的拓展方法,然后把self和此操作需要的参数传过去,在此静态方法中执行我们想要的操作。如果当前需要更多的操作,那也只需要的写更多的拓展方法即可。
通过这种形式来减少对于go的组件的引用,特别是.transform这种临时引用。

传参方面: 减少Unity独有值类型的等相关的传参。建立在上面拓展方法的基础上,我们的实现方式就是写重载的拓展方法,允许多样的传参或者统一go上面组件获取的调用去实现功能。

示例演示

比如在lua侧想获取一个GO的transform,我们一般会直接调用 obj.transform;再比如我们想要获取游戏中的一个GO,然后控制其显示和隐藏,就可以使用SetActive这个接口。

1
2
3
4
5
6
7
8
9
10
11
12
local GameObject = CS.UnityEngine.GameObject

//首先,我们先获取一个测试Obj和它的组件和子物体
local obj = GameObject("Test")
local img_obj = obj:GetComponet(typeof(CS.UnityEngine.UI.Image))
//获取子物体一般是通过.transfrom:Find的方式获取
local rect_objChild = obj.transform:Find("objChild"):GetComponent(typeof(CS.UnityEngine.RectTransform))

//然后我们设置它的现隐
obj.gameObject:SetActive(true or false)
//通过图片的组件获取便是
img_obj.gameObject:SetActive(true or false)

而如果使用拓展方法的话,就会使用这种写法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
[LuaCallCSharp]
public static class UnityObjectExtends
{

public static Transform GetTransform(this Component target)
{
if (null == target)
{
return null;
}
return target.transform;
}

public static Transform GetTransform(this GameObject target)
{
if (null == target)
{
return null;
}
return target.transform;
}

public static Transform GetTransform(this Transform target)
{
if (null == target)
{
return null;
}
return target;
}

//-------------------------------分割线-------------------------------------

//对于控制现隐的操作,我们可以在此进行拓展

//value,1表示开启,0表示关闭
public static void SetActiveE(this Component target, int value)
{
if (target == null)
{
return;
}
SetActiveE(target.gameObject, value);
}

public static void SetActiveE(this GameObject target, int value)
{
if (target == null)
{
return;
}
bool flag = 1 == value;
if (target.activeSelf == !flag)
{
target.SetActive(flag);
}
}

public static void SetActiveE(this Transform target, int value)
{
if (target == null)
{
return;
}
bool flag = 1 == value;
if (target.gameObject.activeSelf == !flag)
{
target.gameObject.SetActive(flag);
}
}

}

然后我们的操作也就变成了

1
2
3
4
5
6
7
8
9
10
11
12
13
local GameObject = CS.UnityEngine.GameObject

//首先,我们先获取一个测试Obj和它的组件和子物体
local obj = GameObject("Test")
local img_obj = obj:GetComponet(typeof(CS.UnityEngine.UI.Image))
//获取子物体的方式也变成了
local rect_objChild = obj:GetTransform():Find("objChild"):GetComponent(typeof(CS.UnityEngine.RectTransform))


//控制现隐
obj:SetActiveE(1 or 0)
//通过组件来控制
img_obj:SetActiveE(1 or 0)

业务开发中,如果要求采用这种方式进行开发,往往会搭配列一个黑名单,禁止直接调用。

比如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
[BlackList]
public static List<List<string>> blackList = new List<List<string>>() {

// Componet
//属性
new List<string>(){ "UnityEngine.Component", "transform"},
new List<string>(){ "UnityEngine.Component", "gameObject"},
//方法
new List<string>(){ "UnityEngine.Component", "GetComponent","System.Type"},
new List<string>(){ "UnityEngine.Component", "GetComponent","System.String"},

// Transform
//属性
new List<string>(){ "UnityEngine.Transform", "childCount"},
new List<string>(){ "UnityEngine.Transform", "localPosition"},
new List<string>(){ "UnityEngine.Transform", "localRotation"},
new List<string>(){ "UnityEngine.Transform", "parent"},
new List<string>(){ "UnityEngine.Transform", "position"},
new List<string>(){ "UnityEngine.Transform", "rotation"},

//方法
new List<string>(){ "UnityEngine.Transform", "Find", "System.String"},
new List<string>(){ "UnityEngine.Transform", "FindChild","System.String"},
new List<string>(){ "UnityEngine.Transform", "GetChild","System.Int32"},
new List<string>(){ "UnityEngine.Transform", "GetChildCount"},

}

blackList的目的是为了防止程序员自己又调用了内置接口,比如来新人或者有些人写着写着忘记了 因为这些函数已经封装了正确的调用函数 所以安全起见全部不容许导出xlua。

参考文档