Unity热更新那些事

目录

  • 热更新方案
  • Unity程序的两种编译方式
    • 编译阶段
    • 执行阶段
      • Mono方式
      • IL2CPP方式
      • 两种方式打包以后的项目目录结构
    • 其他
  • ILRuntime热更新
    • ILRuntime使用注意
    • ILRuntime的实现原理
    • ILRuntime的性能优化建议
    • ILRuntime的性能优化建议
  • HybridCLR热更新

参考链接
Unity热更新那些事
一小时极速掌握ILRuntime热更新
一小时极速掌握HybridCLR热更新

热更新方案

Unity热更新那些事_第1张图片

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”文件夹)

Unity热更新那些事_第2张图片

执行阶段

基本概念

  • CLR:通用语言运行平台(Common Language Runtime),是微软的.Net虚拟机
  • 主要作用:
    • 编译 – 运行前把C#编译为CIL
    • 运行 – 在运行的时候把CIL转换为各平台的原生码(安卓:ARM指令集,windows:x86、x64指令集)

Unity热更新那些事_第3张图片

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,具体取决于目标平台
    Unity热更新那些事_第4张图片

IL2CPP方式脚本编译流程

  • IL2CPP做的改变由下图红色部分标明
  • 在得到中间语言IL后,使用IL2CPP将他们重新变回C++代码,然后再由各个平台的C++编译器直接编译成能执行的原生汇编代码
    Unity热更新那些事_第5张图片

IL2CPP的优缺点

  • 优点
    • 运行速度快(CPP转原生码比CIL快)
    • 减少Unity公司的维护成本(Mono VM官方不支持这么多平台,所以很多平台的Mono VM都需要Unity自己维护,而C++编译器是各个平台现成的)
  • 缺点
    • 包体会变大
    • 编译速度慢
    • 不支持JIT

两种方式打包以后的项目目录结构

Unity热更新那些事_第6张图片

其他

IOS平台热更的困境

  • Unity只有IL2CPP模式的才支持64位系统,Mono不支持
  • 苹果在2016年1月要求所有新上架游戏必须支持64位架构
  • IOS系统禁止动态加载代码到内存并执行
    Unity热更新那些事_第7张图片

总结: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的水平。

你可能感兴趣的:(Unity,unity,游戏引擎,热更新)