首页 > 编程语言 >UnityC#脚本的热更新原理

UnityC#脚本的热更新原理

时间:2023-05-05 23:58:44浏览次数:56  
标签:脚本 ILRuntime C# 代码 跨平台 IL2CPP Unity

Unity的脚本如何跨平台

想要了解Unity的热更原理,必须要先了解Unity脚本的编译和跨平台机制。通常游戏的跨平台主要指安卓和IOS端。Unity的官方脚本语言是C#,但也有不少项目会采用C# + Lua语言的方式进行开发。它们主要有三种跨平台的形式:JIT、AOT、脚本语言。
Unity的C#代码在代码被打包时会被编译器变为成为中间语言IL(Intermediate Language),而不是机器码(NativeCode,机器的可执行代码)。后续对这些IL的编译方式不同可以分为AOT和JIT。

20230505232944

JIT(Just In Time)

JIT是一种动态编译技术,是指Unity打包时将C#编译成IL后,在运行时.NET JIT编译器将IL翻译NativeCode的过程。通常IL由Mono VM编译执行,这是因为Mono VM中包含了.NET JIT编译器。
下面是MonoVM的运行流程图,它就在运行时将IL编译成机器码并保存到内存中并执行。
20230505233132

AOT(Ahead Of Time)

指在程序程序运行前将代码变成称机器码,它不需要再运行时对代码进行解释和编译,这样可以提高程序的执行速度和安全性。Unity AOT的跨平台原理是将程序的C#源代码在打包时编译成与平台无关的IL然后通过特定的编译器将IL代码编译成特定平台的Native Code。不同平台只需要提供对应的编译器即可,无需在运行时对代码进行解释和编译,从而实现跨平台。
下面是Unity推荐的IL2CPP编译器原理图。在打包时把C#代码先编译成IL,再由IL2CPP编译器编译成C++代码,再由特定平台的C++编译器编译成NativeCode。
最后需要L2CPP VM的原因是虽然代码转换成了C++代码,但C#中的内存是由GC自动管理,而C++需要手动管理内存,因此还需要一个IL2CPP VM用于GC管理等操作。
20230505233206

脚本语言

Lua是一种跨平台的脚本语言,它主要依赖解释器和虚拟机实现跨平台功能。正常的lua脚本跨平台流程是:

  1. 编写lua脚本
  2. 使用lua解释器将lua脚本解释称字节码
  3. 由lua虚拟机执行字节码

由于解释器和虚拟机都是跨平台的,lua脚本也就可以在不同的平台上运行了。

字节码(bytecode)指的是一种中间码,它是一种介于源代码和机器码之间的一种代码形式。字节码是针对特定虚拟机(如Java虚拟机、.NET CLR虚拟机)的指令集,每条指令都比较简单,并且都能够被轻易地转换成机器码。字节码通常是在解释执行或者即时编译的过程中生成的,可以有效地提高程序的执行效率和跨平台能力。
在编译型语言中,源代码会被直接编译成机器码,而在解释型语言中,源代码则会被解释器逐行执行。相比之下,字节码的执行效率通常比解释器高,但比直接执行机器码要低。但是,字节码的好处在于它可以在多个平台上运行,只需要在特定平台上实现一个对应的解释器或者即时编译器即可。这也是为什么很多跨平台的语言(如Java和Lua)都采用了字节码的形式。
举个例子,Java源代码在编译时会被转换成Java字节码,然后在JVM上解释执行或者即时编译成机器码。这样,Java程序就可以在不同的操作系统和硬件上运行,只需要在不同平台上实现对应的JVM即可。

另外,lua也提供了JIT版本,以便在运行时将lua代码编译成NativeCode执行,与普通的lua解释器相比,可以显著地提升lua代码的执行速度。

C#如何热更新

Lua脚本语言是解释执行的,运行前加载更新后的代码就可以达到热更新的效果。在本文中要说明的是C#的热更新。
理想化的热更新流程是:

  1. 把需要更新的代码编译成动态链接库
  2. 游戏启动时加载新的动态链接库
  3. 用反射的形式获取动态链接库中的实例或方法

这种模式在PC和Android平台是可以的,但在IOS平台是不可行的。因为IOS对申请的内存禁止了可执行权限,所以运行时创建/加载的NativeCode是无法执行的。
void *mmap(void *start, size_t length, int prot, int flags, int fd, off_t offsize)函数是申请内存的函数,它的第三个参数是代表了申请内存的保护方式:

  • PROT_EXEC 映射区域可被执行
  • PROT_READ 映射区域可被读取
  • PROT_WRITE 映射区域可被写入

而IOS平台是不支持PROT_EXEC的。运行时申请的内存不可执行,所以这种理想化的热更新流程无法在IOS平台起作用。
详细原因可以看下这篇文章
为了解决IOS上的热更新问题,有两个主流方案:ILRuntime 和 HybridCLR。

ILRuntime

Unity会把C#代码打包成DLL,ILRuntime在运行时用自己的解释器来解释IL并执行,而不是直接调用.NET FrameWork或Mono虚拟机来运行代码。它借助Mono.Cecil库来读取DLL的PE信息,以及当中类型的所有信息,最终得到方法的IL汇编码,然后通过内置的IL解译执行虚拟机来执行DLL中的代码。

但是ILRuntime会有一些限制,见https://ourpalm.github.io/ILRuntime/public/v1/guide/FastQA.html

  1. ILRuntime和原始的 compiler是两套东西,也就是说你的热更DLL和主工程的DLL实质是不互通的(如热更DLL中一个类要继承主工程DLL的一个类),所以就存在跨域问题,需要写委托适配器,委托转换器。在发布版本后这些不能热更,使用之前一定要预留好可能会使用的
  2. 部分 C# 语法不支持:由于 ILRuntime 是基于 Mono 实现的,而 Mono 不支持所有 C# 语法,所以 ILRuntime 在某些 C# 语法方面也有限制,比如属性、泛型委托、可选参数等
  3. 需要特殊处理的代码:由于 ILRuntime 的实现方式,一些特殊的代码需要进行特殊处理,比如反射、LINQ、协程等
  4. 性能问题:由于 ILRuntime 需要动态解析和执行代码,相对于编译时静态绑定的方式,其性能会有一定程度的下降。同时,在使用过程中也需要注意避免频繁的跨域调用和反射操作,以免影响性能
  5. ILRuntime对多线程Thread不兼容,在热更代码里使用多线程会导致Unity崩溃闪退

HybridCLR

是一个特性完整、零成本、高性能、低内存近乎完美的Unity全平台原生c#热更方案。
IL2CPP是一个纯静态的AOT运行时,不支持运行时加载dll,因此不支持热更新。HybridCLR扩充了IL2CPP的代码,使其由纯AOT Runtime变成“AOT+Interpreter”混合Runtime,进而原生支持动态加载Assembly,使得基于IL2CPP打包的游戏不仅能在Android平台,也能在IOS、Consoles等限制了JIT的平台上高效地以AOT+interpreter混合模式执行。

ILRuntime是引入一个第三方VM(Virtual Machine),在VM中解释执行代码,来实现热更新。这些热更新方案的VM与IL2CPP是独立的,意味着它们的元数据是不相通的,在热更新里新增一个类型是无法被IL2CPP所识别的例如通过System.Activator.CreateInstance是不可能创建出这个热更新类型的实例),这种看起来像、实际上却又不是的伪CLR虚拟机,在与IL2CPP这种复杂的CLR运行时交互时,产生极大量的兼容性问题,另外还有严重的性能问题。
HybridCLR对IL2CPP运行时进行扩充,添加interpreter模块,进而实现像Mono一样的混合执行模式。这样一来就能彻底支持热更新了,并且兼容性极佳。对开发者来说,除了以解释模式运行的部分执行得比较慢,其他方面跟标准的运行时没有区别。
通俗地说,il2cpp相当于Mono的AOT模块,HybridCLR相当于Mono的interpreter模块,两者合一成为完整Mono。HybridCLR使得IL2CPP变成一个全功能的Runtime,原生(即通过System.Reflection.Assembly.Load)支持动态加载dll,从而支持ios平台的热更新。
20230505233235
正因为HybridCLR是原生Runtime级别实现,热更新部分的类型与主工程AOT部分类型是完全等价并且无缝统一的。可以随意调用、继承、反射、多线程,不需要生成代码或者写适配器。而其他热更新方案则是独立VM,与IL2CPP的关系本质上相当于Mono中嵌入lua的关系。因此类型系统不统一,为了让热更新类型能够继承AOT部分类型,需要写适配器,并且解释器中的类型不能为主工程的类型系统所识别。特性不完整、开发麻烦、运行效率低下。

HybirdCLR的原理细节Walon有分享,可以在其知乎专栏查看。

参考

标签:脚本,ILRuntime,C#,代码,跨平台,IL2CPP,Unity
From: https://www.cnblogs.com/silence394/p/17375732.html

相关文章

  • 用 VSCode 调试网页的 JS 代码有多香
    用VSCode调试网页的JS代码有多香Javascript代码主要有两个运行环境,一个是Node.js,一个是浏览器。一般来说,调试Node.js上跑的JS代码我会用VSCode的debugger,调试浏览器上的JS代码我会用chromedevtools。 相比纯看代码来说,我更推荐结合debugger来看,它可以让......
  • TCP的三次握手和四次挥手分析
    一、tcp报文格式主要关注的字段为:源端口号(SourcePort),目的端口号(DestinationPort)序列号seq(SequenceNumber)确认号ack(AcknowledgmentNumber)标志位:ACK,SYN,FIN二、三次握手客户端将TCP报文标志位SYN置为1,随机产生一个序号值seq=x,发送给服务端。发送完毕后,客户端进入SYN_......
  • Cesium中的Globe.js
    Globe顾名思义就是地球的意思吧。先看构造函数:functionGlobe(ellipsoid){ellipsoid=defaultValue(ellipsoid,Ellipsoid.WGS84);varterrainProvider=newEllipsoidTerrainProvider({ellipsoid:ellipsoid,});varimageryLayerCollection=newImagery......
  • c++打卡第十九天
    一、问题描述 二、设计思路。①、定义总鱼数为x条,这个x我们需要使用double类型定义,②、第一次剩余x1=x-(x/2+1/2);第二次剩余x2=x1-(x1/3+1/3)第三次剩余x3=x2-(x2/4+1/4)第四次剩余x4=x3-(x3/5+1/5)x4=11;③、我们可以使用循环实现此方程。即x-(x/j+1/j);③、使用数组存储每次卖鱼......
  • CF1093E
    首先将将\(b_i\)的定义改为\(b_i\)在\(a\)中出现的位置\(pos\),那么询问操作就是询问\(b_{[l_b,r_b]}\)中有几个数的值在\([l_a,r_a]\)中。因为时间有\(\texttt{6.00S}\)且\(n,m\le2\times10^5\),所以考虑分块。根据套路,进行块内排序,对于询问操作,散块直接暴力统......
  • SpringMVC----入门
    Springmvc在Spring特征里面处于满核心的地位,在官网上的对Spring特征(FEATURES)罗列中,对应这是“MODERNWEB”(现代web),也就是Spring特征的第一项。也反映了其重要作用,另一方面也是因为mvc在Spring项目中诞生比较早。只要是同网络应用相关的,无论是有同用户互动的(带UI的)或者没有互动......
  • C++类模板(模板类)
    参考资料:C++类模板(模板类)详解(biancheng.net) 人们需要编写多个形式和功能都相似的函数,因此有了函数模板来减少重复劳动;人们也需要编写多个形式和功能都相似的类,于是 C++ 引人了类模板的概念,编译器从类模板可以自动生成多个类,避免了程序员的重复劳动。例如,在《C++运算符重......
  • ChatGPT最全提示词Prompts总结,看这一篇就够了!
    以下几乎涵盖了各类人群想要使用ChatGPT的所有提示词,需要的朋友可以直接复制粘贴使用。从翻译到整理耗费超过2个小时,如果内容对大家有帮助,请不要吝啬你们的喜欢、点赞、关注~​如何正确的提问?担任创业技术律师我将要求您准备一页纸的设计合作伙伴协议草案,该协议是一家拥有I......
  • LeetCode 周赛 343(2023/04/30)结合「下一个排列」的贪心构造问题
    本文已收录到AndroidFamily,技术和职场问题,请关注公众号[彭旭锐]提问。大家好,我是小彭。今天是五一假期的第二天,打周赛的人数比前一天的双周赛多了,难道大家都只玩一天吗?这场周赛是LeetCode第343场单周赛,如果不考虑第一题摆烂的翻译,整体题目质量还是很不错哒。往期回顾:L......
  • CTFhub-HTTP-302跳转
    题目环境: 访问题目中的链接:http://challenge-42aaee4c28b9e47d.sandbox.ctfhub.com:10800查看网页的源码:没找到有价值的信息 用burpsuit做拦截,重放测试: flag:ctfhub{2e9248c0c0ec4618842e3198} ......