0%

XLua热更新

XLua热更新

简介

Xlua热更新实战,用来熟悉xlua服务器与客户端交互

Lua之所以是热更新主流方案,是因为Lua是解释型语言,和图片,声音这些一样,都是资源。

仓库可以直接拉下来用

其中的NetBox可以理解为一个小服务器,直接打开挂着就行,里面的端口号可以改

在unity工程中,C#脚本Helloworld和test是输出。

项目传送门:传送门

核心设计逻辑

用法简介

Helloworld和test两个文件里的输出随便改一下,然后点击菜单栏“Tools/AB包加密/创建AB包版本文件/Window 版本“

此时输出的文件在AssetBundlesEncrypt里,复制到Net Box Server/Test/Web下

再把刚才改的两个文件还原一下,运行Unity会看到热更新文件下载到了C:\Users\Administrator\AppData\LocalLow\DefaultCompany\Learn_HotUpdate1目录下

输出的内容就是Web服务器上改动过的内容

代码核心

HotUpdateMgr

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
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.Networking;
using System.IO;
using System.Text;

public class HotUpdateMgr : MonoSingletonBase<HotUpdateMgr>
{
/// <summary>
/// _sBaseUrl下载网址
/// </summary>
#if UNITY_EDITOR || UNITY_STANDALONE_WIN
public static string _sBaseUrl = "http://127.0.0.1:5858";
#elif UNITY_ANDROID
public static string _sBaseUrl = "http://192.168.255.10:5858";
#elif UNITY_IPHONE
public static string _sBaseUrl = "http://192.168.255.10:5858";
#endif

private string _sABVersionName = "";

/// <summary>
/// 本地版本信息缓存路径
/// </summary>
private string _sVersionLocalFilePath = "";

/// <summary>
/// 同时下载的最大数量
/// </summary>
private int _nMaxDownloader = 5;

/// <summary>
/// 当前需要下载的AB包数据
/// </summary>
List<ABPackInfo> _list_allNeedABPack = new List<ABPackInfo>();

/// <summary>
/// 所需下载资源总大小
/// </summary>
private float _nDownloadTotalSize = 0;

/// <summary>
/// 当前已下载资源的大小
/// </summary>
private float _nCurDownloadedSize = 0;

/// <summary>
/// AB包下载器
/// </summary>
private List<ABDownloader> _list_allABDownloader = new List<ABDownloader>();

/// <summary>
/// 客户端的AB版本数据
/// </summary>
private Dictionary<string, ABPackInfo> _dict_clientABInfoList = null;

protected override void Awake()
{
string sPlatformStr = ABPackUtils.GetABPackPathPlatformStr();
_sABVersionName = sPlatformStr + ABPackUtils.sABVersionName;
_sVersionLocalFilePath = Application.persistentDataPath + _sABVersionName;
IOUtils.CreateDirectroryOfFile(_sVersionLocalFilePath);
}

/// <summary>
/// 开始热更
/// </summary>
public void StartHotUpdate()
{
Debug.Log("开始热更 >>>>>> ");
StartCoroutine(DownloadAllABPackVersion());
}

/// <summary>
/// 解析版本文件,返回一个文件列表
/// </summary>
/// <param name="sContent"></param>
/// <returns></returns>
public Dictionary<string, ABPackInfo> ConvertToAllABPackDesc(string sContent)
{
Dictionary<string, ABPackInfo> dict_allABPackDesc = new Dictionary<string, ABPackInfo>();
string[] arrLines = sContent.Split('\n');//用回车 字符 \n 分割每一行
foreach (string item in arrLines)
{
string[] arrData = item.Split(' ');//用空格分割每行数据的三个类型
if (arrData.Length == 3)
{
ABPackInfo obj_ABPackData = new ABPackInfo();
obj_ABPackData.sABName = arrData[0]; // 名称即路径
obj_ABPackData.sMd5 = arrData[1]; // md5值
obj_ABPackData.nSize = int.Parse(arrData[2]); // AB包大小

//Debug.Log(string.Format("解析的路径:{0}\n解析的MD5:{1}\n解析的文件大小KB:{2}", obj_ABPackData.sABName, obj_ABPackData.sMd5, obj_ABPackData.nSize));
dict_allABPackDesc.Add(obj_ABPackData.sABName, obj_ABPackData);
}
}

return dict_allABPackDesc;
}


/// <summary>
/// 获取服务端的AB包版本信息
/// </summary>
/// <returns></returns>
IEnumerator DownloadAllABPackVersion()
{
string sVersionUrl = _sBaseUrl + @"/" + _sABVersionName;
//Debug.Log("下载版本数据路径:" + sVersionUrl);

using (UnityWebRequest uObj_versionWeb = UnityWebRequest.Get(sVersionUrl))
{
yield return uObj_versionWeb.SendWebRequest(); // 等待资源下载
if (uObj_versionWeb.isNetworkError || uObj_versionWeb.isHttpError)
{
Debug.LogError("获取版本AB包数据错误: " + uObj_versionWeb.error);
yield break;
}
else
{
string sVersionData = uObj_versionWeb.downloadHandler.text;
//Debug.Log("成功获取到版本相关数据 >>>> \n" + sVersionData);
CheckNeedDownloadABPack(sVersionData);
}
}
}

/// <summary>
/// 检测需要下载
/// </summary>
/// <param name="sServerVersionData"></param>
void CheckNeedDownloadABPack(string sServerVersionData)
{
//Debug.Log("运行平台:" + Application.platform);
//Debug.Log("本地版本文件路径是:" + _sVersionLocalFilePath);

Dictionary<string, ABPackInfo> dict_serverDownList = ConvertToAllABPackDesc(sServerVersionData); // 服务端获取的资源下载列表

if (File.Exists(_sVersionLocalFilePath))
{
//Debug.Log("存在本地,对比服务器版本信息");
string sClientVersionData = File.ReadAllText(_sVersionLocalFilePath); // 本地版本信息
_dict_clientABInfoList = ConvertToAllABPackDesc(sClientVersionData); // 客户端本地缓存的资源下载列表

//遍历服务器文件
foreach (ABPackInfo obj_itemData in dict_serverDownList.Values)
{
// 存在对应已下载文件,对比Md5值是否一致
if (_dict_clientABInfoList.ContainsKey(obj_itemData.sABName))
{
// md5值不一致,则更新文件
if (_dict_clientABInfoList[obj_itemData.sABName].sMd5 != obj_itemData.sMd5)
{
_list_allNeedABPack.Add(obj_itemData);
_nDownloadTotalSize = _nDownloadTotalSize + obj_itemData.nSize;

//Debug.Log("MD5 值不一样,资源存在变更,增加文件 >>>>> " + obj_itemData.sABName);
}
}
else
{
_list_allNeedABPack.Add(obj_itemData);
_nDownloadTotalSize = _nDownloadTotalSize + obj_itemData.nSize;
}
}
}
else // 如果说不存在本地缓存,那就直接下载所有的AB包
{
foreach (ABPackInfo obj_itemData in dict_serverDownList.Values)
{
_list_allNeedABPack.Add(obj_itemData);
_nDownloadTotalSize = _nDownloadTotalSize + obj_itemData.nSize;
//Debug.Log("所需下载文件 >>>>> " + obj_itemData.sABName);
}
}
StartDownloadAllABPack();
}

/// <summary>
/// 开始下载所有所需下载的AB包资源
/// </summary>
/// <param name="list_allABPack"></param>
void StartDownloadAllABPack()
{
int nMaxCount = _list_allNeedABPack.Count;
if (nMaxCount <= 0)
{
HotUpdateEnd();
return;
}

int nNeedCount = Mathf.Min(nMaxCount, _nMaxDownloader);
for (int i = 0; i < nNeedCount; i++)
{
ABPackInfo obj_ABPackDesc = _list_allNeedABPack[0];
ABDownloader obj_downloader = new ABDownloader();
_list_allABDownloader.Add(obj_downloader);
StartCoroutine(obj_downloader.DownloadABPack(obj_ABPackDesc));
_list_allNeedABPack.RemoveAt(0);
}
}

/// <summary>
/// 切换下载下一个AB包
/// </summary>
/// <param name="obj_ABDownloader">需要切换的下载器</param>
public void ChangeDownloadNextABPack(ABDownloader obj_ABDownloader)
{
//Debug.Log("切换下载下一个 AB 包");
_nCurDownloadedSize += obj_ABDownloader.GetDownloadResSize();

if (_list_allNeedABPack.Count > 0) // 还存在需要下载的资源,下载器切换资源,继续下载
{
StartCoroutine(obj_ABDownloader.DownloadABPack(_list_allNeedABPack[0]));
_list_allNeedABPack.RemoveAt(0);
}
else
{
bool bIsDownloadSuc = true; // 资源是否全部下载完成
foreach(ABDownloader obj_downloader in _list_allABDownloader)
{
if(obj_downloader.bIsDownloading) // 存在一个下载中,即表示当前还有未下载完成的部分
{
bIsDownloadSuc = false;
break;
}
}

if (bIsDownloadSuc) // 已完成全部下载
{
HotUpdateEnd();
}
}
}

/// <summary>
/// 更新本地缓存的AB包版本数据
/// </summary>
/// <param name="obj_ABPackDecs"></param>
public void UpdateClientABInfo(ABPackInfo obj_ABPackDecs)
{
if (_dict_clientABInfoList == null)
{
_dict_clientABInfoList = new Dictionary<string, ABPackInfo>();
}

_dict_clientABInfoList[obj_ABPackDecs.sABName] = obj_ABPackDecs;

StringBuilder obj_sb = new StringBuilder();
foreach (ABPackInfo obj_temp in _dict_clientABInfoList.Values)
{
obj_sb.AppendLine(ABPackUtils.GetABPackVersionStr(obj_temp.sABName, obj_temp.sMd5, obj_temp.nSize.ToString()));
}

IOUtils.CreatTextFile(_sVersionLocalFilePath, obj_sb.ToString());
}

/// <summary>
/// 热更新结束,进入下一个阶段
/// </summary>
private void HotUpdateEnd()
{
// TODO 进入下一个阶段
Debug.Log("热更新: 已完成所有的AB包下载, 进入下一个阶段 TODO");
HotUpdateTest.GetInstance().RunLua();
HotUpdateTest.GetInstance().InitShow();
}
}

注释写的很全了

核心逻辑就是对比下版本号,下载到可读写目录,优先读这个目录就可以了

XLua热更新步骤

  1. 下载xLua插件,解压后将该目录中Assets文件夹下的所有资源复制到Unity工程的Assets文件夹下。

  2. 在Unity编辑器(File->Build Settings->Player Settings->Other Settings->Scripting Define Symbols)下中添加HOTFIX_ENABLE宏以支持xLua热更新,Unity编辑器和各个手机平台都要添加。建议平时用Lua写业务逻辑时可以关闭HOTFIX_ENABLE宏,当打包手机版本或者在编辑器下开发补丁时才添加HOTFIX_ENABLE宏。

  3. 对所有较大可能变动的类型加上[Hotfix]标签。如果可能变动的类比较多,手动添加比较麻烦,一般游戏初次上线时,由于不确定添加哪些类,因此我们可以用反射将当前程序集下的所有类自动加上[Hotfix]标签,还可以按某个namespace或目录等条件进行设置。代码如下:

1
2
3
4
5
6
7
8
9
10
11
[Hotfix]
public static List<Type> by_property
{
get
{
// 需要using System.Linq;
return (from type in Assembly.Load("Assembly-CSharp").GetTypes()
where type.Namespace == "XXXX"
select type).ToList();
}
}
  1. 新建一个MonoBehavior脚本并挂载到需要热更新的场景中,然后在Awake函数中新建一个Lua虚拟机用于加载和执行Lua热更新脚本文件。代码如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// 需要using XLua;
void Awake()
{
// 新建一个Lua虚拟机,为减少开销,建议全局唯一。
LuaEnv luaEnv = new LuaEnv();
// DoString表示执行Lua代码,由于Unity不能识别.lua文件,只能把Lua文件变成文本文件进行读取。
// require用于加载Lua文件,内置多个Loader加载器,我们也可以自己写Loader。
luaEnv.DoString("require 'hotfix'");
}

// 在游戏对象被销毁时,释放Lua虚拟机内存。
void OnDestroy()
{
luaEnv.Dispose();
}
  1. 由于xLua内置了从Resources目录下加载Lua文本文件,因此我们新建一个hotfix.lua.txt文本文件,然后在里面用Lua实现热更新逻辑。代码如下:
1
2
3
4
5
// CS.XXX表示在C#代码中打[HotFix]标签的XXX类,"Start"表示XXX类中要进行更改的Start函数, 
// function(self)表示Start函数更改后的函数逻辑,待热更新完后XXX类的Start函数就会执行function(self)中的代码逻辑。
xlua.hotfix(CS.XXX, "Start", function(self)
print("hello world")
end)
  1. 点击Unity编辑器的XLua/Generate Code工具,该操作会收集所有打上[HotFix]标签的类并生成适配代码。

  2. 点击Unity编辑器的XLua/Hotfix inject in Editor工具,该操作会对所有打上[HotFix]标签的类进行IL注入。

  3. 运行游戏,若发现XXX类的Start函数输出了hello world,则表示热更新成功,即整个热更新流程就走完了。

热更新底层原理

不限于XLua,XLua只是工具,核心还是C#和Lua的交互

C#调用Lua:

C#生成Bridge文件,Bridge调用dll文件(dll是C写的库),先调用Lua中的dll文件,再由dll文件执行Lua代码。

C#-> Bridge -> dll -> Lua / C#-> dll -> Lua

Lua调用C#:

先生成Wrap文件(中间文件/配置文件),wrap文件把字段方法注册到lua虚拟机中(解释器Iuajit),然后Lua通过wrap就可以调C#了。

碎碎念(项目中的代码底层原理)

这里的XLua,xLua实际上是C#和Lua进行交互的桥梁,因此xLua不仅可以用于热更新,还可以借助它用Lua实现游戏中一些性能要求不高的业务逻辑。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
[Hotfix]
public class Test : MonoBehaviour
{
void Start ()
{
// 接下来对Start函数进行热更新,改为输出Hello World。
Debug.Log("test");
}

void Update ()
{

}
}

Test类打上[HotFix]标签后,执行XLua/Generate Code后,xLua会根据内置的模板代码生成器在XLua目录下的Gen目录中生成一个DelegatesGensBridge.cs文件,该文件在XLua命名空间下生成一个DelegateBridge类,这个类中的__Gen_Delegate_Imp*函数会映射到xlua.hotfix中的function。代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
namespace XLua
{
public partial class DelegateBridge : DelegateBridgeBase
{
// DelegateBridge类的关键函数__Gen_Delegate_Imp*
public void __Gen_Delegate_Imp0(object p0)
{
RealStatePtr L = luaEnv.rawL;
// luaReference就是指向xlua.hotfix(CS.XXX, "Start", function(self))的function
int errFunc = LuaAPI.pcall_prepare(L, errorFuncRef, luaReference);
ObjectTranslator translator = luaEnv.translator;
translator.PushAny(L, p0);
PCall(L, 1, 0, errFunc);
LuaAPI.lua_settop(L, errFunc - 1);
}
}
}

生成适配器代码后,执行XLua/Hotfix inject in Editor后,xLua会使用Mono.Cecil库对当前工程下的Assembly-CSharp.dll程序集进行IL注入。IL是.NET平台上的C#、F#等高级语言编译后产生的中间代码,该中间代码IL再经.NET平台中的CLR(类似于JVM)编译成机器码让CPU执行相关指令。由于移动平台无法把C#代码编译成IL中间代码,所以绝大多数热更新方案都会涉及到IL注入,只有这样Unity内置的VM才能对热更新的代码进行处理。

Mono是社区对.NET Framework的跨平台实现方案,实现了.NET Framework的绝大部分类库,因此基于Mono研发的Unity引擎才具有跨平台能力。而Mono VM就是基于Mono框架实现的,不同的平台实现不同的Mono VM,从而可以不同平台上执行C#脚本。由于IL代码是C#代码编译而来的,因此我们可以借用ILSpy工具对C#编译出来的程序集DLL文件进行反编译得到C#源代码,看看IL注入后打上[HotFix]标签的类的变化。注入后的C#代码如下:

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
[Hotfix(HotfixFlag.Stateless)]
public class Test : MonoBehaviour
{
// 构造函数对应的DelegateBridge变量
private static DelegateBridge _c__Hotfix0_ctor;
private static DelegateBridge __Hotfix0_Start;
private static DelegateBridge __Hotfix0_Update;
private static DelegateBridge __Hotfix0_TestFunc;

public Test()
: this()
{
_c__Hotfix0_ctor?.__Gen_Delegate_Imp0(this);
}

private void Start()
{
DelegateBridge _Hotfix0_Start = __Hotfix0_Start;
// 如果lua脚本里定义了热更新函数,就执行对应的热更新函数逻辑。
if (_Hotfix0_Start != null)
{
_Hotfix0_Start.__Gen_Delegate_Imp0(this);
}
else
{
Debug.Log((object)"test");
}
}

private void Update()
{
__Hotfix0_Update?.__Gen_Delegate_Imp0(this);
}

private void TestFunc()
{
__Hotfix0_TestFunc?.__Gen_Delegate_Imp0(this);
}
}

从反编译的C#代码看出,xLua进行IL注入时会为打上[Hotfix]标签的类的所有函数创建一个DelegateBridge变量,同时添加对应的判断条件。如果Lua脚本中添加了对应的热更新函数,DelegateBridge变量就不为空,并将DelegateBridge变量中的__Gen_Delegate_Imp0方法指向xlua.hotfix(CS.XXX, “Start”, function(self))中的具体function。这时由于DelegateBridge变量不为空,所以C#中的函数就会执行Lua脚本中对应的热更新函数逻辑。但如果没有定义对应的热更新函数,或对应的热更新函数为nil,DelegateBridge变量就为空,则C#中的函数依然执行原有的函数逻辑。因此,xLua热更新实际上就是在运行时用Lua函数替换对应的C#函数。