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 >{ #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 = "" ; private string _sVersionLocalFilePath = "" ; private int _nMaxDownloader = 5 ; List<ABPackInfo> _list_allNeedABPack = new List<ABPackInfo>(); private float _nDownloadTotalSize = 0 ; private float _nCurDownloadedSize = 0 ; private List<ABDownloader> _list_allABDownloader = new List<ABDownloader>(); 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); } public void StartHotUpdate () { Debug.Log("开始热更 >>>>>> " ); StartCoroutine(DownloadAllABPackVersion()); } public Dictionary<string , ABPackInfo> ConvertToAllABPackDesc (string sContent ) { Dictionary<string , ABPackInfo> dict_allABPackDesc = new Dictionary<string , ABPackInfo>(); string [] arrLines = sContent.Split('\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 ]; obj_ABPackData.nSize = int .Parse(arrData[2 ]); dict_allABPackDesc.Add(obj_ABPackData.sABName, obj_ABPackData); } } return dict_allABPackDesc; } IEnumerator DownloadAllABPackVersion () { string sVersionUrl = _sBaseUrl + @"/" + _sABVersionName; 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; CheckNeedDownloadABPack(sVersionData); } } } void CheckNeedDownloadABPack (string sServerVersionData ) { Dictionary<string , ABPackInfo> dict_serverDownList = ConvertToAllABPackDesc(sServerVersionData); if (File.Exists(_sVersionLocalFilePath)) { string sClientVersionData = File.ReadAllText(_sVersionLocalFilePath); _dict_clientABInfoList = ConvertToAllABPackDesc(sClientVersionData); foreach (ABPackInfo obj_itemData in dict_serverDownList.Values) { if (_dict_clientABInfoList.ContainsKey(obj_itemData.sABName)) { if (_dict_clientABInfoList[obj_itemData.sABName].sMd5 != obj_itemData.sMd5) { _list_allNeedABPack.Add(obj_itemData); _nDownloadTotalSize = _nDownloadTotalSize + obj_itemData.nSize; } } else { _list_allNeedABPack.Add(obj_itemData); _nDownloadTotalSize = _nDownloadTotalSize + obj_itemData.nSize; } } } else { foreach (ABPackInfo obj_itemData in dict_serverDownList.Values) { _list_allNeedABPack.Add(obj_itemData); _nDownloadTotalSize = _nDownloadTotalSize + obj_itemData.nSize; } } StartDownloadAllABPack(); } 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 ); } } public void ChangeDownloadNextABPack (ABDownloader obj_ABDownloader ) { _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(); } } } 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()); } private void HotUpdateEnd () { Debug.Log("热更新: 已完成所有的AB包下载, 进入下一个阶段 TODO" ); HotUpdateTest.GetInstance().RunLua(); HotUpdateTest.GetInstance().InitShow(); } }
注释写的很全了
核心逻辑就是对比下版本号,下载到可读写目录,优先读这个目录就可以了
XLua热更新步骤
下载xLua插件 ,解压后将该目录中Assets文件夹下的所有资源复制到Unity工程的Assets文件夹下。
在Unity编辑器(File->Build Settings->Player Settings->Other Settings->Scripting Define Symbols)下中添加HOTFIX_ENABLE宏以支持xLua热更新,Unity编辑器和各个手机平台都要添加。建议平时用Lua写业务逻辑时可以关闭HOTFIX_ENABLE宏,当打包手机版本或者在编辑器下开发补丁时才添加HOTFIX_ENABLE宏。
对所有较大可能变动的类型加上[Hotfix]标签。如果可能变动的类比较多,手动添加比较麻烦,一般游戏初次上线时,由于不确定添加哪些类,因此我们可以用反射将当前程序集下的所有类自动加上[Hotfix]标签,还可以按某个namespace或目录等条件进行设置。代码如下:
1 2 3 4 5 6 7 8 9 10 11 [Hotfix ] public static List<Type> by_property{ get { return (from type in Assembly.Load("Assembly-CSharp" ).GetTypes() where type.Namespace == "XXXX" select type).ToList(); } }
新建一个MonoBehavior脚本并挂载到需要热更新的场景中,然后在Awake函数中新建一个Lua虚拟机用于加载和执行Lua热更新脚本文件。代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 void Awake (){ LuaEnv luaEnv = new LuaEnv(); luaEnv.DoString("require 'hotfix'" ); } void OnDestroy (){ luaEnv.Dispose(); }
由于xLua内置了从Resources目录下加载Lua文本文件,因此我们新建一个hotfix.lua.txt文本文件,然后在里面用Lua实现热更新逻辑。代码如下:
1 2 3 4 5 xlua.hotfix(CS.XXX, "Start" , function(self) print("hello world" ) end)
点击Unity编辑器的XLua/Generate Code工具,该操作会收集所有打上[HotFix]标签的类并生成适配代码。
点击Unity编辑器的XLua/Hotfix inject in Editor工具,该操作会对所有打上[HotFix]标签的类进行IL注入。
运行游戏,若发现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 () { 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 { public void __Gen_Delegate_Imp0(object p0) { RealStatePtr L = luaEnv.rawL; 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 { 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; 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#函数。