做Unity这么多年,几乎每年都会遇到一次“要不要上热更新”的争论。尤其是线上Bug修复要等包体审核、版本覆盖周期长、玩家一听说又要重新下载几百兆安装包就骂娘的时候,热更新几乎是绕不开的刚需。这两年C#项目的热更方案里,HybridCLR属于热度确实很高的一个,也是我最近几个项目里真正用到线上的方案。这篇就按实际接入流程写一遍,把那些文档里没写全、论坛里问得最多的细节一起捋清楚。
先说个结论:HybridCLR不是银弹,也并不会让项目从“不能热更”立刻变成“全能热更”,但如果你团队本身就是Unity C#技术栈、不打算为了热更再引入一整套Lua流程,那它确实是一条成本相对可控的路线。下面我按真实项目接入的顺序来写,从方案选型、工程准备、程序集拆分、编辑器/命令行构建,到运行时加载和真机问题排查,全程都是我实际踩过的路子。
1. 先想明白:HybridCLR在热更新里到底解决什么问题
1.1 为什么放着Lua不用,反而选了C#热更
过去Unity主流的商业热更方案无非是Lua系和ILRuntime系。Lua的优势是热更彻底、历史包袱少,社区里积累了非常多的框架,比如xLua、slua、tolua,但缺点同样明显:新功能要用Lua重写,UI逻辑、战斗逻辑、网络协议全得在C#和Lua之间做胶水层,团队必须养一个熟悉Lua的中间层岗位。项目一旦大起来,Lua侧的卡顿定位、内存泄漏排查、编辑器联调体验,都会变成持续消耗人力的点。
HybridCLR的思路不一样,它是基于IL2CPP的AOT编译产物,在运行时用一个解释器来执行“没被AOT编译进去的那部分IL代码”。简单说,你写的还是C#,编译出来还是标准程序集,只是主包里作为AOT编译的那部分DLL固定死了,另外一套热更程序集可以后续通过网络下发。因此业务团队不用切换语言,原来的C#代码风格、依赖库、甚至是同事写的烂代码风格都能无缝延续。
它的运行机制一句话解释就是:Unity以IL2CPP方式打包后,正常C#代码会变成C++再编成原生机器码,这部分没法直接热更;HybridCLR在启动时加载热更DLL的字节码,并用内置的解释器去逐条执行这些DLL里的指令。你线上修Bug,实际上是让客户端把新的HotUpdate.dll拉下来,替换掉之前加载的那一份。
这个思路决定了它的适用场景很清晰:需要热的是C#逻辑,而且你不想因此把开发模式切换成“逻辑全用脚本语言写”。但要注意,如果项目追求的是“美术资源、配置表、UI图集全量线上更新”,那就不是HybridCLR负责的范畴,那属于AssetBundle或Addressables的资源热更体系。一般我会把它和资源热更分开看待:HybridCLR负责程序集,Addressables或AssetBundle负责资源和配置,两者组合才能做到接近完整的热更新闭环。
1.2 “热哪些、不热哪些”的边界要提前划清楚
我在几个项目里见到最典型的问题,是有人觉得既然上了HybridCLR,所有代码都能热更,于是连SDK初始化、登录协议、埋点上报这种基础底层代码都往热更程序集里塞。这其实是给自己埋雷。真实情况是,HybridCLR解释执行的代码有额外性能开销,你总不能把每帧循环里最热的战斗计算逻辑也全部丢进去。比较合理的划分方式大致是这样:
- 适合热更:玩法逻辑、UI界面流程、任务系统、活动配置表现、常规Bug修复。
- 尽量留在AOT主包:底层网络框架、SDK桥接层、安全校验、核心战斗的伤害/寻路/物理计算、需要极致性能的算法。
- 完全不能热更:修改AOT程序集接口签名后却不一起更新主包,这会导致运行时加载热更程序集后方法签名对不上,直接抛MethodNotFoundException。
我自己项目里的判断标准很简单:改动频率高、对性能不敏感、而且出问题后需要立刻线上修复的逻辑,放热更侧;一年不动一次、跟平台绑定很紧、性能敏感的底层,老老实实待在主包。
还有一类业务也要注意:支付、风控、账号体系等涉及合规校验的代码,尽量不要设计成可直接通过下载替换的DLL,因为热更DLL容易被离线篡改后二次打包。如果项目有强的安全需求,需要额外做DLL加解密、签名校验,HybridCLR本身不强制校验下载文件的合法性,安全策略得自己补。
2. 接入前的工程准备:版本、包体和平台参数
2.1 Unity版本与HybridCLR版本对应关系
不同Unity版本在IL2CPP内部的代码结构差异很大,HybridCLR必须针对Unity版本做适配。你不能随便在Unity 2023.2上拿支持Unity 2021.3的包直接跑,哪怕勉强跑起来,也极可能遇到莫名其妙的崩溃。目前我用的稳定搭配其实是Unity 2021.3 LTS或2022.3 LTS加HybridCLR的Release包,这两个LTS周期长、社区案例多,真遇到问题也更容易搜到解决方案。
接入时建议从官方仓库的Release页面拿与当前Unity版本匹配的版本号,而不是直接拉主干代码。主干代码通常对应最新的Unity预览版,对还在正常发版的项目并不友好。另外有朋友习惯用UPM的方式通过Git URL直接加依赖,我建议在正式立项时冻结版本号,不要用不指定tag或分支的方式,否则某一天同事拉代码后依赖悄悄升级,打包行为突然变化,排查起来会非常头大。
2.2 安装HybridCLR包与对应模块
安装步骤本身不难,但有个前置条件很容易被忽略:编辑器本身必须已经安装了IL2CPP模块。国内很多同事装Unity时图快,只装了默认的Mono模块,后面一打包支持IL2CPP的平台就发现缺模块,还得重新跑一次Unity Hub的安装流程。
确认IL2CPP模块就位后,打开Package Manager,选择“Add package from git URL”,把HybridCLR的Unity包地址粘进去。等待编译完成,菜单栏会出现HybridCLR入口。接下来打开菜单里的“HybridCLR -> Installer…”,这里会看到两个安装项:一个是安装HybridCLR需要的初始化文件,另一个是安装IL2CPP补丁。注意,Installer操作会改动项目里的il2cpp目录或相关配置,执行前建议先提交一遍版本管理,万一补丁和某个协作插件冲突,方便回滚。
环境准备这一点,很多人会忽略“不同平台要分别安装/生成一次”。我的建议是,开发期尽量统一使用Standalone(Windows/macOS)做逻辑调试,真机打包Android或iOS时再切换到对应Target并重新执行安装/生成流程,避免在编辑器里写完热更DLL后发现打出的Android包并不生效。
这里还会牵扯到另一个高频问题,就是打开项目时报“No valid Unity Editor license found,please activate your license”。它本身不是HybridCLR引起的,但团队协作换电脑后经常出现,而且很多人会误以为是HybridCLR补丁破坏了许可证。遇到这类问题,一般关闭Unity Hub里所有实例,重新登录账号激活许可,或者检查是否用了命令行批处理却没有正确传-serial和-username参数。CI打包机上尤其常见,把它和HybridCLR安装顺序分开排查,能省不少时间。
另外,国内做Android包时现在越来越多工具有最低API级别要求。有的第三方SDK会在Gradle里自动要求targetSdk提升到API 35左右,或者编译期报“Minimum API level需要调整到xx”。这跟HybridCLR没有直接关系,但会间接影响构建脚本。如果遇到Gradle或AGP版本提示不兼容,优先去Project Settings里确认IL2CPP、ARM64、Gradle模板版本,而不是回头怀疑HybridCLR补丁没装对。Android平台较稳妥的方式是单独导出Gradle工程,用Android Studio验证原生层日志,这样HybridCLR初始化失败、IL2CPP崩溃会有更清晰的线索。
2.3 热更DLL的保存与版本管理规范
HybridCLR和资源热更不一样,它热更的是程序集,天然对版本一致性更敏感。主包发布后,服务端提供的HotUpdate.dll版本必须和主包里的启动器代码匹配。尤其是接口签名,如果线上主包里的启动器只认识Hotfix.App.Main(),你后发的HotUpdate.dll里已经改成Hotfix.App.Run(string arg),客户端加载时必然失败。
我建议在工程里维护一个hotfix_version.txt或版本常量,打包时把版本信息和DLL一起上传;启动器下载DLL前先请求服务端版本号,再跟本地已缓存版本比对,不一样才拉新文件。这块跟Addressables的版本管理思路一致,但千万别图省事让热更DLL和美术资源共用一个Bundle名,否则资源刷新时把DLL覆盖了一半,压缩包又没下载完整,运行时会加载出损坏的程序集。
3. 项目程序集拆分:让热更代码从主包中彻底剥离
3.1 为什么拆了“假的”热更程序集等于没拆
程序集拆分是HybridCLR接入最核心的一步。如果没有把代码拆到独立的热更新程序集(通常叫HotUpdate.dll或Game.Hotfix.dll),而是把代码直接放在默认的Assembly-CSharp里,那无论HybridCLR安装得多么成功,这部分代码都会在IL2CPP阶段被编译进主包,后续下发DLL时连个可以替换的目标都没有。
具体操作很简单但容易犯错:在Assets下新增一个文件夹,比如叫Assets/HotUpdate,然后右键Create -> Assembly Definition,生成一个asmdef文件。项目里所有希望动态更新的脚本都放进这个文件夹,并让asmdef的Auto Referenced按照需求配置。需要注意,主工程里的普通代码不应该反向引用这个热更程序集,否则主包在编译AOT时会把这个程序集一起依赖进去,绕了一圈又变成了“热更代码被主程序集引用,无法真正独立替换”。
为了保持依赖清晰,我一般会把热更代码整理成几个内部模块,但asmdef只建一个,比如Game.Hotfix。模块与模块之间尽量通过接口或消息机制通信,接口和数据定义放另一个纯AOT程序集里。毕竟热更程序集本身可以被整体替换,内部拆太细反而会增加打包产物管理和依赖检查的复杂性。
3.2 AOT主程序集如何为热更侧提供接口
热更代码能直接调用的,只有“主包AOT侧已经存在的程序集”。你没法在热更程序集里引用一个压根不在主包内的新DLL,因为运行到那一层,CLR环境中找不到这个程序集的AOT版本,补元数据也无济于事。所以实际工程里应该预先规划一个公共契约库,比如Game.Core.dll或Game.Shared.dll,里面只放接口、常量、纯数据结构的定义,由主程序和热更程序共同引用。
例如:
// 在Game.Shared.dll(AOT侧)中定义 public interface IGameApp { void Loop(float deltaTime); } public class GameConfig { public static string RemoteServerRoot = "https://your-cdn.example.com/hotfix/"; }热更侧只需要继承或实现这些接口,主包启动器加载热更DLL后,通过反射找到实现类,再以接口方式调用。这样即使热更侧内部逻辑改动翻天覆地,只要接口保持不变,主包不需要跟着发版。
这里有个反例我在Review代码时经常看到:有人图省事,把登录成功后跳转主界面的逻辑写进主程序集,但UI界面预制体的点击回调又挂在热更程序集里,结果主程序反向引用热更程序集,直接导致整个项目编译依赖环。规范的拆分原则应该是:热更侧可以引用AOT侧的库,但AOT侧代码禁止引用热更侧程序集。哪怕你有绕开编译错误的技巧,也尽量不要用,因为它会破坏“热更程序集可独立替换”的前提。
3.3 通过HybridCLR的生成功能自动处理AOT引用
HybridCLR的编辑器扩展里有几个生成命令,大部分项目只需要执行“HybridCLR -> Generate -> All”。它做的事包括:
- 扫描当前工程依赖的AOT程序集清单。
- 生成桥接函数,补全AOT泛型的调用代理解析。
- 生成link.xml,保留IL2CPP裁剪时需要保留的类型。
这些生成逻辑依赖工程代码结构,所以每次修改AOT侧的公共类型、泛型定义或程序集依赖后,都应重新生成一遍。忘掉重新生成是最常见的“发布后突然某些泛型方法崩掉”的原因之一。
热更程序集里如果大量使用List<T>、Dictionary<TKey,TValue>,或者通过反序列化组件动态创建泛型对象,IL2CPP在AOT编译时并不一定会为所有可能的泛型实例化生成代码。HybridCLR提供了解释型泛型的能力,但仍然需要桥接函数配合。多数情况下,Generate All会自动把当前已公开的AOT类型纳入桥接范围。遇到特殊情况,比如某个泛型类型是通过反射拼出来的、又不在常规类型引用链上,就需要你手动在AOT侧写一个“空引用类”来标记这些类型,强迫代码生成器把它们包含进去。
我记得第一次接的时候完全没概念,代码里大量使用JsonUtility.FromJson<Dictionary<string, WeaponData>>(),打包后跑几天没有异常,一旦走到某个之前从未打开过的界面就抛ExecutionEngineException。后来才发现AOT侧没给这些反射使用到的泛型做预生成,Generate All也没扫描全,需要手动补标记类。这类问题非常难查,因为它不像空引用那样每次必现,而是要运行到特定分支才崩,所以前期程序集设计和引用梳理一定要仔细。
4. 接入实操:打包、元数据补全与运行时加载
4.1 为什么必须补充AOT程序集元数据
HybridCLR解释器虽然能解释执行热更DLL里的逻辑,但解释器本身也需要依赖AOT元数据来识别类型和方法。IL2CPP打包后的主包里,常规的托管元数据已经被裁剪,往往只剩运行必需的部分。热更代码在执行时访问一个AOT类型,比如UnityEngine.GameObject或List<int>,如果对应的元数据信息没有准备好,就会抛出TypeLoadException或FileNotFoundException。
因此启动流程里必须在加载热更程序集之前,先用RuntimeApi.LoadMetadataForAOTAssembly把关键的AOT程序集元数据补进去。这里不是把所有AOT程序集全加载一遍,而是至少覆盖最常用的核心库,通常在示例里看到的是:
private static readonly string[] AotDllNames = { "mscorlib.dll", "System.dll", "System.Core.dll", "UnityEngine.CoreModule.dll", };如果实际工程用了System.Xml、System.Net.Http、Newtonsoft.Json等额外AOT程序集,也需要在列表里补上对应DLL。这一步不完全等于“把DLL文件照搬加载”,它加载的是元数据,不是重新编译方法体,开销相对可控,但依然建议放在启动时的Loading界面之后,一次性批量执行,不要等到进游戏后再突然加载。
4.2 编译热更DLL并打进AB包
HybridCLR不会自动把你的工程代码DLL变成可直接下载的文件。实际项目里,热更DLL需要先通过菜单命令或CI脚本编译出来,然后作为TextAsset打进AssetBundle,或者直接上传到CDN并加密。
我习惯的方式是维护一个编辑器构建脚本,把编译和复制流程串起来。整个流程大致为:
- 切换到目标平台(Android/iOS/Windows)。
- 执行Generate All,生成最新的桥接函数和link.xml。
- 用编译命令生成热更DLL到临时目录。
- 把DLL文件改名为.bytes或打成AB,同步到本地StreamingAssets或上传CDN。
- 最后构建主包。
伪代码如下,实际项目里你可以包一层CI调用:
using UnityEditor; using UnityEditor.Build.Reporting; public static class HybridCLRBuild { public static void BuildHotfixDll() { var target = EditorUserBuildSettings.activeBuildTarget; HybridCLR.Editor.Commands.CompileDllCommand.CompileDll(target); AssetDatabase.Refresh(); } public static void BuildAssetBundle() { // 这里把 Temp/HybridCLR 下生成的 Game.Hotfix.dll // 复制到 AssetBundle 输出目录,并打成 ab // 具体实现取决于项目的资源热更方案 } public static void BuildMainApp() { var report = BuildPipeline.BuildPlayer( new BuildPlayerOptions { scenes = new[] { "Assets/Scenes/Boot.unity" }, locationPathName = "Build/Client/Game.exe", target = BuildTarget.StandaloneWindows64, options = BuildOptions.None }); if (report.summary.result != BuildResult.Succeeded) throw new System.Exception("Build failed."); } }需要注意,HybridCLR热更DLL即使不改变源码,因平台不同,编译出的DLL也不一定通用。比如Windows和Android在同一套源码下生成的IL代码通常一致,但如果你在编辑器里只执行了当前激活平台的编译,然后拿着Windows下生成的DLL去打Android包,通常也能跑,因为IL层面差异很小。但为了严谨和避免边缘情况,我仍然会每次按目标平台执行一次编译。
打AB时,为了兼容WebGL或微信小游戏等平台,尽量将DLL作为TextAsset加载而不是直接放在StreamingAssets里用File读取,后者在Web平台不支持正常工作。下载到内存后使用Assembly.Load(byte[])加载,这样能规避一些平台文件系统差异。
4.3 运行时加载时序与入口调用
一个稳的启动流程应该按这个顺序执行:
- 更新器先检查并下载最新的热更DLL和AOT元数据DLL。
- 加载并注册AOT元数据。
- 加载热更程序集本体。
- 反射或接口方式调用热更入口方法。
入口方法建议设计成无参或只传一个启动上下文对象,因为主包一旦发布,这个入口签名就固定了。下面是典型的代码骨架:
using System; using System.Collections; using System.Collections.Generic; using System.IO; using System.Reflection; using HybridCLR; using UnityEngine; using UnityEngine.Networking; public class BootStrap : MonoBehaviour { private const string HotfixDllName = "Game.Hotfix.dll"; private IEnumerator Start() { // 1. 先从远端 / 本地缓存拿到资源下载地址 // 实际项目里这里通常对接自家的资源热更SDK string hotfixDllUrl = "https://your-cdn.example.com/hotfix/" + HotfixDllName; byte[] hotfixDllBytes = null; using (UnityWebRequest req = UnityWebRequest.Get(hotfixDllUrl)) { yield return req.SendWebRequest(); if (req.result != UnityWebRequest.Result.Success) { Debug.LogError("download hotfix dll failed: " + req.error); yield break; } hotfixDllBytes = req.downloadHandler.data; } // 2. 加载AOT元数据(顺序很重要) foreach (string aotDll in AotMetadataLoader.GetAotDllNames()) { byte[] dllBytes = LoadFromCacheOrStreamingAssets(aotDll); int err = RuntimeApi.LoadMetadataForAOTAssembly(dllBytes, HomologousImageMode.SuperSet); if (err != 0) { Debug.LogError($"LoadMetadataForAOTAssembly failed: {aotDll}, err={err}"); yield break; } } // 3. 加载热更程序集 Assembly asm = Assembly.Load(hotfixDllBytes); Type appType = asm.GetType("Game.Hotfix.App"); if (appType == null) { Debug.LogError("can not find Game.Hotfix.App in hotfix dll"); yield break; } // 4. 调用热更入口 var mainMethod = appType.GetMethod("Main"); mainMethod?.Invoke(null, null); } }LoadMetadataForAOTAssembly返回0表示成功,返回其他非零值表示对应元数据加载失败。这里常碰到“第二次重复加载同一个AOT元数据会报错”的情况,所以初始化代码最好做防重入处理,用一个静态布尔变量标记,避免热重载或场景切换导致重复加载。
需要留意的一点是,下载热更DLL后不要立刻删掉缓存文件,建议保存到本地可写目录。主包每次启动都先走一次本地缓存读取,只有缓存不存在或版本不符时才重新下载,能显著减少玩家流量消耗。每次新版本覆盖时,旧的DLL文件本身虽然可以被覆盖,但如果你同时对DLL做了加密或改了命名规则,旧缓存文件不及时清理,会白白占用玩家存储空间,也会让出现问题时的定位变困难。
4.4 热更入口代码如何启动整个游戏逻辑
主包BootStrap完成DLL加载后,热更侧需要自己负责游戏主循环的初始化。这个入口代码在项目演进中会经常变化,它应该放在热更程序集内,方便后续所有游戏启动流程都能被热更覆盖。
一个比较稳妥的热更入口实现如下:
// 这段代码在 Game.Hotfix 程序集里 public static class App { public static void Main() { Debug.Log("[Hotfix] App Main start"); var go = new GameObject("[HotfixEntry]"); GameLoopBehaviour loop = go.AddComponent<GameLoopBehaviour>(); GameObject.DontDestroyOnLoad(go); // 示例:加载第一个游戏场景 UnityEngine.SceneManagement.SceneManager.LoadScene("LoginScene"); } }主包尽量保持精简,只负责“下载资源 -> 加载AOT元数据 -> 加载热更DLL -> 调用App.Main”,之后的场景切换、UI框架初始化、网络连接建立都由热更侧驱动。这样后续迭代时哪怕要调整开场加载流程,也不需要重新发布主包。
5. 真机与线上常见问题排查记录
5.1 崩溃或异常:加载不到System.dll、mscorlib.dll
这个基本可以断定是AOT元数据没加载,或者加载顺序不对。一定要先加载所有AOT元数据DLL,再加载热更程序集。如果热更程序集里引用了System.Core.dll中的类型,而AOT元数据列表漏了System.Core,那么调用到对应类型方法时会报“FileNotFoundException”或者“TypeLoadException: Could not load type … from assembly …”。
解决办法是打开HybridCLR Sample项目里默认的AOT元数据清单,从里面挑实际用到的加上。项目里如果用了LitJson、protobuf-net等第三方库,它们的DLL通常在AOT主包里,一样要往清单里加。不要图省事只加一个UnityEngine.CoreModule,等真机崩溃再回溯会非常被动。
5.2 找不到DllNotFoundException: slua 这类原生插件错误
如果你的老项目之前基于slua或tolua开发,导入HybridCLR后在新机器运行,偶尔会遇到“DllNotFoundException: slua”这类异常。这通常是工程里的Lua原生插件没有正确包含在当前构建目标中,跟HybridCLR本身没有关系。原因往往是构建时Platform设置不正确导致Plugins目录下的原生库没被拷贝,或者HybridCLR补丁重编IL2CPP后动态库加载路径变化。可以先去Project Settings确认当前激活平台,然后检查对应slua相关原生库的Platform设置。
如果项目已经完全切到HybridCLR,则建议把所有Lua相关脚本、原生库、初始化代码彻底移除。不要抱着“暂时留着备用”的心态,运行时残留的Lua初始化会触发额外的原生库加载,一旦路径不对就会在启动阶段抛出各种DllNotFound,反而干扰排查。
5.3 高版本Android API与IL2CPP构建环境问题
现在很多项目开始把target API级别提到35左右,这过程中部分开发机上的Android SDK或NDK版本老旧,IL2CPP构建会间歇性失败或安装后启动崩溃。跟HybridCLR直接相关的点在于:HybridCLR的IL2CPP补丁会改写libil2cpp相关原生层,如果NDK版本与Unity内置的libil2cpp不兼容,崩溃现场会出现在il2cpp基础设施里,看起来特别像热更框架的问题。
这种问题先不要急着去HybridCLR群里问,把NDK版本、Gradle版本、Unity版本看一遍,尽量使用Unity Hub里建议的NDK配套版本。导出Gradle工程后在Android Studio原生Logcat中查看崩溃堆栈,能明显看到是libunity.so还是libil2cpp.so内部的问题。先把原生环境统一到官方支持版本,再回来验证HybridCLR功能,通常能迅速定位。
另外,Android与iOS双端要注意代码裁剪问题。AOT侧程序集如果被裁剪得太狠,某些被热更代码反射使用的方法在运行时无法恢复。一般HybridCLR生成link.xml会覆盖大多数,但如果你为了极致减包又自定义了额外的link.xml裁剪,建议留出热更需要的AOT类型白名单。不要一上来就启用Aggressive Strip,否则上线后哪个界面少个方法,排查成本远大于省下的那几MB。
5.4 Addressables或自定义资源管理没有正确释放导致的诡异行为
HybridCLR只管程序集,不管资源生命周期。很多项目在加载热更DLL后,又开始用Addressables加载AB资源。热更代码里动态创建的Prefab、Texture、Material都依赖资源系统正确引用计数。滥用Addressables的LoadAssetAsync但从不Release,会出现内存持续增长,而且热更新后旧资源版本还可能被残留在缓存中,表现为“明明更新了DLL,但新逻辑显示还是旧效果”。
这里建议让资源热更模块提供一个统一的资源下载并缓存接口,DLL和AB走同一套版本号校验。每次热更版本提升时,服务端下发新的版本清单,客户端对比本地已有记录,只更新变化部分。别把DLL的下载单独绕过资源管理,否则后面处理补丁回滚和断点续传时非常麻烦。
5.5 编辑器或打包机上的许可证与重复批处理问题
有些团队做CI时,为了出包会频繁调用Unity -batchmode,一旦许可证激活状态异常,界面会提示“No valid Unity Editor license found。please activate your license.”。以前我遇到这问题,总以为是HybridCLR的Installer改变了一些文件导致,后来发现多数是CI机器上多个Unity进程并发抢许可证导致激活失败。解决办法是CI里保证同一时间只有一个Unity进程执行批处理,并在构建脚本最前面加一个许可证自检逻辑,失败则直接退出并输出日志。
6. 最后再分享一点实际操作中的体会
如果你是从零接入HybridCLR,我建议不要一上来就把巨型业务工程整体拆完,那会非常痛苦。先拿一个小Demo工程跑通:建两个程序集,AOT侧一个入口方法,热更侧一个简单方法,打包后用本地路径下载DLL并加载,能跑通后再按这个模式迁移真实业务。我第一次投入真实项目时,花在“程序集循环依赖”的时间远比写加载代码多,因为习惯的Mono开发思维里,脚本之间互相引用很随意,但一旦切到AOT/热更分离模式,依赖方向必须强制统一。
过程中如果某个版本在论坛里看到很多人反馈崩溃,别犹豫,先看看是不是版本匹配问题。HybridCLR这种跟IL2CPP深度绑定的框架,版本敏感度极高,换了Unity小版本甚至也会带来差异。养成习惯:每次升级Unity前先确认HybridCLR是否已经有对应适配版;不确认清楚就不盲目升级。毕竟程序集热更一旦生效,回退成本是玩家的卸载率,不值得为编辑器新特性冒险。
这套接入里,我认为最值得投入时间研究的是程序集拆分和AOT元数据覆盖范围,而不是RuntimeApi怎么调用。前者决定你后续迭代顺不顺畅,后者示例代码一大堆,照着抄就能用。真机遇到诡异崩溃时,多利用导出工程后的原生日志,把问题拆成“HybridCLR框架问题”和“业务资源问题”两个方向排查,不要因为用了HybridCLR就把所有异常都归到它头上。保持这个思路,热更接入这件事会比想象中顺利很多。