目录
- 热更新方案
- Unity程序的两种编译方式
- 编译阶段
- 执行阶段
- Mono方式
- IL2CPP方式
- 两种方式打包以后的项目目录结构
- 其他
- ILRuntime热更新
- ILRuntime使用注意
- ILRuntime的实现原理
- ILRuntime的性能优化建议
- ILRuntime的性能优化建议
- HybridCLR热更新
参考链接
Unity热更新那些事
一小时极速掌握ILRuntime热更新
一小时极速掌握HybridCLR热更新
热更新方案
Unity程序的两种编译方式
- Mono方式
- IL2Cpp方式
编译阶段
执行:源码 -> 四个项目 -> 动态链接库(dll文件) -> CIL(通用汇编语言)
顺序:firstpass,Editor-firstpass->Editor,CSharp
动态链接库 | 对应 |
---|---|
Assembly-CSharp | 自己写的C#程序代码脚本 |
Assembly-CSharp-Editor | 编辑器相关脚本(需要创建“Editor”文件夹) |
Assembly-CSharp-Editor-firstpass | 编辑器插件 |
Assembly-CSharp-firstpass | 插件(需要创建“Plugins”文件夹) |
执行阶段
基本概念
- CLR:通用语言运行平台(Common Language Runtime),是微软的.Net虚拟机
- 主要作用:
- 编译 – 运行前把C#编译为CIL
- 运行 – 在运行的时候把CIL转换为各平台的原生码(安卓:ARM指令集,windows:x86、x64指令集)
Mono方式
- 一个基于CLR的开源项目,允许引擎和用户的托管代码运行在每一个目标平台上
- Mono支持的平台:Anfroid,Apple IOS,Linux,Windows等
跨平台原理
- 把C#通过Mono complier(其他语言用的是Unity单独开发的一个Unity complier),编译为CIL语言
- 各个平台下的Mono虚拟机,运行CIL语言,转换成原生码给CPU执行
Mono虚拟机如何运行CIL
- JIT(Just In TIme)模式 – 在编译的时候,把C#编译成CIL,在运行时逐条读入,逐条解析翻译成原生码交给CPU再执行;
- AOT(Ahead Of Time)模式 – 在编译成CIL之后,会把CIL再处理一遍编译为原生码,运行时交给CPU直接执行,Mono下的AOT只会处理部分的CIL,还有一部分CIL采用了JIT模式;
- Full AOT模式 – 在编译为CIL之后,把所有的CIL编译为原生码,在运行的时候直接执行(ios平台只能使用这种)
IL2CPP方式
IL2CPP会在项目转成CIL之后,再把CIL转为CPP,然后在运行的时候把CPP加载进来,由各个平台的IL2PP VM转换为原生码
IL2CPP工作原理
使用IL2CPP开始构建时,Unity会自动执行以下步骤:
- 将Unity Scripting API代码编译为常规.NET DLL(托管程序集)
- 应用托管字节码剥离(此步骤可显著减小构建的游戏大小)
- 将所有托管程序集转换为标准C++代码
- 使用本机平台编译器编译生成的C++代码和IL2CPP的运行时部分
- 将代码链接到可执行文件或DLL,具体取决于目标平台
IL2CPP方式脚本编译流程
- IL2CPP做的改变由下图红色部分标明
- 在得到中间语言IL后,使用IL2CPP将他们重新变回C++代码,然后再由各个平台的C++编译器直接编译成能执行的原生汇编代码
IL2CPP的优缺点
- 优点
- 运行速度快(CPP转原生码比CIL快)
- 减少Unity公司的维护成本(Mono VM官方不支持这么多平台,所以很多平台的Mono VM都需要Unity自己维护,而C++编译器是各个平台现成的)
- 缺点
- 包体会变大
- 编译速度慢
- 不支持JIT
两种方式打包以后的项目目录结构
其他
IOS平台热更的困境
- Unity只有IL2CPP模式的才支持64位系统,Mono不支持
- 苹果在2016年1月要求所有新上架游戏必须支持64位架构
- IOS系统禁止动态加载代码到内存并执行
总结:C#脚本限制
- IOS系统禁止动态加载代码到内存,并执行
- 反射:
- System.Reflection可用(只要编译器可以推断通过反射使用的代码需要在运行时存在)
- System.Reflection.Emit命名空间中的任何方法不可用
- 序列化:
- 如果一个类型或一个方法仅通过反射被创建或被调用,则AOT编译器无法检测到需要为该类型或方法生成代码
- 泛型虚方法:
- 泛型虚方法由于在编译时类型不确定,编译器也不会在编译期生成针对特定类型的泛型方法调用
解决方案
采用解释执行语言,而非编译执行
- Lua:Tolua/Xlua
- C#:ILRuntime
ILRuntime热更新
官方文档
ILRuntime项目为基于C#的平台(例如Unity)提供了一个纯C#实现,快速、方便且可靠的IL运行时,使得能够在不支持JIT的硬件环境(如iOS)能够实现代码的热更新
ILRuntime使用注意
- 跨域委托:需要额外添加适配器或者转换器
- 跨域继承:如果想在热更DLL项目当中继承/实现一个Unity主工程里的类/接口,需要在Unity主工程中实现一个继承适配器,并注册
- 反射转换:热更工程中的IL类型和C#类型系统不能混用,要类型映射后使用
- CLR重定向
- CLR绑定
ILRuntime的实现原理
- ILRuntime借助Mono.Cecil库来读取DLL的PE信息,以及当中类型的所有信息,最终得到方法的IL汇编码,然后通过内置的IL解译执行虚拟机来执行DLL中的代码
- 为了高性能进行运算,尤其是栈上的基础类型运算,如int,float,long之类类型的运算,直接借助C#的Stack类实现IL托管栈肯定是个非常糟糕的做法。因为这意味着每次读取和写入这些基础类型的值,都需要将他们进行装箱和拆箱操作,这个过程会非常耗时并且会产生巨量的GC Alloc,使得整个运行时执行效率非常低下
- 因此ILRuntime使用unsafe代码以及非托管内存,实现了自己的IL托管栈。
ILRuntime的性能优化建议
- 在Release模式下进行性能测试
- 关闭Development Build选项来发布Unity项目
- 避免GC:
- ILRuntime跨域调用默认采用反射,这种方式少用,多用CLR绑定或基于InvocationContext的调用
- 基于IL托管栈重新实现值类型的代码绑定(使用unsafe代码以及非托管内存)
- 在频繁调用的方法(例如Update方法)上避免使用params可变参数列表(会new数组出来)
ILRuntime的性能优化建议
- 不依赖MonoBehaviour的代码框架
- 自动化CLR绑定代码生成
- 与Addressable资源管理和热更系统的结合
HybridCLR热更新
官方文档
-
HybridCLR是一个特性完整、零成本、高性能、低内存的近乎完美的Unity全平台原生c#热更方案。
-
HybridCLR扩充了il2cpp的代码,使它由纯AOT runtime变成AOT+Interpreter 混合runtime,进而原生支持动态加载assembly,使得基于il2cpp backend打包的游戏不仅能在Android平台,也能在IOS、Consoles等限制了JIT的平台上高效地以AOT+interpreter混合模式执行,从底层彻底支持了热更新。
-
HybridCLR不仅支持传统的全解释执行模式,还开创性地实现了 Differential Hybrid Execution(DHE) 差分混合执行技术。即可以对AOT dll任意增删改,会智能地让变化或者新增的类和函数以interpreter模式运行,但未改动的类和函数以AOT方式运行,让热更新的游戏逻辑的运行性能基本达到原生AOT的水平。