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 = "!";  //  分割符号  区分元数据dll
        private 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();  //  热更新程序集  程序集名称_程序集
        #endregion
        
        private 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();
            }
            
        }

        //打开版本更新UI
        private 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列表,目前不知道怎么出现多个catalog
                if (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("不存在需要下载的内容");
                    }
                }
                #endregion
                
                if (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的Catalog
            yield 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!");
                else
                    Debug.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未被支持,故如果想实现类似效果可获取程序集通过反射实现。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值