stonstad 发表于 2022-11-24 17:00

基于华佗热更新(HybridCLR)的热更新方案设计和接入

前言:主要记录了在接入华佗热更新中的一些思考和对整个项目的设计,当然也包括接入过程中的一些坑。希望可以给需要接入热更新的人一些启发,同时也欢迎大家留下自己的的建议和想法。
一. 什么是HybridCLR?


HybridCLR(原华佗热更新) 是一个 特性完整、零成本、高性能、低内存 的近乎完美的Unity全平台原生c#热更方案。

没有人比自己更了解自己的代码,所以想了解更详细的内容,可见官方链接:
https://focus-creative-games.github.io/hybridclr/about/
二. 为什么使用HybridCLR?

使用HybridCLR热更新,可以以最小的成本接入项目,无需对项目进行大规模的重写和修改。 HybridCLR热更新针对il2cpp进行了拓展,可以直接使用C#语言开发,无需使用lua,typescrpt等脚本语言。 AOT代码和JIT代码使用C#同一套语言,并且对于跨域继承,反射等高级语言特性支持完善,也无需使用多个项目进行热更代码的逻辑分离,对于开发的便捷性有着很大的提升。 在执行效率和内存占用等性能指标上也有着大幅的的优势。

所以为什么使用HybridCLR?这里可以用它的创造者walon的自负且自信的一句话回答:
HybridCLR是一个划时代的Unity平台C#原生热更新技术,它将国内Unity开发的技术框架水平提高到新的高度,并深刻地改变Unity平台的开发生态。
三. 如何接入HybridCLR?


关于如何接入HybridCLR,官方文档已经很详尽了,所以具体的接入操作可以去上面官方文档中查看。这里只介绍下接入过程中的一些问题,具体的操作不再赘述。

推荐使用unity2020.3.33版本,其他版本可能有些问题或者额外操作,等熟悉后再尝试接入其他版本的unity。
接入流程主要分为以下步骤:
安装 hybridclr_unity package,见官方文档: https://focus-creative-games.github.io/hybridclr/install/ ; 配置PlayerSettings中的设置,见官方文档 https://focus-creative-games.github.io/hybridclr/project_settings/ ; 根据需求划分程序集,关于如何划分程序集的更多的内容参考下面的章节: 六.程序集设计; 配置HybridCLR的参数,新手主要关注 hotUpdate Assembly Definitions (需要热更的程序集,根据步骤3中的划分填写) 和 hotUpdate dlls(需要热更的dll) 两个参数; 到这里,不涉及到写代码部分的操作已经完成,接下来的内容就于代码相关了。
四. 热更新的流程设计


在写热更新代码前,先来梳理下热更新的流程吧。


图1. 热更新初始化流程

步骤 0~2


如上图中所示,第0~2步是常规的下载-加载资源流程,这些部分根据项目随意发挥,当然我们的热更dll也是在这里需要打包成ab包下载下来的。
步骤 3 加载热更dll


加载热更的dll很简单,只需要加载资源,然后调用Assembly.Load() 即可。
// 加载资源 (示范代码) var dllBytes = await AssetComponent.LoadAsync<TextAsset>("Assets/Dlls/game.bytes"); // 加载程序集 Assembly gameAss = System.Reflection.Assembly.Load(dllBytes.bytes); 步骤 4 加载补充元数据dll


加载补充元数据dll。这一步是可选的,但是如果热更程序集引用到了aot程序集的情况下,这一步是必不可少的,不然很大几率我们会看到:

MissingMethodException: AOT generic method isn't instantiated in aot module xxx

上面这个错误就是因为AOT泛型函数实例化缺失导致的,所以我们最好补充元数据。补充元数据的代码如下:
// 前三个是系统自带,推荐带上,第四个是自己游戏AOT的程序集 var AotAssemblyDlls = new string[] {   "mscorlib","System","System.Core",   "MyGame.Framwork", }; for (int i = 0; i < AotAssemblyDlls.Length; i++) {   var aotDllName = AotAssemblyDlls;   var path = $"Assets/ResBundles_Dll/aotDll/{aotDllName}.bytes";   var asset = AssetComponent.Load<TextAsset>($"Assets/AotDlls/{aotDllName}.bytes");   // 加载assembly对应的dll,会自动为它hook。一旦aot泛型函数的native函数不存在,用解释器版本代码   LoadImageErrorCode err = RuntimeApi.LoadMetadataForAOTAssembly(asset.bytes);   if (err == LoadImageErrorCode.OK)         Debug.Log($"{stateID}: LoadMetadataForAOTAssembly:{aotDllName}. ret:{err}");   else         Debug.LogError($"{stateID}: LoadMetadataForAOTAssembly:{aotDllName}. ret:{err}"); }
到这里可能很多人疑问,我们的元数据dll从哪来的?

其实在打包build过程中,会生成裁剪过的dll,HybirdCLR会自动帮我们把dll拷贝到目录:项目目录/HybridCLRData/AssembliesPostIl2CppStrip/目标平台/xxx 。

我们也可以通过以下代码获取当前目标平台的目录:
SettingsUtil.GetAssembliesPostIl2CppStripDir(EditorUserBuildSettings.activeBuildTarget);
所以如果通过ab加载,在打包assetbundle资源前,我们必须要进行一次构建操作。然后把构建生成的需要补充的dll拷贝到项目目录中,然后根据自己的ab打包策略,将dll打包成ab资源。
步骤 5 加载热更场景,执行热更代码


因为AOT代码无法直接调用热更代码,那么如何运行热更dll里的启动代码呢?推荐以下为比较好的方案:

推荐方案:在所有资源都已下载,所有热更代码都已加载后,通过加载热更资源中的场景,场景上挂有热更脚本(例如InitAfterHotFix.cs),在InitAfterHotFix的Awake方法中进行后续游戏的初始化。

其他方案:当然也可以通过Assembly类获取到热更程序集的启动类的启动方法,使用反射的方式实现。
五. 打包流程


在以上步骤已处理完毕,代码添加好了以后,下面将开始打包的流程。
打包流程主要分成以下几步:

[*]
编译热更新Dll
unity中HybridCLR->CompileDll->ActiveBuildTarget按钮,或者直接调用代码: CompileDllCommand.CompileDll(EditorUserBuildSettings.activeBuildTarget);
把编译出来的热更Dll拷贝到项目里,按照自己的ab打包策略放入自己规划的目录内;

生成LinkXml,MothedBridge等等

unity中HybridCLR->Generate->All按钮,或者直接调用代码: HybridCLR.Editor.Commands.PrebuildCommand.GenerateAll();

拷贝AOT补充元数据dll到项目里,按照自己的ab打包策略放入自己规划的目录内;

补充元数据dll的需要经过一次打包后生成,具体生成逻辑和文件目录在第四节中有介绍。

打包AssetBundle资源;

根据自己的需求,将ab资源放入StreamingAssets或远程服务器。
六. 程序集设计


程序集设计虽然放在最后一章节,但是应该是项目最开始就优先考虑和设计的。
6.1 划分程序集,不仅仅是为了热更新


程序集的划分,不仅仅为了热更新。在不需要热更新的项目中,合理的规划和拆分代码模块,设置合理的引用关系,可以解除基础框架-游戏模块-三方插件的耦合。


图2. 普通的项目的程序集划分

如上图中游戏模块作为需要经常改动的模块,它引用了基础框架模块和三方插件。而后者没有对其的引用。如果某个不规范的程序员擅自在基础框架中添加了涉及到游戏模块的代码,那么Ta会发现代码报错,编辑器中无法查到对应代码的引用。

如果基础框架和游戏模块同在同一个程序集中,作为管理者必须要严格规范下属并经常review代码,不然不规范的下属几天就可能把基础框架和游戏模块揉合成一团小猫玩过的毛线团,很难理顺!
6.2 程序集与热更新


在热更新需求下的程序集划分与普通的项目差别很小,基本通用,主要在于我们如何划分AOT程序集和JIT程序集,而且还有对于unity默认的Assembly-CSharp程序集的定义。

例如上面的5.1中的例子,我们可以有2种划分方案:

AOT程序集包括基础框架模块和三方插件,热更程序集包括游戏模块的1~N个程序集。这种方案下适用于基础框架稳固完善的项目;

AOT程序集仅包括三方插件(甚至三方插件也可以不包括),剩余所有的程序集作为热更。这种方案下基本所有内容都可以热更,适用于新项目的快速迭代。

但是这种划分下有个问题,我们必须要把加载热更资源和加载dll等初始化操作,放入AOT程序集中实现。这过程中必然会有下载/加载ab资源等操作,必然会用到热更的基础框架中的逻辑。

我们可以通过Assembly加载,通过反射调用到热更的代码,但是这样必然很繁琐。还有另外一种方法,为了避免AOT引用热更代码,我们也可以在AOT中实现另外一套资源下载/加载逻辑。

在上述2种方案中都会涉及到unity默认的Assembly-CSharp程序集的定义。我们可以将其定义为AOT程序集,不引用任何热更程序集,那么下载热更资源,加载dll等操作均在这里实现。我们也可以将其定义为热更程序集,那么下载热更资源,加载dll等操作均在其他的AOT程序集中实现。

把Assembly-CSharp程序集当作热更程序集有个不好的地方,那便是我们随意加入的测试代码(没有放在有程序集定义的文件夹下),某些导入的三方插件等等,只要未定义过程序集,那么默认就会加入到Assembly-CSharp程序集。所以会经常误把某些代码打入热更dll。为避免程序花费时间和精力去检查这些内容,建议不用这种方案。
6.3 unity里的Assembly Definition文件


在一个普通的C#项目中,我们可以在vs中右键程序集(项目)条目然后打开属性,查看该程序集的相关设定。但是unity生成的项目中,我们是无法使用这个操作的。

因为程序集的生成和划分是由我们在unity中处理,然后unity自动生成。其中unity中用来划分程序集的文件就是Assembly Definition。

我们通过Assembly Definition文件,可以处理以下事情:
添加自定义的宏:Define Constraints; 添加删除程序集的依赖:Assembly Definition Refercences; 选择生效的平台:Platforms;

除了上面这些常用的功能,还有General选项下这些很有用的选项:
Allow ‘unsafe’ Code:是否启用不安全的代码; Auto Referenced:是否自动被依赖;勾选后会被默认的Assembly-CSharp程序集自动依赖。所以如果我们想在Assembly-CSharp中隔离对当前程序集的依赖,取消勾选。 No Engine References:不依赖于引擎提供的代码模块。适用于可以在unity或其他平台的项目中通用的程序集。 Override References:可以手动指定所依赖的预编译的程序集,因为unity项目中的预编译程序集可以被其他默认依赖,勾选后当前程序集可以选择(不)依赖某个预编译的程序集。 Root Namespace: 当前程序集的默认命名空间,填写后我们使用unity添加新代码文件,会自动添加命名空间。我测试只有在unity中创建才生效。
页: [1]
查看完整版本: 基于华佗热更新(HybridCLR)的热更新方案设计和接入