编辑
2023-09-01
Unity
00

目录

Mono
1. Unity 和 Mono 的关系
2. Unity 跨平台的必备概念
2.1 Unity 的组成
编写;早期支持 C#/JavaScript/Boo;项目代码最终由 Mono 编译。
2.2 回顾 Mono 的跨平台原理
2.3 Mono 的主要构成
3. 跨平台的基本原理(Mono)
4. 基于 Mono 跨平台的优缺点
5. 与 .NET 生态的关系
IL2CPP
1. IL2CPP 是什么
2. IL2CPP 跨平台原理
3.IL2CPP可能带来的问题
代码裁剪问题
解决方案
泛型问题
解决方案
两者差异(Mono&IL2CPP)

Mono

1. Unity 和 Mono 的关系

  • Unity 背景:引擎底层主要由 C/C++ 实现。
  • 选择 Mono 的原因:为降低上层逻辑开发门槛、扩大开发者群,Unity 采用 Mono 作为上层脚本环境;Mono 同时具备 跨平台跨语言 特性,契合"一次开发,多平台运行"的目标。

2. Unity 跨平台的必备概念

2.1 Unity 的组成

  • Unity Engine(引擎):提供 UnityEngine.dllC/C++ 编写,含平台相关代码、图形 API、物理、灯光等底层能力。
  • Unity Editor(编辑器):提供 UnityEditor.dll;主要 C# 编写;早期支持 C#/JavaScript/Boo;项目代码最终由 Mono 编译

2.2 回顾 Mono 的跨平台原理

  • 基于 CLI 统一中间语言 CIL,再用 CLR/Mono VM 将 CIL 转为目标 OS 的原生代码运行。

2.3 Mono 的主要构成

  • C# 编译器(mcs)Mono Runtime(虚拟机):含 JIT、AOT、GC、类库加载器BCL 基础类库Mono 类库(扩展功能用于不同 OS 应用构建)。

3. 跨平台的基本原理(Mono)

  • 在 Unity 下使用多种语言实现逻辑,发布时编译为 IL;在对应 OS 上通过 Mono VM 把 IL 翻译为机器码并运行

图 1:Unity + Mono 执行流程

unity_mono_flow.png

4. 基于 Mono 跨平台的优缺点

  • 优点:只要在不同 OS 上实现 Mono VM,理论上可支持的平台 非常多(接近"无限")。
  • 缺点(补充说明)
    • 维护成本:Unity 升级需跟进 Mono 运行时与类库兼容;
    • 特性滞后:旧版 Mono 可能不支持最新 C# 语法/运行时能力(在工程中需注意 API/编译目标选择)。

5. 与 .NET 生态的关系

  • .NET Framework(2002):Windows 为主。
  • Mono(2004):开源、跨平台,早期/至今都是 .NET 的跨平台方案之一。
  • .NET Core(2016):官方跨平台实现,后统一为现代 .NET。

图 2:Unity 组件与 Mono 的位置关系

unity_components_mono.png

IL2CPP

1. IL2CPP 是什么

  • 定义:IL2CPP = IL → C++ 的脚本后处理方式;将 C# 编译生成的 IL 转译成 C++ 源码,再由各平台 C++ 编译器生成本机机器码直接运行;
  • 出现时间:在 Unity 4.6.1 p5 之后加入,作为继 Mono 之后的新型跨平台方案。

2. IL2CPP 跨平台原理

  • 核心流程IL →(IL2CPP 转译为C++代码)→ C++ →(各平台 C++ 编译器 AOT提前编译为目标平台的机器码)→ 原生机器码 → IL2CPP VM(GC/线程/运行时服务)
  • 要点
    • 借助各平台成熟的 C++ 编译器与优化链路,获得更高的运行效率与可移植性
    • 尽管中间形态变为 C++,但 内存管理仍遵循 C# 的 GC 语义;因此仍有 IL2CPP VM 承担 GC、线程等运行时服务。

图 3:IL2CPP 跨平台流程
il2cpp_note_il2cpp_flow.png

3.IL2CPP可能带来的问题

代码裁剪问题

IL2CPP在打包时会自动对Unity工程的DLL进行裁剪,将代码中没有引用到的类型裁剪掉,以达到减小发布后包的尺寸的目的;然而在实际使用过程中,很多类型有可能会被意外剪裁掉,造成运行时抛出找不到某个类型的异常。特别是通过反射等方式在编译时无法得知的函数调用,在运行时都很有可能遇到问题

解决方案

  1. IL2CPP处理模式时,将PlayerSetting->Other Setting->Managed Stripping Level(代码剥离)设置为Low,Disable: Mono模式下才能设置为不删除任何代码(但是没必要),IL2CPP模式下是一定会进行代码裁剪的,只不过是优先等级高低的问题,但不推荐使用这种方式,这种方式还是不保守的不可控的,并不知道它会删除那些代码,优先使用方法2 image.png
  • Low:默认低级别,保守的删除代码,删除大多数无法访问的代码,同时也最大程度减少剥离实际使用的代码的可能性
  • Medium:中等级别,不如低级别剥离谨慎,也不会达到高级别的极端
  • Hight:高级别,尽可能多的删除无法访问的代码,有限优化尺寸减小。如果选择该模式一般需要配合link.xml使用
  1. 通过Unity提供的link.xml方式来告诉Unity引擎,哪些类型是不能够被剪裁掉的,在Unity工程的Assets目录中(或其任何子目录中)建立一个叫link.xml(完全限定名字为link)的XML文件

内容示例:

xml
<?xml version="1.0" encoding="UTF-8"?> <!--保存整个程序集--> <assembly fullname="UnityEngine" preserve="all"/> <!--没有“preserve”属性,也没有指定类型意味着保留所有--> <assembly fullname="UnityEngine"/> <!--完全限定程序集名称--> <assembly fullname="Assembly-CSharp, Version=0.0.0.0, Culture=neutral, PublicKeyToken=null"> <type fullname="Assembly-CSharp.Foo" preserve="all"/> </assembly> <!--在程序集中保留类型和成员--> <assembly fullname="Assembly-CSharp"> <!--保留整个类型--> <type fullname="MyGame.A" preserve="all"/> <!--没有“保留”属性,也没有指定成员 意味着保留所有成员--> <type fullname="MyGame.B"/> <!--保留类型上的所有字段--> <type fullname="MyGame.C" preserve="fields"/> <!--保留类型上的所有方法--> <type fullname="MyGame.D" preserve="methods"/> <!--只保留类型--> <type fullname="MyGame.E" preserve="nothing"/> <!--仅保留类型的特定成员--> <type fullname="MyGame.F"> <!--类型和名称保留--> <field signature="System.Int32 field1" /> <!--按名称而不是签名保留字段--> <field name="field2" /> <!--方法--> <method signature="System.Void Method1()" /> <!--保留带有参数的方法--> <method signature="System.Void Method2(System.Int32,System.String)" /> <!--按名称保留方法--> <method name="Method3" /> <!--属性--> <!--保留属性--> <property signature="System.Int32 Property1" /> <property signature="System.Int32 Property2" accessors="all" /> <!--保留属性、其支持字段(如果存在)和getter方法--> <property signature="System.Int32 Property3" accessors="get" /> <!--保留属性、其支持字段(如果存在)和setter方法--> <property signature="System.Int32 Property4" accessors="set" /> <!--按名称保留属性--> <property name="Property5" /> <!--事件--> <!--保存事件及其支持字段(如果存在),添加和删除方法--> <event signature="System.EventHandler Event1" /> <!--根据名字保留事件--> <event name="Event2" /> </type> <!--泛型相关保留--> <type fullname="MyGame.G`1"> <!--保留带有泛型的字段--> <field signature="System.Collections.Generic.List`1&lt;System.Int32&gt; field1" /> <field signature="System.Collections.Generic.List`1&lt;T&gt; field2" /> <!--保留带有泛型的方法--> <method signature="System.Void Method1(System.Collections.Generic.List`1&lt;System.Int32&gt;)" /> <!--保留带有泛型的事件--> <event signature="System.EventHandler`1&lt;System.EventArgs&gt; Event1" /> </type> <!--如果使用类型,则保留该类型的所有字段。如果类型不是用过的话会被移除--> <type fullname="MyGame.I" preserve="fields" required="0"/> <!--如果使用某个类型,则保留该类型的所有方法。如果未使用该类型,则会将其删除--> <type fullname="MyGame.J" preserve="methods" required="0"/> <!--保留命名空间中的所有类型--> <type fullname="MyGame.SomeNamespace*" /> <!--保留名称中带有公共前缀的所有类型--> <type fullname="Prefix*" /> </assembly> </linker>

泛型问题

  • IL2CPP和Mono最大的区别是 不能在运行时动态生成代码和类型(反射) ,IL2CPP 必须在编译阶段就把所有需要用到的泛型类型实例化好。如果在打包生成前某个泛型组合没有在代码中显式用过,编译器不会生成它的 C++ 对应实现,运行时就会报 “找不到方法/类型” 的错误;
  • 举例: List<A>List<B> 中A和B是我们自定义的类,我能必须在代码中显示的调用过,IL2CPP才能保留List<A>List<B>两个类型。如果在热更新时我们调用List<C>,但是它之前并没有在代码中显示调用过,那么这时就会出现报错等问题。主要就是因为JITAOT两个编译模式的不同造成的什么是JIT和AOT

解决方案

  • 泛型类:随便声明一个类,然后在这个类中声明一些public的泛型类的变量(告诉IL2CPP这个泛型类被引用,被使用过了)
  • 泛型方法:随便写一个静态方法,在将这个泛型方法在其中调用一下。这个静态方法无需被调用,这样做的目的其实就是在预言编译之前让IL2CPP知道我们需要使用这个内容;

两者差异(Mono&IL2CPP)

对比维度Mono(JIT 即时编译)IL2CPP(AOT 提前编译)
打包速度快 —— 直接生成 IL,打包流程简单慢 —— 需 IL → C++ → 各平台编译 → 本机码
运行效率慢 —— 运行时逐步翻译 IL,性能有损耗快 —— 直接执行原生机器码,效率更高
灵活性高 —— 支持 JIT、反射、动态类型生成低 —— 必须编译期确定类型,反射/泛型需预保留
平台支持部分平台受限(如 WebGL、iOS 等)跨平台兼容性更好,官方推荐用于正式发布
维护成本高 —— 需要持续维护各平台 Mono VM相对低 —— 借助各平台成熟 C++ 编译链路
适用场景开发调试阶段、快速迭代正式打包、上线发布,追求性能与兼容性

本文作者:xuxuxuJS

本文链接:

版权声明:本博客所有文章除特别声明外,均采用 BY-NC-SA 许可协议。转载请注明出处!