导读:Unity的脚本如何跨平台.想要了解Unity的热更原理,必须要先了解Unity脚本的编译和跨平台机制。通常游戏的跨平台主要指安卓和IOS端。Unity的官方脚本语言是C#,但也有不少项目会采用C#.+ Lua语言的方式进行开发。它们主要有三种跨平台的形式:JIT、AOT、脚本语
想要了解Unity的热更原理,必须要先了解Unity脚本的编译和跨平台机制。通常游戏的跨平台主要指安卓和IOS端。Unity的官方脚本语言是C#,但也有不少项目会采用C#
+ Lua语言的方式进行开发。它们主要有三种跨平台的形式:JIT、AOT、脚本语言。
Unity的C#代码在代码被打包时会被编译器变为成为中间语言IL(Intermediate
Language),而不是机器码(NativeCode,机器的可执行代码)。 后续对这些IL的编译方式不同可以分为AOT和JIT。
JIT是一种动态编译技术,是指Unity打包时将C#编译成IL后,在运行时.NET JIT编译器将IL翻译NativeCode的过程。通常IL由Mono
VM编译执行,这是因为Mono VM中包含了.NET JIT编译器。
下面是MonoVM的运行流程图,它就在运行时将IL编译成机器码并保存到内存中并执行。
指在程序程序运行前将代码变成称机器码,它不需要再运行时对代码进行解释和编译,这样可以提高程序的执行速度和安全性。Unity
AOT的跨平台原理是将程序的C#源代码在打包时编译成与平台无关的IL, 然后通过特定的编译器将IL代码编译成特定平台的Native
Code。不同平台只需要提供对应的编译器即可,无需在运行时对代码进行解释和编译,从而实现跨平台。
下面是Unity推荐的IL2CPP编译器原理图。在打包时把C#代码先编译成IL,再由IL2CPP编译器编译成C++代码,再由特定平台的C++编译器编译成NativeCode。
最后需要L2CPP VM的原因是虽然代码转换成了C++代码,但C#中的内存是由GC自动管理,而C++需要手动管理内存,因此还需要一个IL2CPP
VM用于GC管理等操作。
Lua是一种跨平台的脚本语言,它主要依赖解释器和虚拟机实现跨平台功能。正常的lua脚本跨平台流程是:
由于解释器和虚拟机都是跨平台的,lua脚本也就可以在不同的平台上运行了。
字节码(bytecode)指的是一种中间码,它是一种介于源代码和机器码之间的一种代码形式。字节码是针对特定虚拟机(如Java虚拟机、.NET CLR虚拟机)的指令集, 每条指令都比较简单,并且都能够被轻易地转换成机器码。字节码通常是在解释执行或者即时编译的过程中生成的,可以有效地提高程序的执行效率和跨平台能力。
在编译型语言中,源代码会被直接编译成机器码,而在解释型语言中,源代码则会被解释器逐行执行。相比之下,字节码的执行效率通常比解释器高,但比直接执行机器码要低。但是,字节码的好处在于它可以在多个平台上运行,只需要在特定平台上实现一个对应的解释器或者即时编译器即可。这也是为什么很多跨平台的语言(如Java和Lua)都采用了字节码的形式。
举个例子,Java源代码在编译时会被转换成Java字节码,然后在JVM上解释执行或者即时编译成机器码。这样,Java程序就可以在不同的操作系统和硬件上运行,只需要在不同平台上实现对应的JVM即可。
另外,lua也提供了JIT版本,以便在运行时将lua代码编译成NativeCode执行,与普通的lua解释器相比,可以显著地提升lua代码的执行速度。
Lua脚本语言是解释执行的,运行前加载更新后的代码就可以达到热更新的效果。在本文中要说明的是C#的热更新。
理想化的热更新流程是:
这种模式在PC和Android平台是可以的,但在IOS平台是不可行的。因为IOS对申请的内存禁止了可执行权限,所以运行时创建/加载的NativeCode是无法执行的。
void *mmap(void *start, size_t length, int prot, int flags, int fd, off_t
offsize)函数是申请内存的函数,它的第三个参数是代表了申请内存的保护方式:
而IOS平台是不支持PROT_EXEC的。运行时申请的内存不可执行,所以这种理想化的热更新流程无法在IOS平台起作用。
详细原因可以看下这篇文章。
为了解决IOS上的热更新问题,有两个主流方案:ILRuntime 和 HybridCLR。
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。
是一个特性完整、零成本、高性能、低内存 的近乎完美 的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平台的热更新。
正因为HybridCLR是原生Runtime级别实现,热更新部分的类型与主工程AOT部分类型是完全等价并且无缝统一的。可以随意调用、继承、反射、多线程,不需要生成代码或者写适配器。而其他热更新方案则是独立VM,与IL2CPP的关系本质上相当于Mono中嵌入lua的关系。因此类型系统不统一,为了让热更新类型能够继承AOT部分类型,需要写适配器,并且解释器中的类型不能为主工程的类型系统所识别。特性不完整、开发麻烦、运行效率低下。
HybirdCLR的原理细节Walon有分享,可以在其知乎专栏查看。
上一篇:产品代码都给你看了,可别再说不会
下一篇:大佬们 有没有unity游戏开发