HybridCLR热更新实例项目及改造流程
热更新项目简单模板
示例项目仓库,结构简单流程易懂
https://github.com/Kerzhrua/HybridCLR_Addressable/tree/MyTest
重要流程代码:
using HybridCLR;
using System;
using System.Collections;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using GamePlay;
using UnityEngine;
using UnityEngine.AddressableAssets;
using UnityEngine.ResourceManagement.AsyncOperations;
using Object = UnityEngine.Object;namespace AOT
{/// <summary>/// 用于启动游戏,执行版本检查,版本更新,加载程序集,进入游戏/// 错误处理是用的打印日志,正式上线的话需要一个错误处理系统来给玩家显示错误信息/// </summary>public class GameLauncher : MonoBehaviour{public const string META_DATA_DLL_SEPARATOR = "!"; // 分割符号 区分元数据dllprivate readonly List<string> _gamePlayDependencyDlls = new List<string>() { }; // GamePlay程序集依赖的热更程序集,这些程序集要先于gameplay程序集加载,需要手动填写#region 编辑器赋值public UIVersionUpdate _versionUpdateUI; // 进度条ui#endregion#region 缓存private byte[] _dllBytes; // dll文件的字节数组private AddressableAssetManager addressableAssetManager = new AddressableAssetManager(); // 资源管理器private Coroutine _launchCoroutine; // 启动器协程private Dictionary<string, Assembly> _allHotUpdateAssemblies = new(); // 热更新程序集 程序集名称_程序集#endregionprivate bool enableHybridCLR = !Application.isEditor; // 是否开启HybridCLR,在编辑器下自动关闭private void Start(){_launchCoroutine = StartCoroutine(Launch());DontDestroyOnLoad(this);}private IEnumerator Launch(){Debug.Log("检查更新");yield return CheckUpdate(); // 检查更新Debug.Log("加载程序集");yield return LoadAssemblies(); // 加载程序集Debug.Log("跳转场景");yield return EnterGame(); // 跳转场景Debug.Log("创建物体还原脚本");var GameTestOp = Addressables.LoadAssetAsync<GameObject>("Assets/Prefabs/GameTest.prefab");yield return new WaitUntil(() => { return GameTestOp.Status == AsyncOperationStatus.Succeeded; });Instantiate(GameTestOp.Result);}/// <summary>/// 检查版本更新/// </summary>/// <returns></returns>private IEnumerator CheckUpdate(){yield return addressableAssetManager.CheckUpdate();if (addressableAssetManager.IsHasContentToDownload()){Debug.Log($"检测到存在内容更新,开始更新内容");Debug.Log($"展示进度ui追踪下载进程");yield return OpenVersionUpdateUI();Debug.Log($"开始下载新内容");yield return Download();}}//打开版本更新UIprivate IEnumerator OpenVersionUpdateUI(){_versionUpdateUI = FindObjectOfType<UIVersionUpdate>(true);if (_versionUpdateUI == null){Debug.LogError("cant find UIVersionUpdate");return null;}_versionUpdateUI.gameObject.SetActive(true);_versionUpdateUI.GetDownloadProgress = addressableAssetManager.GetDownloadProgress;return null;}//下载资源private IEnumerator Download(){yield return addressableAssetManager.DownloadAssets();_versionUpdateUI.GetDownloadProgress = null;}/// <summary>/// 加载程序集/// </summary>/// <returns></returns>private IEnumerator LoadAssemblies(){Debug.Log("为AOT补充元数据");yield return LoadMetadataForAOTAssemblies();Debug.Log("加载游戏程序集依赖的dll");yield return LoadGamePlayDependencyAssemblies();Debug.Log("加载游戏程序集");yield return LoadGamePlayAssemblies();Debug.Log("重新加载catalog");yield return addressableAssetManager.ReloadAddressableCatalog();}/// <summary>/// 补充元数据/// </summary>/// <returns></returns>private IEnumerator LoadMetadataForAOTAssemblies(){var mataDataDlls = GetMetaDataDllToLoad();foreach (var aotDllName in mataDataDlls){if(string.IsNullOrEmpty(aotDllName)) continue;var path = $"Assets/HotUpdateDlls/MetaDataDll/{aotDllName}.bytes";yield return ReadDllBytes(path);if (_dllBytes != null){var state = HybridCLR.RuntimeApi.LoadMetadataForAOTAssembly(_dllBytes, HomologousImageMode.SuperSet);Debug.Log($"加载元数据结果:{aotDllName}. 状态码:{state}");}}}/// <summary>/// 获得要加载的元数据dll 从生成的AOTGenericReferences获取/// 文件从HybridCLRData\AssembliesPostIl2CppStrip\{platform}下获取 注意添加.bytes后缀/// </summary>/// <returns></returns>private string[] GetMetaDataDllToLoad(){return new string[]{"System.Core.dll","System.dll","Unity.Addressables.dll","Unity.ResourceManager.dll","UnityEngine.CoreModule.dll","mscorlib.dll",};}//加载GamePlay依赖的第三方程序集private IEnumerator LoadGamePlayDependencyAssemblies(){foreach (var dllName in _gamePlayDependencyDlls){yield return LoadSingleHotUpdateAssembly(dllName);}}/// <summary>/// 加载GamePlay程序集/// 文件从HybridCLRData\HotUpdateDlls\{platform}获取 注意添加.bytes后缀/// </summary>/// <returns></returns>private IEnumerator LoadGamePlayAssemblies(){yield return LoadSingleHotUpdateAssembly("GamePlay.dll");}private IEnumerator EnterGame(){yield return addressableAssetManager.ChangeScene("Assets/Scenes/GameScenes/StartScene.unity");}#region 工具方法/// <summary>/// 读取动态链接库字节/// </summary>/// <param name="path"></param>private IEnumerator ReadDllBytes(string path){Debug.Log("读取dll数据:" + path);TextAsset dllText = null;yield return addressableAssetManager.LoadAssetCoroutine<TextAsset>(path, (text) =>{dllText = text;});if (dllText == null){Debug.LogError($"读取dll数据失败,路径:{path}");_dllBytes = null;}else{Debug.Log("读取dll数据成功:" + path);_dllBytes = dllText.bytes;}addressableAssetManager.UnloadAsset(dllText);}/// <summary>/// 加载程序集/// </summary>/// <param name="dllName"></param>/// <returns></returns>private IEnumerator LoadSingleHotUpdateAssembly(string dllName){var path = $"Assets/HotUpdateDlls/HotUpdateDll/{dllName}.bytes";yield return ReadDllBytes(path);if (_dllBytes != null){var assembly = Assembly.Load(_dllBytes);_allHotUpdateAssemblies.Add(assembly.FullName, assembly);Debug.Log($"加载程序集成功:{assembly.FullName}");}}/// <summary>/// 获取程序集/// </summary>/// <param name="assemblyName"></param>/// <returns></returns>private Assembly GetAssembly(string assemblyName){assemblyName = assemblyName.Replace(".dll", "");IEnumerable<Assembly> allAssemblies =enableHybridCLR ? _allHotUpdateAssemblies.Values : AppDomain.CurrentDomain.GetAssemblies();return allAssemblies.First(assembly => assembly.FullName.Contains(assemblyName));}#endregion}
}
更新资源代码:
using System;
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.AddressableAssets;
using UnityEngine.AddressableAssets.ResourceLocators;
using UnityEngine.ResourceManagement.AsyncOperations;
using UnityEngine.SceneManagement;
using UnityEngine.Serialization;namespace AOT
{/// <summary>/// Addressable的资源管理器(只用于启动游戏时更新)/// </summary>public class AddressableAssetManager{//记录在playerPres里的需要下载的catalogs的ID 记录这个数据是为了当下载过程被打断,下次登录可以根据这个信息继续下载const string DOWNLOAD_CATALOGS_ID = "DownloadCatalogIDs";#region 缓存private List<object> _KeysNeedToDownload = new(); // 需要下载的文件keys,key是用来做资源定位的,也就是资源的唯一标识//此对象里保存了需要下载的catalog,每次获取新的catalog会将此对象保存到手机上,如果在下载的过程中关闭了游戏,下次打开还能拿到catalog继续下载private DownloadContent _downloadContent = new();private AsyncOperationHandle _downloadOP; // 异步操作处理对象 用于异步下载#endregion[Serializable]private class DownloadContent // 需要下载的内容{public List<string> catalogIDs = new(); // 需要更新的catalog的id列表}/// <summary>/// 是否存在需要下载的更新内容/// </summary>/// <returns></returns>public bool IsHasContentToDownload(){if (_downloadContent != null && _downloadContent.catalogIDs != null &&_downloadContent.catalogIDs.Count > 0){return true;}return false;}/// <summary>/// 加载资源/// </summary>/// <param name="path"></param>/// <typeparam name="T"></typeparam>/// <returns></returns>public IEnumerator LoadAssetCoroutine<T>(string path, System.Action<T> onComplete){var op = Addressables.LoadAssetAsync<T>(path);if (!op.IsValid()){Debug.LogError($"Invalid Addressables path: {path}");onComplete?.Invoke(default);yield break;}// 等待异步加载完成yield return op;if (op.Status == AsyncOperationStatus.Succeeded){onComplete?.Invoke(op.Result);}else{Debug.LogError($"Failed to load asset at path: {path}, Error: {op.OperationException}");onComplete?.Invoke(default);}}/// <summary>/// 释放加载的资源/// </summary>/// <param name="asset"></param>public void UnloadAsset(UnityEngine.Object asset){if (asset != null)Addressables.Release(asset);}/// <summary>/// 检查addressable更新 更新catalog文件到最新版本,找出所有需要更新的资源唯一id/// </summary>/// <returns></returns>public IEnumerator CheckUpdate(){// 检查catalog需要更新的内容var checkUpdateOP = Addressables.CheckForCatalogUpdates(false);yield return checkUpdateOP; // 等待检查完成if (checkUpdateOP.Status == AsyncOperationStatus.Succeeded){#region 确保下载内容对象_downloadContent的下载内容正确性,主要考虑下载中断问题_downloadContent.catalogIDs = checkUpdateOP.Result; // 赋值到下载内容对象 这是需要更新的catalog的id列表,目前不知道怎么出现多个catalogif (IsHasContentToDownload()) // 存在需要更新的内容{Debug.Log("检测到存在新内容");//说明服务器上有新的资源,记录要下载的catalog值在playerprefs中,如果下载的过程中被打断,下次打开游戏使用该值还能继续下载var jsonStr = JsonUtility.ToJson(_downloadContent);PlayerPrefs.SetString(DOWNLOAD_CATALOGS_ID, jsonStr);PlayerPrefs.Save();}else // 不存在需要更新的内容 但这只是catalog与本地一致,并不代表资源更新完毕{if (PlayerPrefs.HasKey(DOWNLOAD_CATALOGS_ID)) // 如果上次下载内容中断 {//上一次的更新还没下载完Debug.Log("继续上一次的下载更新");var jsonStr = PlayerPrefs.GetString(DOWNLOAD_CATALOGS_ID); // 获取未下载完成的内容JsonUtility.FromJsonOverwrite(jsonStr, _downloadContent); // 覆盖到下载内容对象}else{//没有需要下载的内容Debug.Log("不存在需要下载的内容");}}#endregionif (IsHasContentToDownload()) // 有内容需要下载{Debug.Log("资源更新列表:" + JsonUtility.ToJson(_downloadContent));Debug.Log("根据更新列表进行下载更新");// 更新catalog,如果不提供catalog的id列表,会检查所有已加载的catalog以获取更新// 如果只调用Addressables.UpdateCatalogs,它会返回所有资源的索引,但是如果只更新对应的catalogId,就只返回那些需要更新的资源索引,以此避免全部重新下载AsyncOperationHandle<List<IResourceLocator>> updateCatalogOP = Addressables.UpdateCatalogs(_downloadContent.catalogIDs, false);// 等待更新完成 这样catalog已经确保是最新的了yield return updateCatalogOP; if (updateCatalogOP.Status == AsyncOperationStatus.Succeeded){// 更新所有需要更新的资源定位key_KeysNeedToDownload.Clear();foreach (var resourceLocator in updateCatalogOP.Result){_KeysNeedToDownload.AddRange(resourceLocator.Keys);}Debug.Log("需要更新的资源key:" + JsonUtility.ToJson(_KeysNeedToDownload));}else{Debug.LogError($"更新catalog失败:{updateCatalogOP.OperationException.Message}");}Addressables.Release(updateCatalogOP);}}else{Debug.LogError($"检查catalog更新失败:{checkUpdateOP.OperationException.Message}");}Addressables.Release(checkUpdateOP);//更新完catalog后重新加载一下Addressable的Catalogyield return ReloadAddressableCatalog();}/// <summary>/// 下载资源 根据需要更新资源的唯一id数组,计算需要下载的大小,/// </summary>/// <returns></returns>public IEnumerator DownloadAssets(){var downloadSizeOp = Addressables.GetDownloadSizeAsync((IEnumerable)_KeysNeedToDownload);yield return downloadSizeOp;Debug.Log($"download size:{downloadSizeOp.Result / (1024f * 1024f)}MB");if (downloadSizeOp.Result > 0) // 如果有需要下载的内容{Addressables.Release(downloadSizeOp);/* 开始下载用于合并请求结果的选项。如果键(A,B)映射到结果([1,2,4],[3,4,5])UseFirst(或None)获取第一个键的结果。--[1,2,4]Union获取每个键的结果,并收集与任何键匹配的项。--[1,2,3,4,5]Intersection获取每个关键字的结果,并收集与每个关键字匹配的项。--[4]*/_downloadOP = Addressables.DownloadDependenciesAsync((IEnumerable)_KeysNeedToDownload, Addressables.MergeMode.Union, false);yield return _downloadOP; // 等待下载完成 全部的if (_downloadOP.Status == AsyncOperationStatus.Succeeded)Debug.Log($"download finish!");elseDebug.LogError($"Download Failed! exception:{_downloadOP.OperationException.Message} \r\n {_downloadOP.OperationException.StackTrace}");Addressables.Release(_downloadOP);}//清除需要下载的内容Debug.Log($"delete key:{DOWNLOAD_CATALOGS_ID}");PlayerPrefs.DeleteKey(DOWNLOAD_CATALOGS_ID);}/// <summary>/// 获取下载进程/// </summary>/// <returns></returns>public UIVersionUpdate.DownloadInfo GetDownloadProgress(){if (!_downloadOP.IsValid())return default;var downloadStatus = _downloadOP.GetDownloadStatus();return new UIVersionUpdate.DownloadInfo(downloadStatus.Percent, downloadStatus.DownloadedBytes, downloadStatus.TotalBytes);}public IEnumerator ChangeScene(string sceneName){var op = Addressables.LoadSceneAsync(sceneName, LoadSceneMode.Single);// 等待完成yield return op;if (op.Status != AsyncOperationStatus.Succeeded){Debug.LogError($"加载场景失败:{op.OperationException.Message} \r\n {op.OperationException.StackTrace}");}}#region Other/// <summary>/// 重新加载catalog/// Addressable初始化时热更新代码所对应的ScriptableObject的类型会被识别为System.Object,需要在热更新dll加载完后重新加载一下Addressable的Catalog/// https://hybridclr.doc.code-philosophy.com/docs/help/commonerrors#%E4%BD%BF%E7%94%A8addressable%E8%BF%9B%E8%A1%8C%E7%83%AD%E6%9B%B4%E6%96%B0%E6%97%B6%E5%8A%A0%E8%BD%BD%E8%B5%84%E6%BA%90%E5%87%BA%E7%8E%B0-unityengineaddressableassetsinvlidkeyexception-exception-of-type-unityengineaddressableassetsinvalidkeyexception-was-thrown-no-asset-found-with-for-key-xxxx-%E5%BC%82%E5%B8%B8/// </summary>/// <returns></returns>public IEnumerator ReloadAddressableCatalog(){var op = Addressables.LoadContentCatalogAsync($"{Addressables.RuntimePath}/catalog.json");// 等待加载完成yield return op;if (op.Status != AsyncOperationStatus.Succeeded){Debug.LogError($"加载catalog失败:{op.OperationException.Message} \r\n {op.OperationException.StackTrace}");}}#endregion}
}
如何改造一个现有项目支持热更?
将游戏的热更逻辑都放在一个程序集中,挂上需要的引用,处理好无报错
创建一个初始Gamelauncher场景,仅包含更新资源和加载dll相关的代码,这些代码在Assembly-CSharp中
当流程处理完即dll文件加载完成后跳转场景到游戏加载场景Load,注意在Load场景中什么都不要放,当场景加载完成后通过实例化预制体的方式来还原脚本,即把项目原本的Load场景的脚本全部放到一个预制体里加载还原。
游戏加载完成后根据需求是否还要切换场景,如还要切到Main场景,切换流程和切换到Load场景一致,Main场景中也什么都没有,在加载完成后再实例化预制体以还原脚本。
当这些处理完成后,可以开始remote打包流程。
热更的Generate All执行流程如下:
// 构建目标平台
BuildTarget target = EditorUserBuildSettings.activeBuildTarget;
// 编译目标平台的dll文件
CompileDllCommand.CompileDll(target);
// 生成必要的 IL2CPP 头文件和定义,扩展 IL2CPP 以支持解释执行
Il2CppDefGeneratorCommand.GenerateIl2CppDef();
// 生成 Link.xml 文件,避免热更新代码因裁剪而无法运行
LinkGeneratorCommand.GenerateLinkXml(target);
// 生成裁剪后的AOT dll 在打包过程中,Unity 会对 AOT 程序集进行裁剪(Strip),移除未使用的代码。AOTDlls 会保存这些裁剪后的 DLL,用于补充元数据(如泛型实例化)
StripAOTDllCommand.GenerateStripedAOTDlls(target);
// 桥接函数生成依赖于AOT dll,必须保证已经build过,生成AOT dll
// 在 AOT 代码和解释执行代码之间建立桥接,使得热更新代码和AOT代码可以互相调用
MethodBridgeGeneratorCommand.GenerateMethodBridge(target);
// 在 AOT 环境下,native 回调 C# 委托需要固定的函数指针,而热更新代码的动态性会导致回调失败。
// 故要为热更新代码中的委托(Delegate)生成反向 P/Invoke 包装器,使得 C# 委托可以被 native 代码(如 Unity 引擎或 C++ 插件)回调
ReversePInvokeWrapperGeneratorCommand.GenerateReversePInvokeWrapper(target);
// 生成 AOT 泛型引用,以解决 AOT 泛型限制问题,让热更新代码可以使用未在 AOT 中实例化的泛型类或方法(如 List<MyHotUpdateType>)
AOTReferenceGeneratorCommand.GenerateAOTGenericReference(target);
在webgl平台下打包有额外的步骤
https://hybridclr.doc.code-philosophy.com/docs/basic/buildwebgl
以管理员权限打开命令行窗口,这个操作不同操作系统版本不一样,请酌情处理。在Win11下为在开始菜单上右键,选中终端管理员菜单项。
运行 cd /d {editor_install_dir}/Editor/Data/il2cpp, 切换目录到安装目录的il2cpp目录
运行ren libil2cpp libil2cpp-origin 将原始libil2cpp改名为libil2cpp-origin
运行 mklink /D libil2cpp "{project}/HybridCLRData/LocalIl2CppData-{platform}/il2cpp/libil2cpp" 建立Editor目录的libil2cpp到本地libil2cpp目录的符号引用
示例:mklink /D libil2cpp "E:\Unity\UnityProjects\DiveVsSlimeLFS/HybridCLRData/LocalIl2CppData-WindowsEditor/il2cpp/libil2cpp"
注意!!!在热更下RuntimeInitializeOnLoadMethod未被支持,故如果想实现类似效果可获取程序集通过反射实现。