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

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

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

  • Lua流派/CSharp转Lua流派
  • CSharp流派
  • JS/TS流派

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

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

  • JIT运行

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

  • AOT运行

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

  • Interpreter运行

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

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

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/3,mono的这个模块发展很快。反观Unity官方mono,恰好停留在[interp]模块加入前,便不再合并mono主干。具体原因我们不再此猜测,只是这样就需要我们自己动手了。

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

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后的结构:

加入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。

添加一个录屏

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 204,921评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 87,635评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 151,393评论 0 338
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,836评论 1 277
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,833评论 5 368
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,685评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,043评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,694评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 42,671评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,670评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,779评论 1 332
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,424评论 4 321
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,027评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,984评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,214评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,108评论 2 351
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,517评论 2 343

推荐阅读更多精彩内容