>> >> >> Reference << << << <<<<<<Ref>>>>>>
>> >> >> Indexer << << << <<<<<<Idx>>>>>>
Matched: 0

Tags

    Categories

      Types

        Top Results

          Mono and IL2CPP
          M: 2025-06-01 - ljf12825

          MonoIL2CPP都是Unity的脚本运行后端(Scripting Backend),它们是两中IL语言的处理方式;Mono的处理方式类似C#程序的执行,而IL2CPP则是将其转化为C++代码,交由C++编译器处理

          Mono

          Mono是一个跨平台的.NET运行时实现,最初由Xamarin公司开发,用来在非Windows平台上运行C#程序;Unity在早期就选择了Mono作为C#脚本运行环境

          核心组件:

          • Mono运行时:执行托管代码的核心引擎
          • 即时编译器(JIT):在运行时将IL代码编译为原生机器码
          • 提前编译器(AOT):可选功能,在运行前编译部分代码
          • 类库:实现.NET基础类库

          特点:

          • 解释执行 + JIT
            • 脚本代码(C#) -> 编译成CIL(Common Intermediate Language,通用中间语言)
            • Mono会在运行时JIT(即时编译)把CIL编译成机器码执行
          • 跨平台性强:一次编译的C#程序可以在多个平台跑
          • 反射支持强:很多第三方库依赖反射,Mono可以支持
          • 灵活,但性能不如原生代码:因为JIT/解释执行比不上直接生成平台原生代码

          局限性:

          • 性能开销:JIT编译和GC可能引起卡顿
          • 安全风险:IL代码较容易被反编译
          • 平台限制:某些平台(如iOS)不允许JIT编译

          IL2CPP

          IL2CPP是Unity自己研发的脚本后端,意为:

          • IL -> C++ -> 平台原生代码(AOT编译)

          工作流程:

          1. C#脚本 -> 编译成IL(和Mono一样的中间语言)
          2. Unity的IL2CPP工具链把IL翻译成C++代码
          3. 使用C++编译器编译生成目标平台的机器码
          4. 最终得到一个完全原生的二进制可执行文件

          特点:

          • AOT(Ahead of Time编译):运行前就已经编译成机器码,运行时无需JIT
          • 性能更高:执行效率比Mono/JIT方案高,平均有1.2-2倍的性能提升
          • 平台兼容性更好:特别是iOS上,苹果严格限制JIT,IL2CPP就成了唯一选择
          • 更安全:因为代码已经转成C++,反编译难度更大(虽然还是能逆向,但比Mono的CIL难得多)
          • 内存效率:更好的内存布局和缓存利用率

          局限性:

          • 构建时间延长:额外的转换和编译步骤
          • 调试困难:生成的C++代码难以直接调试
          • 灵活性降低:不支持运行时代码生成
          • 体积增大:最终二进制文件通常更大

          总体流程

          1. 从C#到IL(中间语言) 无论是Mono还是IL2CPP,第一步都是一样的:
          • C#脚本(例如Player.cs
          • Unity调用Roslyn编译器(csc.exe)把C#源码编译成IL(中间语言,Common Intermediate Language)
            • 输出结果是一个或多个.dll程序集(如Assembly-CSharp.dll

          这一步和普通.NET程序是一样的

          1. Mono路线(编辑器/Debug模式常用) Mono的工作流是:
          • 加载程序集:Unity编辑器或游戏运行时,Mono加载你的Assembly-Csharp.dll
          • JIT编译:Mono运行时逐个把IL转换成本地机器码,存在内存里
            • 执行时翻译 -> 所以叫JIT(Just-In-Time)
          • 执行机器码:CPU直接执行

          特点:

          • 编译快:改一行代码 -> 重新编译成dll -> Mono直接跑
          • 运行稍慢:因为每次方法第一次调用时,Mono都要现编译一下
          • 调试友好:断点、反射、热重载都支持
          1. IL2CPP路线(发布版本常用) IL2CPP的流程比Mono多几步
          • C#编译成IL(.dll)(和上一步一样)
          • IL2CPP工具链把.dll里的IL代码转换成C++源码
            • 例如Player.cs -> Assembly-CSharp.dll -> il2cpp -> Assembly-CSharp.cpp
          • C++编译器(Clang/MSVC/GCC)把生成的C++源码编译成目标平台的原生机器码(.exe/.apk/.ipa/.so/.dll等)
          • 运行时执行:游戏运行时直接执行已经编译好的机器码

          特点

          • 性能好:不需要JIT,运行时就是纯机器码,接近C++程序的速度
          • 安全性高:反编译难度比Mono大很多
          • 编译慢:因为每次都要跑C++编译器

          流程图


          Mono vs IL2CPP

          特性MonoIL2CPP
          编译方式JIT / 解释执行AOT(IL → C++ → 原生机器码)
          性能中等,依赖 JIT 优化高,接近 C++ 原生性能
          平台支持跨平台,但某些平台有限制(如 iOS 禁止 JIT)全平台,尤其是移动端和主机端首选
          反射完整支持部分受限,需要 link.xml 保留
          编译时间快,开发迭代效率高慢,需要 C++ 编译整个工程
          安全性容易被反编译(ILSpy 直接看源码)较难逆向(但不是绝对安全)

          使用场景:
          选择Mono的情况

          • 开发阶段快速迭代
          • 需要动态加载代码或使用反射
          • 目标平台允许JIT执行
          • 项目对性能要求不高
          • 需要最小化构建体积

          选择IL2CPP的情况

          • 发布到iOS平台
          • 需要最大化运行时性能
          • 项目对安全性要求高
          • 目标设备内存有限
          • 使用复杂泛型或值类型

          Unity的选择

          • 编辑器模式/PC Debug模式 -> Mono(调试快、编译快)
          • iOS/主机平台/大多数正式发布版本 -> IL2CPP(性能、安全性)
          • Android -> 可以选Mono或IL2CPP(推荐IL2CPP)

          Unity的策略其实是

          • 开发时用Mono提高迭代效率
          • 打包发布时用IL2CPP提高性能和安全

          迁移注意事项: 从Mono迁移到IL2CPP时需要注意:

          • 反射限制:某些反射操作可能不再工作
          • 序列化变化:二进制序列化可能有差异
          • 平台特定代码:需要检查平台相关代码的兼容性
          • 第三方库:确保所有插件支持IL2CPP
          • 构建配置:可能需要调整链接器设置

          Unity Packaging and Building

          在Unity中构建时,无法直接修改Mono或IL2CPP的底层C/C++代码,因为它们是与Unity引擎核心打包在一起的编译好的二进制库
          但是,Mono和IL2CPP的选择并非一个简单的构建选项切换,
          它是一项核心的架构决策,直接影响项目的性能特征、内存模型、平台兼容性以及后期优化策略
          所以理解它们的行为差异,并以此指导上层C#代码编写、项目架构设计和性能优化策略很重要

          内存管理

          GC的深层机制对比

          Mono

          Mono使用的是BoehmGC

          BoehmGC全称:Boehm-Demers-Weiser Conservative Garbage Collector,由Hans Boehm(HP实验室)编写,它是一种保守式垃圾回收器(Conservative Garbage Collector),它的主要作用是为C/C++/其他非托管语言提供垃圾回收支持;Mono在早期(Unity 4.x ~ 2018.1之前)选择直接集成现成的、稳定的BoehmGC作为默认垃圾回收器

          BoehmGC的工作原理
          和现代.NET CLR / Unity新GC不一样,BoehmGC是保守式GC,它具有以下特点

          • 保守式(Conservative)
            • BoehmGC不严格知道哪些是指针,哪些是普通函数
            • 它通过扫描堆栈、寄存器、全局变量的值,看起来像“指针”的东西就当作指针
            • 好处:兼容C/C++等没有精确元数据的语言
            • 坏处:可能会误判 -> 内存无法释放(内存泄漏风险)
          • 标记-清除(Mark-Sweep)
            • GC暂停程序(Stop-the-world)
            • 从根(root)对象触发,递归标记能到达的对象
            • 未被标记的对象回收(释放内存)
          • 非分代(Non-generational)
            • 没有分代优化(不像.NET/Java那样有Gen0/Gen1/Gen2)
            • 所以回收性能较差,容易造成长时间卡顿

          BoehmGC in Unity\

          • 在Unity4.x/5.x/2017.x里,C#脚本跑在Mono + BoehmGC上
          • 问题
            • GC卡顿明显(Stop-the-world很长)
              • GC暂停时间与堆大小线性相关,无法规避。需将堆大小控制在严格阈值内
              • 吞吐量较低,暂停时间不可预测;应避免在性能关键帧如(Update)中进行任何可能触发主GC的分配
            • 频繁分配内存(尤其是string、List、Lambda)会造成就帧率抖动
              • 会产生大量内存碎片,且不会被压缩。这就导致长期运行后,即使总内存充足,也可能找不到连续控件而分配失败,必须重启
            • 没有分代 -> 小对象也要和大对象一起回收,效率差

          这就是为什么老Unity游戏经常出现掉帧/卡顿,原因之一就是BoehmGC的GC暂停

          • Mono团队后来实现了 SGen GC(Simple Generational GC);
            • 分代收集器(Gen0,Gen1,Gen2)性能更好
            • 适合C#程序的高分配频率场景
          • Unity从2018.1开始引入了增量式GC(Incremental GC):
            • 仍基于Boehm/SGen,但支持分步执行回收,减少一次性卡顿
          • Unity未来方向:完全替换为更现代的GC(类似CoreCLR的GC),但目前仍在迭代中
          IL2CPP

          IL2CPP不是一个运行时,它只是IL -> C++ -> 机器码的转换工具链
          真正执行的时候,Unity还是需要一个托管内存的GC来帮忙管理C#对象,它会绑定到Unity内置的GC实现

          • Unity2017及之前
            • IL2CPP使用BoehmGC,和Mono一样
          • Unity2018 ~ 现在
            • IL2CPP默认使用Unity集成的libgc(基于Boehm/SGen改进版)
            • 并且支持增量式GC(Incremental GC)
          • Unity2021之后
            • 引入了新的可选GC模式,更接近.NET Core的分代GC思路(但还没有完全一致)
            • 移动端、主机端IL2CPP都能用Incremental GC,减少卡顿

          IL2CPP编译出来的C++代码

          • 对象分配会走Unity提供的GC_malloc等API
          • Unity内置的GC(基于Boehm/SGen/Incremental GC)接管这些分配
          • GC会在合适的时间做标记-清除或增量扫描
          现实意义
          • 开发者角度
            • 不管是Mono还是IL2CPP,平时写C#代码都用new、不用手动释放对象,Unity的GC都会帮助回收
            • 差别在于运行时体验(Mono更容易卡顿,IL2CPP + IncrementalGC更平滑)
          • 优化角度
            • 在Mono下,频繁分配会立刻暴露GC卡顿
            • 在IL2CPP下,GC机制更强,但仍要避免频繁分配大对象(如string拼接、临时List)

          高级优化策略

          • 自定义内存分配策略:
            • 结构体(Struct)的极致运用:使用struct构建数据导向设计(DOD),将热点数据连续存储在原生数组(NativeArray)或栈上,彻底规避GC和缓存不命中
            • 非托管内存(Unsafe Code):在机制性能要求的场景(如网格处理、复杂算法),使用stackallocNativeArray(与Burst编译器结合)或直接Marshal.AllocHGlobal在非托管堆分配,完全脱离GC管辖
          • GC行为预测与主动管理
            • 在预计的加载界面或过场动画中,主动调用System.GC.Collect(),在可控时机触发GC,避免在战斗或复杂场景中发生
            • 使用Profiler监控GC.AllocGC.Collect调用,建立性能基线,并确保关键路径上的分配为零

          编译与链接

          泛型代码共享的深层原理

          • 问题本质:AOT编译必须为所有泛型实例化生成具体代码
          • Mono(iOS AOT):为所有值类型泛型(List<int>.List<MyStruct>)生成独立代码,导致代码爆炸
          • IL2CPP的解决方案:实现泛型代码共享(Generic Sharing)
            • 引用类型共享:List<AnyClass>共享同一份底层实现,通过运行时传入的“方法头”区分类型
            • 值类型特定化:仍需为不同大小的值类型(如int, long,MyStruct)生成特定代码

          链接器(Linker),link.xml与代码裁剪(Code Stripping)

          Linker

          在Unity里,Linker是构建管线中的一个步骤:

          • 它的任务是分析当前C#程序集(.dll),移除未被使用的类型、方法、字段,减少最终包体大小
          • Unity用的是Mono Linker(后来升级为IL Linker,和.NET官方的ILLinker类似)
          Code Stripping

          代码裁剪就是Linker的主要工作

          原理:

          • Linker会从入口点(如Main()、Unity的脚本生命周期方法)开始分析依赖
          • 标记可达的类型和方法
          • 没有被引用的代码会被裁剪掉

          优点:

          • 包体小很多(Unity自带的.NET库、第三方库很多时候只用到一点点)
          • 运行时加载更快

          缺点:

          • 反射、序列化、动态调用的代码可能被误删,因为Linker静态分析不到
            • 例如:
            var type = Type.GetType("MyNamespace.MyClass");
            Activator.CreateInstance(type);
            
            这里MyClass看起来没有直接引用 -> Linker可能会裁掉 -> 运行时崩溃
          link.xml

          为了解决“代码被误裁掉”的问题,Unity提供了link.xml配置文件,显式告诉Linker:哪些类/方法/程序集不要裁剪

          <linker>
          <!-- 保留整个程序集 -->
          <assembly fullname="MyGameAssembly" preserve="all"/>
          
          <!-- 保留特定类型 -->
          <assembly fullname"UnityEngine">
            <type fullname="UnityEngine.GameObject" preserve="all"/>
          </assembly>
          
          <!-- 保留某个类的特定方法 -->
          <assembly fullname="MyGameAssembly">
            <type fullname="MyNamespace.MyClass">
              <method name="MyMethod" />
            </type>
          </assembly>
          </linker>
          

          preserve属性

          • "all"保留整个类型/程序集
          • "fields"只保留字段
          • "methods"只保留方法

          Unity规定:

          • link.xml必须放在Assets文件夹或它的子目录中
            • Assets/link.xml
            • Assets/Configs/link.xml
            • Assets/Plugins/YourLib/link.xml
          • 名字必须是link.xml(不能改名,否则Unity不会识别)

          只要它在Assets目录下,Unity构建时就会自动收集并传递给Linker

          多个link.xml的情况

          • 可以在项目里放置多个link.xml(比如第三方库自带一个,自己再写一个)
          • Unity构建时会合并所有link.xml文件
          • 如果同一个类型/程序集在多个文件里都有配置,Unity会做并集,不会冲突

          构件时打开Editor log,会看到link.xml被解析的日志

          实际使用场景

          需要写link.xml的情况

          1. 反射:
          • Newtonsoft.Json/Odin Serializer / XLua这类库经常用反射动态创建类型
          • 必须告诉Linker保留哪些类型
          1. 序列化:
          • Unity的序列化(尤其是ScriptableObjectJsonUtility)可能用到未显式引用的字段
          1. 热更新框架
          • HybridCLR/XLua/ILRuntime依赖反射加载C#,必须配合link.xml
          调试与问题定位
          • 构建后报错MissingMethodException/TypeLoadException 很可能是Linker裁掉了代码
          • 解决方法:
            • 确认是不是动态调用的代码
            • link.xml中声明保留
            • 重新打包

          平台特定深度调优

          1. iOS

          2. Android

          3. WebGL


          未来发展趋势

          Unity正在持续改进IL2CPP,缩短构建时间、增强调试支持、改进泛型处理、更好的异常处理、增量构建支持等。随着Unity的发展,IL2CPP正成为更主流的选择,特别是在性能敏感和移动平台项目中

          • CoreCLR:Unity正在投资将.NET Core运行时(CoreCLR)集成为第三个脚本后端。这将提供最新的C#语言特性、更高的性能和微软的官方支持。关注其进展,评估其与IL2CPP的性能差异
          • Burst编译器:对于机制性能的数学、图形算法,Burst是终极解决方案。它将C# Job编译为高度优化的原生代码,性能堪比C++
            • 高级用法:将Burst与IL2CPP结合,使用Burst处理计算密集型任务,IL2CPP处理游戏逻辑,形成高性能混合架构

          架构层面的考量

          • 热更新需求:如果项目有强烈的热更新需求(如某些手游),Mono(在允许JIT的平台)配合像HybridCLR这样的方案是目前的主流选择。IL2CPP由于其AOT特性,热更新需要更复杂的技术(如Lua)