• Unity热更新那些事


    参考链接
    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的水平。

  • 相关阅读:
    将已有jar包放进maven仓库
    动态链接库与可执行文件
    smqtt:高性能开源MQTT消息代理Broker
    css定位详解
    Vue数据响应Object.defineProperty
    Qt的Pro文件
    关于编程本质那些事
    预测和分类钻孔的毛刺钻孔切削ANN预测
    RustDay03——记录刷完Rust100题
    (AVL)平衡二叉树
  • 原文地址:https://blog.csdn.net/kiritoV/article/details/134116388