Ilingis 发表于 2022-7-2 13:07

Huatuo——划时代的Unity原生C#热更新技术

huatuo介绍

huatuo是一个特性完整、零成本、高性能、低内存的近乎完美的Unity全平台原生c#热更方案。
huatuo扩充了il2cpp的代码,使它由纯AOT runtime变成‘AOT+Interpreter’ 混合runtime,进而原生支持动态加载assembly,使得基于il2cpp backend打包的游戏不仅能在Android平台,也能在IOS、Consoles等限制了JIT的平台上高效地以AOT+interpreter混合模式执行。

huatuo开创性地实现了 `differential hybrid dll` 技术。即可以对AOT dll任意增删改,huatuo会智能地让变化或者新增的类和函数以interpreter模式运行,但未改动的类和函数以AOT方式运行,让热更新的游戏逻辑的运行性能基本达到原生AOT的水平。
基础概念

CLR

CLR即 Common Language Runtime,中文叫公共语言运行时,是让 .NET 程序执行所需的外部服务的集合,.NET 平台的核心和最重要的组件,类似于 Java 的 JVM。更详细介绍请看 公共语言运行时 (CLR) 概述
il2cpp

il2cpp是Unity开发的跨平台CLR解决方案。诞生它的一个关键原因是Unity需要跨平台运行,但一些平台如iOS这种禁止了JIT,导致依赖了JIT的官方CLR虚拟机无法运行,必须使用AOT技术将mananged程序提前转化为目标平台的静态原生程序后再运行。而mono虽然也支持AOT,但性能较差以及跨平台支持不佳。
il2cpp方案包含一套AOT运行时以及一套dll到C++代码及元数据的转换工具,使得原始的c#开发的代码最终能在iOS这样的平台运行起来。
il2cpp与热更新

很不幸,不像mono有Hybrid mode execution,支持动态加载dll,il2cpp是一个纯静态的AOT运行时,不支持运行时加载dll,因此不支持热更新。
目前unity平台的主流热更新方案xlua、ILRuntime之类都是引入一个第三方vm(virtual machine),在vm中解释执行代码,来实现热更新。限于篇幅我们只分析使用c#为开发语言的热更新方案。这些热更新方案的vm与il2cpp是独立的,意味着它们的元数据系统是不相通的,在热更新里新增一个类型是无法被il2cpp所识别的(例如通过System.Activator.CreateInstance是不可能创建出这个热更新类型的实例),这种看起来像、实际上却又不是的伪CLR虚拟机,在与il2cpp这种复杂的CLR运行时交互时,产生极大量的兼容性问题,另外还有严重的性能问题。
一个大胆的想法是,是否有可能对il2cpp运行时进行扩充,添加interpreter模块,进而实现mono hybrid mode execution 这样机制?这样一来就能彻底支持热更新了,并且兼容性极佳。对开发者来说,除了以解释模式运行的部分执行得比较慢,其他方面跟标准的运行时没有区别。
对il2cpp加以了解并且深思熟虑后的答案是——确实是可行的!具体分析参见 关于huatuo可行性的思维实验 。这个想法诞生了huatuo,unity平台第一个支持ios的跨平台原生c#热更新方案!
原理

huatuo扩充了il2cpp运行时,将它由AOT运行时改造为'AOT + interpreter'双引擎的混合运行时,进而完美支持在iOS这种禁止JIT的平台上以解释模式无缝地运行动态加载的dll。如下图所示:


更具体一些,至少需要实现以下功能:

[*]加载和解析dll元数据
[*]动态注册元数据,其中关键为hook动态函数的执行流到解释器函数
[*]实现一个高效正确的解释器
[*]正确处理gc及多线程等运行时机制
特性

标准运行时特性

近乎完整实现了ECMA-335规范,不支持的特性仅包括:

[*]不支持delegate的BeginInvoke, EndInvoke。纯粹是觉得没必要实现
[*]不支持 MonoPInvokeCallbackAttribute。意味着你如果同时还接了lua,你没法直接将热更新的c#函数注册到lua中,但有一个不复杂的办法能做到这点。
由于huatuo极其完整的实现,使用huatuo后的c#开发体验跟editor下mono开发几乎完全相同(除了调用一些il2cpp没实现的.net framework函数,非专家级别的开发者难以构造出huatuo不支持的用例)。
另外,由于是运行时级别的实现,huatuo支持这些特性的同时,不需要你额外生成或者调整任何代码。对于开发者来说,相比Unity下原生c#开发,零额外的学习和开发成本。
AOT相关特性

由于il2cpp AOT模块的存在,il2cpp比于标准运行时多了一些不存在的机制,因此huatuo也有一些额外的特性

[*]支持使用 interpreter assembly替换 AOT assembly(限制:必须不存在其他AOT assembly对它的直接引用)
[*]支持补充元数据机制,彻底支持AOT泛型,参见AOT泛型原理
[*]支持AOT hotfix,可以修复AOT模块的bug
[*]支持任意c#函数注册到lua之类的虚拟机,不限于static函数,并且也不需要MonoPInvokeCallbackAttribute。条件是注册和回调方式需要略微调整
[*]开创性地实现了 `differential hybrid dll` 技术。即可以将某个热更新dll先AOT形式打包,后面可以对该dll任意增删改,huatuo会智能地让变化或者新增的类和函数以interpreter模式运行,但未改动的类和函数以AOT方式运行。这意味着热更新的游戏逻辑的运行性能将接近原生AOT的水平。
Unity相关特性


[*]完美支持Unity的 assembly def模块机制。
[*]完美支持代码中挂载热更新脚本,无使用场景限制
[*]完美支持资源上挂载热更新脚本,但要求打包工作流有少许调整,参见MonoBehaviour工作流
与其他流行的c#热更新方案的区别

从原理来说,huatuo几乎将Unity C#原生热更新技术做到理论上的极限,与当前所有主流热更新方案不在一个层次。
本质比较

huatuo是原生的c#热更新方案。通俗地说,il2cpp相当于mono的aot模块,huatuo相当于mono的interpreter模块,两者合一成为完整mono。huatuo使得il2cpp变成一个全功能的runtime,原生(即通过System.Reflection.Assembly.Load)支持动态加载dll,从而支持ios平台的热更新。
正因为huatuo是原生runtime级别实现,热更新部分的类型与主工程AOT部分类型是完全等价并且无缝统一的。可以随意调用、继承、反射、多线程,不需要生成代码或者写适配器。
其他热更新方案则是独立vm,与il2cpp的关系本质上相当于mono中嵌入lua的关系。因此类型系统不统一,为了让热更新类型能够继承AOT部分类型,需要写适配器,并且解释器中的类型不能为主工程的类型系统所识别。特性不完整、开发麻烦、运行效率低下。
实际使用体验或者特性比较


[*]huatuo学习和使用成本几乎为零。huatuo让il2cpp变成全功能的runtime,学习和使用成本几乎为零,几乎零侵入性。而其他方案则有大量的坑和需要规避的规则,学习和使用成本,需要对原项目作大量改造。
[*]huatuo可以使用所有c#的特性。而其他方案往往有大量的限制。
[*]huatuo中可以直接支持使用和继承主工程中的类型。其他方案要写适配器或者生成代码。
[*]huatuo中热更新部分元数据与AOT元数据无缝统一。像反射代码能够正常工作的,AOT部分也可以通过标准Reflection接口创建出热更新对象。其他方案做不到。
[*]huatuo对多线程支持良好。像多线程、ThreadStatic、async等等特性都是huatuo直接支持,其他方案除了async特性外均难以支持。
[*]huatuo中Unity工作流与原生几乎完全相同。huatuo中热更新MonoBehaviour可以直接挂载在热更新资源上,并且正确工作。其他方案不行。
[*]huatuo兼容性极高。各种第三方库只要在il2cpp下能工作,在huatuo下也能正常工作。其他方案往往要大量魔改源码。
[*]huatuo内存效率极高。huatuo中热更新类型与主工程的AOT类型完全等价,占用一样多的空间。其他方案的同等类型则是假类型,不仅不能被runtime识别,还多占了数倍空间。
[*]huatuo执行效率高。huatuo中热更新部分与主工程AOT部分交互属于il2cpp内部交互,效率极高。而其他方案则是独立虚拟机与il2cpp之间的效率,不仅交互麻烦还效率低下。
运行性能

实际性能如理论估计,全面并且大幅胜出当前主流的xlua、puerts、ILRuntime之类的热更新方案。

[*]基础指令(数值计算及条件跳转等指令),由于各个语言之间差距不大,因此胜出不明显
[*]对象模型指令。由于没有跨语言交互的成本,几乎是数倍到数十倍的提升(如果指令自身消耗特别大,则差距不那么明显)
性能测试用例来自ILRuntime提供的标准测试用例,测试项目来自Don't worry的github仓库。
测试结果显示,绝大多数测试用例都有数倍到数十倍的性能差距,差距极其夸张。唯独数值计算跟xlua有少量劣势,这是因为当前huatuo 未对指令作任何优化 ,后续优化版本大多数基础指令都将有100-300%的性能提升。



内存

huatuo是运行时级别的实现,因为热更新的脚本,除了执行代码是以解释模式执行,其他方式跟AOT部分的类型是完全相同的,包括占用内存。因此下面只介绍il2cpp中class类型和ValueType类型对象的内存大小情况。
对象内存大小

对于值类型,对象大小并不是简单为所有成员所占大小,必须考虑到内存对齐。对于引用类型,多了额外了对象头(16字节),并且内存对齐为8字节。对于array类型,则更为复杂。
primitive type

如熟知, byte占1字节,int占4字节,其他不赘述。
普通ValueType对象大小

在未指定Explicit Layout的情况下,根据字段大小及内存对齐规则计算出总大小,与c++的struct计算规则相似。这里不详细阐述,直接举例吧
// V1 对象大小 1
struct V1
{
    public byte a1;
}
// V2 对象大小 8
struct V2
{
    public byte a1;
    public int a2;
}
// V3 对象大小 24
struct V3
{
    public int a1;
    public int a2;
    public object a3;
    public byte a4;
}
class 类型内存占用

与 ValueType相似,但多了对象头的16字节,并且强制内存对齐为8字节。示例:
// C1 对象大小 24
class C1
{
    public byte a1;
}
// C2 对象大小 24
class C2
{
    public byte a1;
    public int a2;
}
// C3 对象大小 40
class C3
{
    public int a1;
    public int a2;
    public object a3;
    public byte a4;
}
对象内存大小对比

lua的计算规则略复杂,参见第三方文章。空table占56字节,每多一个字段至少多占32字节。
ILRuntime的类型除了enum外统一以IlTypeInstance表达,空类型占72字节,每多一个字段至少多用16字节。如果对象中包含引用类型数据,则整体又至少多24字节,并且每多一个object字段多8字节。
类型XluaILRuntimehuatuoV188881V21201048V318416824C1888824C212010424C318416840当前稳定性状况

技术评估上目前稳定性处于Beta版本。由于huatuo技术原理的先进性,bug本质上不多,稳定得非常快。已经有一段时间未收到2020.3.33版本的bug反馈。

[*]目前PC、Android、iOS 已跑通所有单元测试,可稳定体验使用。
[*]2022.6.7 第一款使用huatuo的android和iOS双端休闲游戏正式上线
[*]2022.7 将有至少2款重度项目和2款中度游戏上线
[*]2022年预计有几十款中重度项目及超过一百款轻度或者独立游戏上线
已经有几十个大中型游戏项目较完整地接入huatuo,并且其中一些在紧锣密鼓作上线前测试。具体参见收集的一些 完整接入的商业项目列表
总结

Huatuo是一个划时代的Unity平台C#原生热更新技术,它将国内Unity开发的技术框架水平提高到新的高度,并深刻地改变Unity平台的开发生态。

Baste 发表于 2022-7-2 13:13

[赞][赞][赞]

ChuanXin 发表于 2022-7-2 13:17

[红心]

fwalker 发表于 2022-7-2 13:22

有个问题,这么骚的操作会不会被苹果ban掉?
页: [1]
查看完整版本: Huatuo——划时代的Unity原生C#热更新技术