当前位置 : 主页 > 网络编程 > 其它编程 >

介绍一种基于Mono的Unity热更新方案

来源:互联网 收集:自由互联 发布时间:2023-07-02
《介绍一种基于Mono的Unity热更新方案》热更新是Unity3D开发总也绕不过去的话题甚至影响到了开发语言程序架构、人员配置不可 《介绍一种基于Mono的Unity热更新方案》 热更新是Unity3D开发
《介绍一种基于Mono的Unity热更新方案》热更新是Unity3D开发总也绕不过去的话题甚至影响到了开发语言程序架构、人员配置不可

《介绍一种基于Mono的Unity热更新方案》

热更新是Unity3D开发总也绕不过去的话题甚至影响到了开发语言程序架构、人员配置不可谓不重要。文章开头先从一些大家都熟知的东西带入。热更新目前有很多成熟的方案笔者很早前因为工作需要了解了一些信息大体分几个流派

Lua流派/CSharp转Lua流派

CSharp流派

JS/TS流派

各个流派均有成熟的框架优劣势在此不再展开选择时往往是结合自己团队的情况来取舍。从方向上看笔者更看好Lua流派Lua天生就作为脚本语言设计集成到游戏引擎中作为逻辑脚本似乎是一件很合理的事情。笔者对Lua不是很熟悉也曾因此在工作面试中被鄙视从个人喜好上还是喜欢CSharp这门语言多一点当然这个喜好也是建立在特定环境下的语言层面的优劣在此也不再展开。在聊新的方案前先从头聊一些热更新方面的知识做引子。

热更新的重灾区是在iOS系统因为一些众所周知的原因Unity最初的Mono运行时在iOS平台下只能以full AOT模式运行这样就无法实现热更新了。这里也引出了运行模式的概念大家熟知的有

JIT运行

MonoV8等引擎默认运行模式这种模式是可以动态Load代码的也就是可以更新代码逻辑。但是在iOS系统上是被禁止的。

AOT运行

提前编译成本机代码运行效率可以比肩原生代码Unity的Mono引擎在iOS系统上即以此模式运行但是不能更新代码。

Interpreter运行

即解释器执行顾名思义脚本语言以此方式运行并没有生成本地机器码目前各个热更流派均是以此方式实现热更新代价是效率低一些。

Lua天生是以解释器运行的具有体积小集成灵活等特性作为大家的首选脚本也发展出了jit模式来解决其他平台上的性能问题有xLuatoLua等成熟框架。

CSharp也发展出了ILRuntime框架来支持解释模式从而实现了CSharp热更新。

那么JS/TS呢笔者以前以为V8引擎一直有解释器的不然iOS上的Chrome是怎么运行的呢带着这个问题查了下才发现V8确实加了解释器并不是很久以前。所以现在JS/TS流派也发展出了成熟的框架比如Puerts。

那么Mono呢再继续查了下也有。Mono的解释器命运就比较曲折了从Mono第一个版本便有再到后来光荣退场然后重新出山当然是肩负了使命的。既然再加回来当然还要再进一步AOT和Interpreter都可以在iOS上运行如果可以让热点代码跑在AOT容易变更的代码跑在Interpreter两部分代码不需要关心自己的运行时不是更好吗再继续查了下也有Mono内部已经实现了两套运行模式的交互部分在aot编译时提前生成了交互代码运行时的代码可以无感知的相互调用并且完善度已经相当高。

Mixed-Mode Execution

Mono支持的一种运行模式混合了AOT和Interpreter在执行没有AOT的程序集时自动将程序集切换到Interpreter内执行所以支持动态Load代码。

事情在朝着好的方向发展似乎一切都比较合理。从mono的提交日志看2018年开始充斥着大量的[interp]模块提交几乎占到了总提交量的1/3mono的这个模块发展很快。反观Unity官方mono恰好停留在[interp]模块加入前便不再合并mono主干。具体原因我们不再此猜测只是这样就需要我们自己动手了。

既然运行时已经支持剩下的工作就是集成到UnityEngine内与Il2cpp亲密无间。在此之前我们先以Unity的默认执行框架做引子以下为笔者个人理解不正确的地方请以官方为准。

7b28c5123db6

Unity Il2cpp执行框架

1号通道最初是通过Mono的Internal call来实现的Il2cpp同样使用此方式来实现(C)到(A)的请求。

2号通道是UnityEngine通过Il2cpp 然后invoke上层接口来实现回调。

0号通道我们先称之为magic实现一些定制特性我们先忽略。

如果要在Unity项目中实现Mono 的Mixed-Mode Execution我们需要在此系统内再加入一个Mono runtime同时绑定上述三条通道这里先说下我们的第一种绑定方案(icall绑定)

针对1号通道Mono原生即支持Internal call(我们简称icall)那么在Mono中直接执行unity assembly然后将icall调用直接指向(A)即是最直接的方式。

针对2号通道需要在(B)层做些手脚通过查代码我们发现Il2cpp的Method实际是一个函数指针那么查找到需要回调的函数并使指针指向我们的实现然后再Invoke Mono内的相同函数即实现了hook功能即实现了UnityEngine的回调。

通过以上两种方式我们在自己的Mono runtime内绑定了大部分的Unity功能。为什么是大部分呢这里可以实现icall绑定的前提是所有icall绑定传递的对象只有一份内存并且是在(A)内UnityEngine.Object即是此目的。Unity当然不会就此收手magic就无用武之地了除此还有其他一些特性最麻烦的是0号magic通道比如MonoBehaviour、Coroutine、传递给(A)一个.Net 的Stream等等。这里因为Unity做了一些特殊处理具体实现我们不得而知即使勉强实现了也无法保证以后兼容性所以我们使用了Wrapper的方式。这里有两种实现方式后面再详细介绍。

我们再来看看加上Mono runtime后的结构

7b28c5123db6

加入Mono runtime

如上面所说我们需要绑定(H)内的icall指向(A)即新增了通道5-3同时需要hook(B)内的函数指针实现回调即新增了通道4-5。

同时为了处理magic情况我们提供两种方案一种是手动在(F)内实现绑定接口(Unity的icall绑定大部分也是这种无规则的手动实现所以给我们的自动绑定带来了很多麻烦)这种方案上层用户(I/K)完全无感知只是因为这部分是由c/cpp实现对部分团队并不友好。所以我们新增了通道6也就是我们的第二种绑定方案(Adapter绑定)

Adapter是指在(D)中指定一些需要在(I/K)内使用的程序集在构建时为这些程序生成两个Adapter程序集分别位于(E)和(J)这样当用户(I/K)调用(D)内的程序接口时会自动通过通道6调用调用方无感知同时通道6是双向的即同时支持调用与回调。

另外指出的是(H)是直接使用的UnityEngine.*.dll只需重新绑定icall即可(F)/(E)/(J)内的绑定代码均由代码生成器生成即除非需要手动实现icall绑定通道3/4/5/6均自动生成。

两种方案是否给框架增加了复杂性呢其实在开发过程中为了保持简洁笔者在这两种方案中反复切换了多次每个单独方案都能实现绝大部分的功能但是总会让一小部分特定的问题复杂化。比如我们全部使用Adapter绑定可以完成需求吗其实是可以的但是碰到Unity使用runtime来支持的特性单纯的从CSharp层来实现复杂度会大大增加或者需要用户修改程序而且后续功能的兼容和扩展性会低很多。两种方式一起用虽然给绑定生成器带来了复杂性用户使用反而简单一些所以保留了两种绑定方案。

至于用户的程序集是在(K)内执行还是在(I)内执行用户可以自己根据实际需求来配置绑定生成器会在构建时自动触发根据配置生成不同的工程然后将此工程以pod库的形式提供给主项目集成。主项目需要在podfile中引用后执行

pod install

即可链接成最终执行项目以上即是笔者本次介绍的方案详细使用细节请移步这里。此方案支持iOS平台下Assembly.Load接口。Android平台建议直接使用Unity的Mono运行时同样支持Assembly.Load接口这样在架构上不需改动。

此方案其实构思已久期间做了不少可行性测试一直因抽不出时间拖着未实现最终也因2020年这个年终闲的时间长了些才得空实现了出来其间缝合多个程序边界并实现自动化的复杂度还是超出了预期总算最终走通了因为感觉到自己可以调配的精力非常有限也深知独立开发很难使这个框架完善所以决定开源出来也顺便取了个名字PureScript起码保持从用户角度看来是一个简洁、单纯的脚本框架。

如果大家有兴趣后面再补充详细实现细节目前项目已经开源。对此方案有兴趣的同学欢迎提交PR或者Star。

添加一个录屏

【文章原创作者:建湖网站制作公司 http://www.1234xp.com/jianhu.html 欢迎留下您的宝贵建议】
上一篇:SQLite–ORDER子句
下一篇:没有了
网友评论