- A+
尝试最近很火的HybridCLR,看了文档,有推荐使用Unity版本的,为了少踩坑,所以采用了2021.3.1f1c1版本的Unity。HybridCLR版本用的是最新版本。
将原来的lua热更版本,复制一份到新的项目中去,移除Xlua和lua相关的代码和内容。保留Addressable进行Bundler的管理,后面的热更dll将会复制到LevelAsset标记地址的目录下,打包成Bundler,然后就会像普通的其他资源一样进行更新。
一、安装
下载unity的时候如果要在Windows下跑,记得勾选il2cpp。打开ProjectSettings,把ScriptingBackend修改未IL2CPP,ApiCompatibilityLevel修改为.Net Framework。最后取消勾选Use incremental GC。
打开Package Manager,点击通过git url安装Package,链接为:“https://gitee.com/focus-creative-games/hybridclr_unity.git”。Package安装完成后,会有一个菜单叫HybridCLR,点击菜单下的Installer,插件会自动识别Unity版本,点击安装即可。插件会对对应版本的il2cpp进行修改,放到项目目录下的HybridCLRData目录下。好了就这么简单的就完成安装了。
二、区分AOT+Interpreter
AOT部分的代码是无法进行热更新的,Interpreter部分是可以热更新的,所以除了必要的东西放在AOT外,其他的都放在Interpreter进行热更新。什么是必要的东西,首先就是热更新逻辑的代码和热更新逻辑驱动的入口代码。项目中使用的是Addressable进行资源热更新的,所以这部分是需要放在AOT的,还有的就是进行热更新检测的入口逻辑,剩余的就可以不需要放在这里了。我这里确定的两个热更新程序集是Frame的程序集和Assembly-Csharp程序集。
三、AOT部分的Main入口
在以前使用xlua的时候是创建虚拟机后驱动lua侧的启动入口方法,在lua中进行热更新的,这样其实热更新逻辑也是可以热更的。但是用HybridCLR时,需要把热更新的dll在启动的时候加载,如果热更逻辑放在热更新的程序集中就必须先加载热更新的程序集,更新后又无法卸载原先加载的,这样使用的还是原来旧的逻辑。所以还是把热更新的逻辑写在AOT部分,毕竟这部分的东西一般来说不会修改。(注意,DHE好像是可以进行卸载的)
总的来说就是需要在Main中写一个热更新逻辑和dll加载逻辑,最后驱动热更新逻辑入口就可以了。同样的,初始化一下Addressable,然后如果是编辑器的话就直接驱动入口就可以了。如果不是的话就像lua逻辑一样检测版本号,设置加载资源地址,更新资源等。在这个完成后,需要做一个补充元数据的操作,unity是有剪裁代码的机制的,对于AOT代码转化成c++后就会丢失一些泛型的元数据,需要告诉HybridCLR,这样热更新代码才能使用新的泛型创建的类型。最后就是把热更新的dll加载了,这里需要注意顺序问题。
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 270 |
using UnityEngine; using System.IO; using Manager; using UnityEngine.Networking; using System.Collections; using System.Collections.Generic; using Addressable; using System.Reflection; using System; using System.Linq; using HybridCLR; public class Main : MonoBehaviour { private StartUpLoadingWindow m_LoadingWindow; private OperationData m_ProgressOperationData = null; public static Assembly FrameAssembly; public static Assembly CSAssembly; public static List<string> aotDlls = new List<string> { "StartUp", "System.Core", "System", "UnityEngine.AndroidJNIModule", "UnityEngine.CoreModule", "mscorlib", }; private void Start() { m_LoadingWindow = transform.GetComponentInChildren<StartUpLoadingWindow>(); m_LoadingWindow.ResetValue(); Init(); } private void Init() { m_LoadingWindow.SetMes("初始化中..."); AssetBundleManager.Instance.Init(); AddressableManager.Instance.InitAddressable(AddressableInitFinish); } void AddressableInitFinish() { if (Application.isEditor) { Boot(null); } else if (Config.IgnoreUpdate) { LoadDlls(); } else { StartCoroutine(CheckVersion()); } } IEnumerator CheckVersion() { m_LoadingWindow.SetMes("正在检测资源版本..."); VersionData local_version = null; //检测本地更新的版本 if (File.Exists(StartUpConfig.LocalSaveVersionPath)) { var json = File.ReadAllText(StartUpConfig.LocalSaveVersionPath); local_version = JsonUtility.FromJson<VersionData>(json); } //获取本地版本 var in_json = Resources.Load<TextAsset>(StartUpConfig.LocalVersionPath); var in_local_version = JsonUtility.FromJson<VersionData>(in_json.text); if (null == local_version) { local_version = in_local_version; } else { //检查app版本和资源版本是否对得上,对不上要清空资源 if (local_version.AppVer != in_local_version.AppVer) { if (Directory.Exists(AssetBundleManager.Instance.GetBundleCachePath())) { Directory.Delete(AssetBundleManager.Instance.GetBundleCachePath()); } if (Directory.Exists(AssetBundleManager.Instance.GetCatalogPath())) { Directory.Delete(AssetBundleManager.Instance.GetCatalogPath()); } } } //获取远程版本信息 UnityWebRequest request = UnityWebRequest.Get(StartUpConfig.OssRemoteVersionUrl); yield return request.SendWebRequest(); if (!string.IsNullOrEmpty(request.error)) { //展示提示 yield break; } var remote_json = request.downloadHandler.text; Debug.Log(remote_json); VersionData remote_version = JsonUtility.FromJson<VersionData>(remote_json); var local_app_ver = VersionUtil.ParseVersion(local_version.AppVer); var remote_app_ver = VersionUtil.ParseVersion(remote_version.AppVer); bool is_need_update = false; for (int i = 0; i < local_app_ver.Length; i++) { if (remote_app_ver[i] > local_app_ver[i]) { is_need_update = true; break; } } if (is_need_update) { //整包热更新 Application.OpenURL(StartUpConfig.OfficialAddress); yield break; } var local_res_ver = VersionUtil.ParseVersion(local_version.ResVer); var remote_res_ver = VersionUtil.ParseVersion(remote_version.ResVer); for (int i = 0; i < local_app_ver.Length; i++) { if (remote_res_ver[i] != local_res_ver[i])//回退机制 { is_need_update = true; break; } } if (is_need_update) { AddressableManager.Instance.SetAddressableRemoteResCdnUrl(string.Format("{0}{1}/{2}", StartUpConfig.OssDownloadBaseUrl, local_version.AppVer, remote_version.ResVer, VersionUtil.GetPlatformStr(Application.platform))); StartCoroutine(CheckUpdate(remote_version)); } else { LoadDlls(); } } IEnumerator CheckUpdate(VersionData remote_version) { m_LoadingWindow.SetMes("正在检测资源版本更新..."); var opdata = AddressableManager.Instance.CheckForCatalogUpdates(); yield return opdata; var catlogs = opdata.GetAsset() as List<string>; opdata = AddressableManager.Instance.UpdateCatalogs(catlogs); yield return opdata; opdata = AddressableManager.Instance.GetCheckContentList("default"); yield return opdata; //获取更新列表和更新大小 AddressableManager.Instance.GetUpdateContentList(); var size = AddressableManager.Instance.GetDownloadSize(); if (size > 0) { //显示进度 m_LoadingWindow.SetMes("正在下载资源更新,总大小为:" + Mathf.FloorToInt(size / 1024) + "kb"); m_ProgressOperationData = AddressableManager.Instance.DownloadDependenciesAsync(); yield return m_ProgressOperationData; m_ProgressOperationData = null; } //保存最新版本 File.WriteAllText(StartUpConfig.LocalSaveVersionPath, JsonUtility.ToJson(remote_version)); LoadDlls(); } private void Update() { if (m_ProgressOperationData == null) return; m_LoadingWindow.SetProgress(m_ProgressOperationData.progress); } void LoadDlls() { if (Config.CloseHybridCLR) { Boot(null); } else { StartCoroutine(LoadMetadataForAOTAssemblies()); } } private IEnumerator LoadMetadataForAOTAssemblies() { string path = Path.Combine(Application.streamingAssetsPath, Config.AotMetadataDir) + "/"; #if UNITY_ANDROID && !UNITY_EDITOR path = path; #elif UNITY_IPHONE && !UNITY_EDITOR path ="file://" + path; #elif UNITY_STANDALONE_WIN || UNITY_EDITOR path = "file://" + path; #endif /// 注意,补充元数据是给AOT dll补充元数据,而不是给热更新dll补充元数据。 /// 热更新dll不缺元数据,不需要补充,如果调用LoadMetadataForAOTAssembly会返回错误 /// HomologousImageMode mode = HomologousImageMode.SuperSet; string KEY = AES.GenDllKey(); foreach (var aotDllName in aotDlls) { var request = UnityWebRequest.Get(path + aotDllName + ".bytes"); yield return request.SendWebRequest(); // 加载assembly对应的dll,会自动为它hook。一旦aot泛型函数的native函数不存在,用解释器版本代码 LoadImageErrorCode err = RuntimeApi.LoadMetadataForAOTAssembly(CompressUtil.UnCompress( AES.AESDecrypt( request.downloadHandler.data, KEY)), mode); request.Dispose(); Debug.Log($"LoadMetadataForAOTAssembly:{aotDllName}. mode:{mode} ret:{err}"); } LoadHotDll(); } void LoadHotDll() { //热更新程序集加载 AddressableManager.Instance.LoadAllHotDll(Config.HotUpdateDir, Boot); } private void Boot(Dictionary<string, TextAsset> allDlls) { if (Config.CloseHybridCLR || Application.isEditor) { FrameAssembly = AppDomain.CurrentDomain.GetAssemblies().First(assembly => assembly.GetName().Name == "FrameAssets"); CSAssembly = AppDomain.CurrentDomain.GetAssemblies().First(assembly => assembly.GetName().Name == "Assembly-CSharp"); } else { string KEY = AES.GenDllKey(); //加载可能依赖的库 foreach (var dll in allDlls) { if (dll.Key != "HotConfigAsset/FrameAssets.bytes" && dll.Key != "HotConfigAsset/LevelAssets.bytes") { Assembly.Load(CompressUtil.UnCompress( AES.AESDecrypt( dll.Value.bytes, KEY))); } } //再加载业务上的框架和业务逻辑 FrameAssembly = Assembly.Load(CompressUtil.UnCompress( AES.AESDecrypt( allDlls["HotConfigAsset/FrameAssets.bytes"].bytes, KEY))); CSAssembly = Assembly.Load(CompressUtil.UnCompress( AES.AESDecrypt( allDlls["HotConfigAsset/LevelAssets.bytes"].bytes, KEY))); } var startType = CSAssembly.GetType("StartUp"); var startMethod = startType.GetMethod("Boot"); startMethod.Invoke(null, null); } } |
这里的dll加载都是用Addressable的lable进行批量加载的,下面打包的部分会说明是怎么标记这些lable的。
四、打包
不同平台的打包,大概的方法都是一样的,首先在打包前需要做一些准备后再调用打包的方法。需要准备的就是HybridCLR中需要的,已经提供好给我们了,调用PrebuildCommand.GenerateAll()就可以了,然后编译程序集,把热更新的程序集和补充元数据的程序集复制到Addressable的地址标记目录,对程序集做一个lable的标志方便批量加载。最后标记地址打包Bundle准备操作就算完成了。
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 |
public static void PackageApk() { PreparePackage(); BuildPipeline.BuildPlayer(GetBuildScenes(), $"Build/Android/{Config.AppName}-{GetAppVer()}.apk", BuildTarget.Android, BuildOptions.CompressWithLz4HC); } public static void PackageStandaloneWindows64() { PreparePackage(); BuildPipeline.BuildPlayer(GetBuildScenes(), $"Build/StandaloneWindows64/{Config.AppName}-{GetAppVer()}/{Config.AppName}.exe", BuildTarget.StandaloneWindows64, BuildOptions.CompressWithLz4HC); } public static void PackageIos() { PreparePackage(); BuildPipeline.BuildPlayer(GetBuildScenes(), $"Build/iPhone/{Config.AppName}-{GetAppVer()}", BuildTarget.iOS, BuildOptions.CompressWithLz4HC); } public static void PreparePackage() { Debug.Log("PreparePackage"); if (FrameConfig.Debug) { CheckImportSRDebuger(); } else { CheckRemoveSRDebuger(); } PrebuildCommand.GenerateAll();//HybridCLR的准备,防剪裁、桥接方法等 PackageRes(); AssetDatabase.Refresh(); } public static void PackageRes() { AotHotUpdateDll.BuildAndCopyABAOTHotUpdateDlls();//编译和复制程序集到地址标记目录 AASUtility.CleanPlayerContent(); PackageAtlas.PackAtlas(); AssetDatabase.Refresh(); AddressableTool.MarkAssets(); AddressableTool.SetAllGroupToRemote(); AddressableTool.BuildPlayerContent(); } |
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 |
using HybridCLR.Editor; using HybridCLR.Editor.Commands; using System.Collections.Generic; using System.IO; using UnityEditor; using UnityEngine; public class AotHotUpdateDll { public static void CompileDll() { CompileDllCommand.CompileDllActiveBuildTarget(); } public static void BuildAndCopyABAOTHotUpdateDlls() { CompileDllCommand.CompileDllActiveBuildTarget(); CopyHotUpdateAssembliesToAssetDataPath(); CopyAOTAssembliesToStreamingAssetsPath(); } public static void CopyHotUpdateAssembliesToAssetDataPath() { var target = EditorUserBuildSettings.activeBuildTarget; string hotfixDllSrcDir = SettingsUtil.GetHotUpdateDllsOutputDirByTarget(target); string hotfixAssembliesDstDir = string.Format("{0}/{1}/{2}",Application.dataPath,Config.AssetDataPath,Config.HotUpdateDir); if (Directory.Exists(hotfixAssembliesDstDir)) { Directory.Delete(hotfixAssembliesDstDir,true); } Directory.CreateDirectory(hotfixAssembliesDstDir); foreach (var dll in SettingsUtil.HotUpdateAssemblyFilesExcludePreserved) { string dllPath = $"{hotfixDllSrcDir}/{dll}"; string dllBytesPath = $"{hotfixAssembliesDstDir}/{dll.Replace(".dll","").Replace("Assembly-CSharp", "LevelAssets")}.bytes"; File.WriteAllBytes(dllBytesPath, AES.AESEncrypt(CompressUtil.Compress(File.ReadAllBytes(dllPath)), AES.GenDllKey())); Debug.Log($"[CopyHotUpdateAssembliesToStreamingAssets] copy hotfix dll {dllPath} -> {dllBytesPath}"); } AssetDatabase.Refresh(); } public static void CopyAOTAssembliesToStreamingAssetsPath() { var target = EditorUserBuildSettings.activeBuildTarget; string aotAssembliesSrcDir = SettingsUtil.GetAssembliesPostIl2CppStripDir(target); string aotAssembliesDstDir = string.Format("{0}/{1}", Application.streamingAssetsPath, Config.AotMetadataDir); if (Directory.Exists(aotAssembliesDstDir)) { Directory.Delete(aotAssembliesDstDir, true); } Directory.CreateDirectory(aotAssembliesDstDir); foreach (var dll in Main.aotDlls) { string srcDllPath = $"{aotAssembliesSrcDir}/{dll}.dll"; if (!File.Exists(srcDllPath)) { Debug.LogError($"ab中添加AOT补充元数据dll:{srcDllPath} 时发生错误,文件不存在。裁剪后的AOT dll在BuildPlayer时才能生成,因此需要你先构建一次游戏App后再打包。"); continue; } string dllBytesPath = $"{aotAssembliesDstDir}/{dll}.bytes"; File.WriteAllBytes(dllBytesPath, AES.AESEncrypt(CompressUtil.Compress(File.ReadAllBytes(srcDllPath)), AES.GenDllKey())); Debug.Log($"[CopyAOTAssembliesToStreamingAssets] copy AOT dll {srcDllPath} -> {dllBytesPath}"); } } } |
1 2 3 4 5 |
//对于程序集的目录的文件,添加lable if (dir.Name == Config.HotUpdateDir) { tipName = Config.HotUpdateDir; } |
对了,补充元数据的dll的生成需要事先打包一次,每个平台都需要打包一次。不要走正式的打包流程,只需要点击Unity的Build即可。
IOS导出为xcode工程后,需要去编译一个魔改过的il2cpp.a替换导出后的il2cpp.a文件,可以根据需要手动替换或则写个python自动编译和替换。