华佗热更新模块

HotfixManager.cs 13KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389
  1. using TFramework;
  2. using System.Collections;
  3. using System.Collections.Generic;
  4. using UnityEngine;
  5. using System;
  6. using UnityEngine.Networking;
  7. using Cysharp.Threading.Tasks;
  8. using Newtonsoft.Json.Serialization;
  9. using System.Threading.Tasks;
  10. using System.IO;
  11. using System.Reflection;
  12. #if HybridCLR
  13. using HybridCLR;
  14. #endif
  15. namespace TModule.Runtime {
  16. public class HotfixManager : BaseManager
  17. {
  18. /// <summary>
  19. /// 是否开启热更
  20. /// </summary>
  21. [SerializeField] bool isHotfix;
  22. /// <summary>
  23. /// 资源下载地址
  24. /// </summary>
  25. [SerializeField] string resServerUrl;
  26. /// <summary>
  27. /// 资源加载模式
  28. /// </summary>
  29. [SerializeField] UpdateMode loadMode;
  30. /// <summary>
  31. /// 热更新dll名称
  32. /// </summary>
  33. [SerializeField] string hotfixDllName;
  34. /// <summary>
  35. /// 热更新启动类
  36. /// </summary>
  37. [SerializeField] string hotfixClass;
  38. /// <summary>
  39. /// 超时时间
  40. /// </summary>
  41. private const int DOWNLOADTIMEOUT = 5;
  42. /// <summary>
  43. /// 获取平台
  44. /// </summary>
  45. public string GetPlatform
  46. {
  47. get
  48. {
  49. #if UNITY_ANDROID
  50. return "Android";
  51. #elif UNITY_IOS
  52. return "IOS";
  53. #elif UNITY_STANDALONE_OSX
  54. return "Mac";
  55. #elif UNITY_STANDALONE_WIN
  56. return "StandaloneWindows64";
  57. #elif UNITY_WEBGL
  58. return "WebGL";
  59. #else
  60. return Application.platform.ToString();
  61. #endif
  62. }
  63. }
  64. public long TotalSize
  65. {
  66. get
  67. {
  68. long szie = 0;
  69. _NeedDownloadResList.ForEach(p => szie += (long)p.m_size);
  70. return szie;
  71. }
  72. }
  73. TAction<float,long,long> onCheckUpdataEvent;
  74. TAction<TAction, long> onCheckOverEvent;
  75. TAction<string> onErrorEvent;
  76. /// <summary>
  77. /// 检查进度
  78. /// </summary>
  79. private float _checkProgress;
  80. private string _loaclVersionPath;
  81. private List<VersionDataEntity> _NeedDownloadResList = new List<VersionDataEntity>();
  82. private List<VersionDataEntity> _LocalResList = new List<VersionDataEntity>();
  83. private long completeTotalCount = 0;
  84. /// <summary>
  85. ///本地资源存放路径
  86. /// </summary>
  87. string localResFilePath;
  88. /// <summary>
  89. /// 版本文件名称
  90. /// </summary>
  91. string versionFileName = "VersionFile.json";
  92. /// <summary>
  93. /// 热更入口
  94. /// </summary>
  95. /// <param name="onCheckUpdata">更新事件 return 返回检查更新进度(仅进度)/更新进度(进度,总大小,已下载大小)</param>
  96. /// <param name="onCheckOver">检查完成 return 是否有更新 更新资源大小</param>
  97. /// <param name="onError">热更出错事件</param>
  98. public void Startup(TAction<float,long,long> onUpdata, TAction<TAction, long> onCheckOver,TAction<string> onError=null)
  99. {
  100. localResFilePath = Application.persistentDataPath + "/";
  101. resServerUrl += '/' + GetPlatform;
  102. AssetConfig.AssetBundleManifestName = GetPlatform;
  103. #if !UNITY_EDITOR
  104. AssetConfig.RuntimeMode = UpdateMode.Bulid;
  105. #else
  106. AssetConfig.RuntimeMode = loadMode;
  107. #endif
  108. onCheckUpdataEvent = onUpdata;
  109. onCheckOverEvent = onCheckOver;
  110. onErrorEvent = onError;
  111. if (isHotfix)
  112. {
  113. if (AssetConfig.RuntimeMode == UpdateMode.Bulid)
  114. {
  115. CheckUpdata();
  116. }
  117. else
  118. {
  119. EnterAPP();
  120. onCheckOverEvent(null, 0);
  121. }
  122. }
  123. else
  124. {
  125. EnterAPP();
  126. onCheckOverEvent(null, 0);
  127. }
  128. }
  129. /// <summary>
  130. /// 检查更新
  131. /// </summary>
  132. private async UniTask CheckUpdata()
  133. {
  134. if (string.IsNullOrEmpty(resServerUrl))
  135. {
  136. Log.Error("未指定资源服务器地址");
  137. return;
  138. }
  139. string strVersionPath = resServerUrl + "/" + versionFileName;
  140. List<VersionDataEntity> versionDatas = await DowloadVersion(strVersionPath);
  141. if(versionDatas!=null)
  142. {
  143. OnInitVersionCallBack(versionDatas);
  144. if (_NeedDownloadResList.Count > 0)
  145. {
  146. onCheckOverEvent?.Invoke(DownloadFiles, TotalSize);
  147. }
  148. else
  149. {
  150. EnterAPP();
  151. onCheckOverEvent?.Invoke(null, 0);
  152. }
  153. }
  154. }
  155. public async void EnterAPP()
  156. {
  157. await UniTask.DelayFrame(2);
  158. Assembly Csharp;
  159. if(string.IsNullOrEmpty(hotfixDllName))
  160. {
  161. Log.Error("开启了热更新,但未指定热更程序集,程序终止");
  162. #if UNITY_EDITOR
  163. UnityEditor.EditorApplication.isPlaying = false;
  164. #endif
  165. return;
  166. }
  167. if(AssetConfig.RuntimeMode == UpdateMode.Editor||!isHotfix)
  168. {
  169. Csharp = AppDomain.CurrentDomain.Load(hotfixDllName);
  170. }
  171. else
  172. {
  173. LoadMetadataForAOTAssemblies();
  174. byte[] assemblyDll = FileOperations.SafeReadAllBytes(AssetConfig.AssetBundleRootPath + $"{hotfixDllName}.dll.bytes");
  175. if (assemblyDll == null)
  176. {
  177. Log.Error($"未加载到{hotfixDllName}.dll.bytes文件,请检查文件是否存在");
  178. return;
  179. }
  180. Csharp = Assembly.Load(assemblyDll);
  181. }
  182. if (string.IsNullOrEmpty(hotfixClass))
  183. {
  184. Log.Error("开启了热更新,但未指定热更入口程序");
  185. return;
  186. }
  187. object hclass = Csharp.CreateInstance(hotfixClass);
  188. if (hclass != null && (hclass as IHotfixStartup) != null)
  189. {
  190. (hclass as IHotfixStartup).Run();
  191. }
  192. else
  193. Log.Error($"热更入口程序{hotfixClass}未找到,{hotfixClass}需要继承自IHotfixStartup,请检查脚本");
  194. }
  195. /// <summary>
  196. /// 为aot assembly加载原始metadata, 这个代码放aot或者热更新都行。
  197. /// 一旦加载后,如果AOT泛型函数对应native实现不存在,则自动替换为解释模式执行
  198. /// </summary>
  199. private static void LoadMetadataForAOTAssemblies()
  200. {
  201. List<string> aotMetaAssemblyFiles = Resources.Load<HotfixConfig>("HotfixUpdate/热更配置表").m_hotfixUpdateAotDlls;
  202. /// 注意,补充元数据是给AOT dll补充元数据,而不是给热更新dll补充元数据。
  203. /// 热更新dll不缺元数据,不需要补充,如果调用LoadMetadataForAOTAssembly会返回错误
  204. ///
  205. #if HybridCLR
  206. HomologousImageMode mode = HomologousImageMode.SuperSet;
  207. foreach (var aotDllName in aotMetaAssemblyFiles)
  208. {
  209. byte[] dllBytes = FileOperations.SafeReadAllBytes(AssetConfig.AssetBundleRootPath + aotDllName + ".bytes");
  210. Debug.Log($"加载{aotDllName}******{dllBytes}");
  211. // 加载assembly对应的dll,会自动为它hook。一旦aot泛型函数的native函数不存在,用解释器版本代码
  212. LoadImageErrorCode err = RuntimeApi.LoadMetadataForAOTAssembly(dllBytes, mode);
  213. if (err != LoadImageErrorCode.OK)
  214. {
  215. Debug.LogError($"Error occurs when loadMetadataForAOT: {err}");
  216. }
  217. else
  218. Debug.Log($"LoadMetadataForAOTAssembly:{aotDllName}. mode:{mode} ret:{err}");
  219. }
  220. #else
  221. Log.Error("当前非热更环境!请先安装HybridCLR");
  222. #endif
  223. }
  224. /// <summary>
  225. /// 下载版本文件
  226. /// </summary>
  227. /// <param name="url"></param>
  228. /// <param name="OnDownLoadOver"></param>
  229. /// <returns></returns>
  230. private async UniTask<List<VersionDataEntity>> DowloadVersion(string url)
  231. {
  232. UnityWebRequest www = UnityWebRequest.Get(url);
  233. www.timeout = DOWNLOADTIMEOUT;
  234. await www.SendWebRequest();
  235. while (!www.isDone)
  236. {
  237. _checkProgress = www.downloadProgress / 2 * 0.1f;
  238. onCheckUpdataEvent?.Invoke(_checkProgress,0,0);
  239. }
  240. if (www.result != UnityWebRequest.Result.ProtocolError && www.result != UnityWebRequest.Result.ConnectionError)
  241. {
  242. string content = www.downloadHandler.text;
  243. return content.JsonStrToObject<List<VersionDataEntity>>();
  244. }
  245. else
  246. {
  247. Debug.Log("下载失败:" + www.error);
  248. onErrorEvent?.Invoke(www.error);
  249. return null;
  250. }
  251. }
  252. /// <summary>
  253. /// 对比版本文件
  254. /// </summary>
  255. /// <param name="arg0"></param>
  256. private void OnInitVersionCallBack(List<VersionDataEntity> arg0)
  257. {
  258. _loaclVersionPath = localResFilePath + versionFileName;
  259. if (FileOperations.FileExists(_loaclVersionPath))
  260. {
  261. List<VersionDataEntity> clienData = FileOperations.ReadJsonData<List<VersionDataEntity>>(_loaclVersionPath);
  262. _LocalResList.AddRange(clienData);
  263. foreach (var item in arg0)
  264. {
  265. var progress = 1.0f / arg0.Count * 0.9f + _checkProgress;
  266. progress = progress >= 1 ? 1 : progress;
  267. onCheckUpdataEvent?.Invoke(progress,0,0);
  268. VersionDataEntity dataEntity = clienData.Find(p => p.m_fullName == item.m_fullName);
  269. //已有资源
  270. if (dataEntity != null)
  271. {
  272. //对比MD5
  273. if (dataEntity.m_md5 != item.m_md5)
  274. _NeedDownloadResList.Add(item);
  275. }
  276. else
  277. _NeedDownloadResList.Add(item);//新资源
  278. }
  279. }
  280. else
  281. arg0.ForEach(p =>
  282. {
  283. _NeedDownloadResList.Add(p);
  284. var progress = 1.0f / arg0.Count * 0.9f + _checkProgress;
  285. progress = progress >= 1 ? 1 : progress;
  286. onCheckUpdataEvent?.Invoke(progress,0,0);
  287. });
  288. }
  289. /// <summary>
  290. /// 更新版本文件
  291. /// </summary>
  292. /// <param name="entity"></param>
  293. private void ModifyLocaData(VersionDataEntity entity)
  294. {
  295. if (_LocalResList == null) return;
  296. bool isExists = false;
  297. for (int i = 0; i < _LocalResList.Count; i++)
  298. {
  299. if (_LocalResList[i].m_fullName.Equals(entity.m_fullName, StringComparison.CurrentCultureIgnoreCase))
  300. {
  301. _LocalResList[i].m_md5 = entity.m_md5;
  302. _LocalResList[i].m_fullName = entity.m_fullName;
  303. _LocalResList[i].m_size = entity.m_size;
  304. isExists = true;
  305. break;
  306. }
  307. }
  308. if (!isExists)
  309. {
  310. _LocalResList.Add(entity);
  311. }
  312. SaveLoaclVersion();
  313. }
  314. /// <summary>
  315. /// 保存版本文件
  316. /// </summary>
  317. private void SaveLoaclVersion() => FileOperations.WriteJsonData(_LocalResList, _loaclVersionPath);
  318. /// <summary>
  319. /// 分配下载任务并开始下载
  320. /// </summary>
  321. /// <param name="downloadList"></param>
  322. private void DownloadFiles()
  323. {
  324. _checkProgress = 0;
  325. foreach (var versionData in _NeedDownloadResList)
  326. {
  327. string savePath = Path.Combine(localResFilePath, versionData.m_fullName);
  328. savePath = savePath.Replace("\\", "/");
  329. string url = resServerUrl+"/"+ versionData.m_fullName;
  330. if (File.Exists(savePath))
  331. File.Delete(savePath);
  332. Main.GetMagr<DownloadManager>().StartDownload(url, savePath, OnDownloading, () =>
  333. {
  334. completeTotalCount += (long)versionData.m_size;
  335. ModifyLocaData(versionData);
  336. if (completeTotalCount >= TotalSize)
  337. {
  338. EnterAPP();
  339. _checkProgress = 1;
  340. onCheckUpdataEvent?.Invoke(_checkProgress, TotalSize, completeTotalCount);
  341. }
  342. }, err => onErrorEvent?.Invoke(err));
  343. }
  344. }
  345. long t = 0;
  346. private void OnDownloading(float prossage,long totaSize,long size)
  347. {
  348. _checkProgress = (completeTotalCount*1.0f)/TotalSize + 1/TotalSize*(size*1.0f / totaSize);
  349. onCheckUpdataEvent?.Invoke(_checkProgress, TotalSize, completeTotalCount);
  350. }
  351. }
  352. }