• Unity3D热更设计:一款基于 HybridCLR的C#热更方案


      在这篇文章之前,可以转到我的这两篇博客:C#热更方案 HybridCLR尝鲜:Windows及Android打包超详细的Unity3D热更新框架,附示例链接,小白也能看的懂_鹿野素材屋的博客-CSDN博客_热更新框架

      这两篇博客看完后,应该就会对热更有个大致的印象了,接下来我们要做的就是将两者合并起来,实现真正的热更。

      首先我们要在脚本加载之前加载出所有的脚本文件,MD5效验部分就不再赘叙,具体代码如下:

    1. using HybridCLR;
    2. using System;
    3. using System.Collections;
    4. using System.Collections.Generic;
    5. using System.IO;
    6. using System.Linq;
    7. using UnityEngine;
    8. using UnityEngine.Networking;
    9. using System.Text;
    10. using System.Security.Cryptography;//包含MD5库
    11. //MD5信息
    12. [Serializable]
    13. public class MD5Message
    14. {
    15. public string file;//文件位置及名字
    16. public string md5;//MD5效验结果
    17. public string fileLength;//文件长度
    18. }
    19. //MD5全部信息
    20. [Serializable]
    21. public class FileMD5
    22. {
    23. public string length;//总长度
    24. public MD5Message[] files;
    25. }
    26. public static class UrlConfig
    27. {
    28. public static string urlPre = "http://192.168.6.123/Chief/MD5";//一些基本的网络配置
    29. }
    30. ///
    31. /// 初始化加载dll脚本:这里如果需要的话,最好是把表格数据加载也放到最开始
    32. ///
    33. public class DllLogin : MonoBehaviour
    34. {
    35. //所有引用
    36. public static List<string> AOTMetaAssemblyNames { get; } = new List<string>()
    37. {
    38. "mscorlib.dll",
    39. "System.dll",
    40. "System.Core.dll",
    41. };
    42. void Start()
    43. {
    44. //先进行热更
    45. Debug.Log("热更");
    46. StartCoroutine(VersionUpdate());
    47. }
    48. #region 热更部分
    49. ///
    50. /// 进度
    51. ///
    52. public float progress;
    53. ///
    54. /// 版本更新
    55. ///
    56. IEnumerator VersionUpdate()
    57. {
    58. Debug.Log(GetTerracePath());
    59. //这里做一些本地化处理
    60. #if UNITY_EDITOR_WIN
    61. pathName = "Android";
    62. #elif UNITY_EDITOR_OSX
    63. pathName = "IOS";
    64. #elif UNITY_ANDROID
    65. pathName = "Android";
    66. #elif UNITY_IOS
    67. pathName = "IOS";
    68. #endif
    69. Debug.Log(UrlConfig.urlPre);
    70. var _isGet = true;
    71. var uri = HttpDownLoadUrl("Bundle.txt");
    72. Debug.Log(uri);
    73. var request = UnityWebRequest.Get(uri);
    74. yield return request.SendWebRequest();
    75. _isGet = !(request.result == UnityWebRequest.Result.ConnectionError);
    76. if (_isGet)
    77. {
    78. int allfilesLength = 0;
    79. var _txt = request.downloadHandler.text;
    80. Debug.Log("拿到txt:");
    81. //这里要通过MD5效验先拿到所有的代码数据
    82. List bims = new List();
    83. FileMD5 date = JsonUtility.FromJson(_txt);
    84. var _files = date.files;
    85. //初始热更 和岛屿升级热更
    86. var _list = ReviseMD5List(_files);
    87. Debug.LogError("需要热更长度" + _list.Count);
    88. DeleteOtherBundles(_files);//删除所有不受版本控制的文件
    89. string md5, file, path;
    90. int lenth;
    91. for (int i = 0; i < _list.Count; i++)
    92. {
    93. MD5Message _md5 = _list[i];
    94. //Debug.Log(_md5.file + " " + _md5.fileLength + " " + _md5.md5);
    95. file = _md5.file;
    96. path = PathUrl(file);
    97. md5 = GetMD5HashFromFile(path);
    98. //Debug.LogError(file + " " + path + " " + md5);
    99. if (string.IsNullOrEmpty(md5) || md5 != _md5.md5)
    100. {
    101. //Debug.LogError("需要添加" + string.IsNullOrEmpty(md5) + " " + md5 + _md5.md5 + " " + md5 != _md5.md5);
    102. bims.Add(new BundleInfo()
    103. {
    104. Url = HttpDownLoadUrl(file),
    105. Path = path
    106. });
    107. lenth = int.Parse(_md5.fileLength);
    108. allfilesLength += lenth;
    109. }
    110. }
    111. Debug.Log(allfilesLength / (1024 * 1024));
    112. Debug.LogError("热更资源数量" + bims.Count);
    113. if (bims.Count <= 1) progress = 1;
    114. //Debug.LogError("......." + GameProp.Inst.isLandUpdate);
    115. //当有新热更资源需要更新时,自动调用岛屿升级回调??????????
    116. if (bims.Count > 0)
    117. {
    118. //Debug.LogError("开始尝试更新");
    119. //if (bims.Count != 1) UIMainProp.Inst.isResUpdate = true;
    120. StartCoroutine(DownLoadBundleFiles(bims, (progress) =>
    121. {
    122. Debug.Log("自动更新中..."+progress+":"+allfilesLength);
    123. }, (isfinish) =>
    124. {
    125. if (isfinish)
    126. {
    127. StartCoroutine(DownLoadAssets(this.StartGame));
    128. }
    129. else
    130. StartCoroutine(VersionUpdate());
    131. }));
    132. }
    133. else
    134. {
    135. StartCoroutine(DownLoadAssets(this.StartGame));
    136. }
    137. }
    138. }
    139. ///
    140. /// 删除所有不受版本控制的所有文件
    141. ///
    142. void DeleteOtherBundles(/*FileMD5 _md5*/MD5Message[] _list)
    143. {
    144. Debug.LogError("~~~~~~~~~~开始删除~~~~~~~");
    145. var _r = "/Android/";
    146. try
    147. {
    148. string[] bundleFiles = Directory.GetFiles(GetTerracePath() + _r, "*.*", SearchOption.AllDirectories);
    149. Debug.LogError("文件名校对 :长度" + bundleFiles.Length + "::::" + GetTerracePath());
    150. foreach (string idx in bundleFiles)
    151. {
    152. try
    153. {
    154. var _s = idx.Replace(GetTerracePath() + _r, "");
    155. _s = _s.Replace(@"\", "/");
    156. //Debug.Log(idx+":"+ _s + ":" + Directory.Exists(_s));
    157. if (/*Directory.Exists(_s) &&*/ !FindNameInFileMD5(_list, _s))
    158. {
    159. Debug.LogError(idx + "即将被删除");
    160. File.Delete(idx);
    161. }
    162. }
    163. catch
    164. {
    165. Debug.Log("删除文件报错" + idx);
    166. }
    167. }
    168. }
    169. catch
    170. {
    171. Debug.Log("没有android文件夹");
    172. }
    173. //Debug.Log("~~~~~~~结束删除~~~~~~~");
    174. }
    175. static bool FindNameInFileMD5(MD5Message[] _list, string _name)
    176. {
    177. foreach (var _m in _list)
    178. {
    179. //Debug.LogError("文件名校对" + _m.file + ":::" + _name);
    180. if (_m.file == _name) return true;
    181. }
    182. return false;
    183. }
    184. public string GetMD5HashFromFile(string fileName)
    185. {
    186. if (File.Exists(fileName))
    187. {
    188. FileStream file = new FileStream(fileName, FileMode.Open);
    189. MD5 md5 = new MD5CryptoServiceProvider();
    190. byte[] retVal = md5.ComputeHash(file);
    191. file.Close();
    192. StringBuilder sb = new StringBuilder();
    193. for (int i = 0; i < retVal.Length; i++)
    194. sb.Append(retVal[i].ToString("x2"));
    195. return sb.ToString();
    196. }
    197. return null;
    198. }
    199. ///
    200. /// 校正MD5 list
    201. ///
    202. public List ReviseMD5List(MD5Message[] _list)
    203. {
    204. var _checkList = new List();
    205. var assets = new List<string>
    206. {
    207. "prefabs",
    208. "Assembly-CSharp.dll",
    209. };
    210. foreach (var _idx in _list)
    211. {
    212. foreach (var _i in assets)
    213. {
    214. var _len = _idx.file.Split('/');
    215. var _end = _len[_len.Length - 1];
    216. if (_idx.file.Contains("11")) Debug.Log(_idx.file + " ::: " + (_end.Contains(_i) + " : " + !_end.Contains("public_uitoy") + " :: " + _end != "Bundle.txt"));
    217. if (_end.Contains(_i))
    218. {
    219. _checkList.Add(_idx);
    220. break;
    221. }
    222. }
    223. }
    224. Debug.Log("检验列表" + _checkList.Count);
    225. return _checkList;
    226. }
    227. ///
    228. /// 路径比对
    229. ///
    230. public IEnumerator DownLoadBundleFiles(List infos, Action<float> LoopCallBack = null, Action<bool> CallBack = null)
    231. {
    232. //Debug.Log("开始路径对比" + infos.Count);
    233. int num = 0;
    234. string dir;
    235. for (int i = 0; i < infos.Count; i++)
    236. {
    237. BundleInfo info = infos[i];
    238. Debug.LogError(info.Url + " :::: " + i);
    239. var uri = info.Url;
    240. var request = UnityWebRequest.Get(uri);
    241. yield return request.SendWebRequest();
    242. var _isGet = !(request.result == UnityWebRequest.Result.ConnectionError);
    243. if (_isGet)
    244. {
    245. try
    246. {
    247. string filepath = info.Path;
    248. dir = Path.GetDirectoryName(filepath);
    249. if (!Directory.Exists(dir))
    250. Directory.CreateDirectory(dir);
    251. File.WriteAllBytes(filepath, request.downloadHandler.data);
    252. num++;
    253. if (LoopCallBack != null)
    254. LoopCallBack.Invoke((float)num / infos.Count);
    255. //Debug.Log(dir + "下载完成");
    256. }
    257. catch (Exception e)
    258. {
    259. Debug.Log("下载失败"+e);
    260. }
    261. }
    262. else
    263. {
    264. Debug.Log("下载失败");
    265. }
    266. }
    267. if (CallBack != null)
    268. CallBack.Invoke(num == infos.Count);
    269. }
    270. ///
    271. /// 记录信息
    272. ///
    273. public struct BundleInfo
    274. {
    275. public string Path { get; set; }
    276. public string Url { get; set; }
    277. public override bool Equals(object obj)
    278. {
    279. return obj is BundleInfo && Url == ((BundleInfo)obj).Url;
    280. }
    281. public override int GetHashCode()
    282. {
    283. return Url.GetHashCode();
    284. }
    285. }
    286. ///
    287. /// 获得下载的URL
    288. ///
    289. public string HttpDownLoadUrl(string _str)
    290. {
    291. return UrlConfig.urlPre + pathName + "/" + _str;
    292. }
    293. public string pathName;
    294. //根据不同路径,对下载路径进行封装
    295. string PathUrl(string _str)
    296. {
    297. var _path = GetTerracePath() + "/" + pathName + "/" + _str;
    298. return _path;
    299. }
    300. //获得不同平台的路径
    301. string GetTerracePath()
    302. {
    303. string sPath = Application.persistentDataPath;
    304. return sPath;
    305. }
    306. #endregion
    307. private static Dictionary<string, byte[]> s_assetDatas = new Dictionary<string, byte[]>();
    308. public static byte[] GetAssetData(string dllName)
    309. {
    310. return s_assetDatas[dllName];
    311. }
    312. private string GetWebRequestPath(string asset)
    313. {
    314. var path = $"{Application.streamingAssetsPath}/{asset}";
    315. if (!path.Contains("://"))
    316. {
    317. path = "file://" + path;
    318. }
    319. return path;
    320. }
    321. IEnumerator DownLoadAssets(Action onDownloadComplete)
    322. {
    323. var assets = new List<string>
    324. {
    325. "prefabs",
    326. "Assembly-CSharp.dll",
    327. }.Concat(AOTMetaAssemblyNames);
    328. foreach (var asset in assets)
    329. {
    330. string dllPath = GetWebRequestPath(asset);
    331. //这个地方改为读本地文件夹
    332. if(asset== "prefabs" || asset== "Assembly-CSharp.dll") dllPath = Path.Combine("file://" + GetTerracePath(), pathName + "/Dll/" + asset);
    333. //var dllPath = Path.Combine("file://" + GetTerracePath(), pathName + "/Dll/" + asset);
    334. Debug.Log($"start download asset:{dllPath}");
    335. UnityWebRequest www = UnityWebRequest.Get(dllPath);
    336. yield return www.SendWebRequest();
    337. #if UNITY_2020_1_OR_NEWER
    338. if (www.result != UnityWebRequest.Result.Success)
    339. {
    340. Debug.Log(www.error);
    341. }
    342. #else
    343. if (www.isHttpError || www.isNetworkError)
    344. {
    345. Debug.Log(www.error);
    346. }
    347. #endif
    348. else
    349. {
    350. // Or retrieve results as binary data
    351. byte[] assetData = www.downloadHandler.data;
    352. Debug.Log($"dll:{asset} size:{assetData.Length}");
    353. s_assetDatas[asset] = assetData;
    354. }
    355. }
    356. onDownloadComplete();
    357. }
    358. void StartGame()
    359. {
    360. LoadMetadataForAOTAssemblies();
    361. #if !UNITY_EDITOR
    362. var gameAss = System.Reflection.Assembly.Load(GetAssetData("Assembly-CSharp.dll"));
    363. #else
    364. var gameAss = AppDomain.CurrentDomain.GetAssemblies().First(assembly => assembly.GetName().Name == "Assembly-CSharp");
    365. #endif
    366. //这个地方必须要有个物体,提供给游戏作为入口
    367. AssetBundle prefabAb = AssetBundle.LoadFromMemory(GetAssetData("prefabs"));
    368. GameObject testPrefab = Instantiate(prefabAb.LoadAsset("HotUpdatePrefab.prefab"));
    369. }
    370. ///
    371. /// 为aot assembly加载原始metadata, 这个代码放aot或者热更新都行。
    372. /// 一旦加载后,如果AOT泛型函数对应native实现不存在,则自动替换为解释模式执行
    373. ///
    374. private static void LoadMetadataForAOTAssemblies()
    375. {
    376. // 可以加载任意aot assembly的对应的dll。但要求dll必须与unity build过程中生成的裁剪后的dll一致,而不能直接使用原始dll。
    377. // 我们在BuildProcessors里添加了处理代码,这些裁剪后的dll在打包时自动被复制到 {项目目录}/HybridCLRData/AssembliesPostIl2CppStrip/{Target} 目录。
    378. /// 注意,补充元数据是给AOT dll补充元数据,而不是给热更新dll补充元数据。
    379. /// 热更新dll不缺元数据,不需要补充,如果调用LoadMetadataForAOTAssembly会返回错误
    380. ///
    381. foreach (var aotDllName in AOTMetaAssemblyNames)
    382. {
    383. byte[] dllBytes = GetAssetData(aotDllName);
    384. // 加载assembly对应的dll,会自动为它hook。一旦aot泛型函数的native函数不存在,用解释器版本代码
    385. LoadImageErrorCode err = RuntimeApi.LoadMetadataForAOTAssembly(dllBytes);
    386. Debug.Log($"LoadMetadataForAOTAssembly:{aotDllName}. ret:{err}");
    387. }
    388. }
    389. }

      通过以上脚本,配合将热更资源放到对应服务器,我们可以实现代码的热更。

      接着在对应预制体身上绑定如下脚本,从而实现找到游戏中物体并正确启动的目的。

      

    1. using HybridCLR;
    2. using System.Collections;
    3. using System.Collections.Generic;
    4. using UnityEngine;
    5. ///
    6. /// 该脚本提供游戏运行初始化操作,必走
    7. ///
    8. public class LoginAwake : MonoBehaviour
    9. {
    10. // Start is called before the first frame update
    11. void Start()
    12. {
    13. Debug.Log("可以进行游戏里面的一些启动操作,从而实现加载");
    14. //这里可以进行游戏里面的一些启动操作,从而实现加载
    15. //GameObject.Find("Login").GetComponent().enabled = true;
    16. gameObject.AddComponent();
    17. Debug.Log("=======看到此条日志代表你成功运行了示例项目的热更新代码=======");
    18. gameObject.AddComponent();
    19. }
    20. // Update is called once per frame
    21. void Update()
    22. {
    23. }
    24. }

      其中CreateByCode是官方提供的测试脚本,Login是我们自己写的初始化脚本。这里需要注意的是,启动的脚本貌似必须得继承HybridCLR,不然可能因为解释器的原因不能正确执行(可能是因为解释器启动前,不支持两套脚本读取方式)

      接着就是我们的Login脚本,这里可以做我们自己的操作,比如说一些ab包热更之类。这里我并没有使用官方加载资源的方式,毕竟使用 HybridCLR只需要解决代码热更就好了。

    1. using System.Collections;
    2. using System.Collections.Generic;
    3. using UnityEngine;
    4. using UnityEngine.UI;
    5. public class Login : MonoBehaviour
    6. {
    7. public Text loginTxt;
    8. // Start is called before the first frame update
    9. void Start()
    10. {
    11. Debug.Log("启动加载");
    12. loginTxt = GameObject.Find("Login/Text").GetComponent();
    13. loginTxt.text = "启动:Http加载";
    14. }
    15. // Update is called once per frame
    16. void Update()
    17. {
    18. }
    19. }

      到目前为止,运行截图如下,代表我们已经成功实现了脚本热更:

    ~~~~~~~~~~~~~~~~~~~~~~~~~以下内容和ab包有关~~~~~~~~~~~~~~~~~~~~~~

      另外多闲聊几句,我们的热更资源一般是在分工程里运行的,ab打包工具用的是assetbundle browser,虽然这款软件的作者已经三年没更新了。

      博客采用的脚本热更方案在开发阶段基本不用管,打包的时候重新构建下就行,其实相对还比较方案吧,目前没有用到lua热更方案的项目可以考虑采用。

      有问题可以私信留言,大家共同交流进步。

  • 相关阅读:
    Python 全栈系列178 单主机使用Docker搭建Mongo分片式集群
    DNSLog原理及代码实现
    适合家电和消费类应用R7F101GEE4CNP、R7F101GEG4CNP、R7F101GEG3CNP、R7F101GEE3CNP新一代RL78通用微控制器
    Apache JMeter 和 Locust的对比
    图解Base64
    (DXE_DRIVER)PciHostBridge
    生产设备上的静电该如何处理?
    NettyのNIO&BIO原理解析
    Codeforces-1689 C: Infected Tree 【树形动态规划】
    【论文解读】Co-attention network with label embedding for text classification
  • 原文地址:https://blog.csdn.net/Tel17610887670/article/details/127518537