Skip to content

CLR 与运行时

CLR 是什么?

一句话讲清楚

CLR 是 .NET 的运行时环境,全称是 Common Language Runtime,中文叫公共语言运行时。它负责把 C# 编译后的中间语言运行起来,并提供 GC、异常处理、类型安全、线程等基础服务。

clr-explanation

C# 程序怎么跑起来

C# 源代码不会直接变成机器码。

流程大概是:

C# 源代码C# 编译器IL 中间语言 + 元数据CLRJIT 编译机器码CPU 执行

也就是说,C# 编译器先把 .cs 文件编译成 IL,也叫中间语言。程序真正运行时,CLR 会加载这些 IL,然后通过 JIT 把需要执行的 IL 编译成本机机器码。

CLR 主要做什么

CLR 不只是“帮你运行代码”,它还负责很多底层工作。

比如:

JIT 编译:把 IL 转成当前平台能执行的机器码。 GC 垃圾回收:自动回收托管堆上的对象。 类型安全:检查类型使用是否合法,避免乱访问内存。 异常处理:支持 try catch finally线程管理:提供线程、线程池、任务调度相关能力。 跨语言互操作:C#、F#、VB 编译成 IL 后都能在 CLR 上运行。

托管代码是什么意思

运行在 CLR 管理下的代码,叫托管代码

比如 C# 里普通的类对象,一般由 CLR 分配和回收内存,所以你不用像 C++ 那样手动 delete

但如果你操作文件句柄、Socket、数据库连接、原生内存这些资源,它们不完全由 GC 管,所以还需要 IDisposableusing 来及时释放。

面试高分回答

NOTE

CLR 是 .NET 的公共语言运行时,它是托管代码的执行环境。C# 编译后生成 IL 和元数据,程序运行时由 CLR 加载程序集,并通过 JIT 把 IL 编译成本机机器码执行。同时 CLR 还提供垃圾回收、异常处理、类型安全、线程管理等服务。简单说,CLR 就是 .NET 程序背后的运行时管家。

JIT 和 AOT 区别是什么?

一句话讲清楚

JIT 是程序运行时再把 IL 编译成机器码;AOT 是程序运行前就提前编译成机器码。核心区别就是:编译发生在运行时,还是运行前

jit-aot-difference

JIT 是什么

JIT 全称是 Just-In-Time,即时编译。

C# 代码先被编译成 IL 中间语言。程序运行时,某个方法第一次被调用,CLR 才把这段 IL 编译成本机机器码,然后交给 CPU 执行。

它的特点是:运行时按需编译,比较灵活,可以根据当前机器环境做优化。但缺点是第一次调用方法时可能有编译开销,所以启动或某些首次执行场景可能会有一点抖动。

AOT 是什么

AOT 全称是 Ahead-Of-Time,提前编译。

它不是等程序运行时再编译,而是在构建阶段、安装阶段或者发布阶段,就提前把代码编译成本机机器码。程序运行时可以直接执行,少了运行时编译这一步。

它的特点是:启动更快,运行时更稳定,适合不允许动态生成机器码的平台,比如 iOS。但缺点是构建时间更长,包体可能更大,而且运行时动态优化能力相对弱一些。

对比表

对比点JITAOT
编译时机运行时编译运行前编译
启动速度可能受首次编译影响通常更稳定
运行时开销有 JIT 编译开销基本没有 JIT 编译开销
优化方式可以利用运行时信息构建期提前优化
平台限制有些平台不允许更适合受限平台
常见场景传统 .NET、Unity MonoiOS、Unity IL2CPP、Native AOT

Unity 里怎么理解

Unity 里常见两个后端:MonoIL2CPP

Mono 更接近 JIT 模式,运行时由 Mono 虚拟机执行和编译代码。 IL2CPP 更接近 AOT 模式,它会先把 IL 转成 C++,再由 C++ 编译器编译成本机代码。

所以 iOS 平台通常使用 IL2CPP,因为 iOS 对运行时动态生成机器码限制比较严格,不适合 JIT。

面试高分回答

IMPORTANT

JIT 和 AOT 的区别主要在编译时机。JIT 是运行时按需把 IL 编译成本机机器码,优点是灵活,可以结合运行时信息优化,缺点是首次执行可能有编译开销。AOT 是运行前提前编译成本机代码,优点是启动更快、运行时更稳定,也适合 iOS 这类限制 JIT 的平台,缺点是构建时间更长、包体可能变大,动态能力也会受限制。在 Unity 中,Mono 偏 JIT,IL2CPP 偏 AOT。

IL 是什么?

一句话讲清楚

IL.NET 的中间语言,全称常叫 Intermediate Language,也叫 CILMSIL。它不是 C# 源码,也不是 CPU 机器码,而是 C# 编译后生成的一种中间指令。

il-explanation

执行流程

C# 程序大概是这样跑起来的:

C# 源代码C# 编译器IL + 元数据CLRJIT/AOT机器码CPU 执行

也就是说,C# 编译器不会一开始就直接生成机器码,而是先生成 IL。等程序运行时,再由 CLR 通过 JIT 把 IL 编译成本机机器码;或者通过 AOT 提前编译成本机代码。

IL 长什么样

比如 C# 代码:

c
public int Add(int a, int b) // 定义一个 Add 方法,接收两个 int 参数
{ // 方法开始
    return a + b; // 返回两个参数相加的结果
} // 方法结束

它大概会变成类似这样的 IL 思路:

c
ldarg.1    // 加载第一个参数 a
ldarg.2    // 加载第二个参数 b
add        // 执行加法
ret        // 返回结果

你不用真的手写 IL,但要知道 IL 是一种栈式指令。它会把数据压入栈,再执行运算。

IL 有什么作用

IL 最大的作用是做一层统一抽象。

C#、F#、VB 这些语言都可以编译成 IL。只要最后变成 IL,就可以交给 CLR 运行。所以 .NET 能支持多语言互操作。

另外,程序集里不只有 IL,还有元数据。元数据记录了类、方法、字段、属性、特性等信息,所以 .NET 才能做反射、类型检查、序列化、特性读取这些事情。

Unity 里怎么理解

Unity 的 C# 脚本也会先编译成 IL。

如果用 Mono 后端,运行时可以解释或 JIT 执行这些 IL。 如果用 IL2CPP,Unity 会把 IL 转成 C++,再由 C++ 编译器编译成本机代码。

所以 IL2CPP 这个名字里的 IL,指的就是这个中间语言。

面试高分回答

TIP

IL 是 .NET 的中间语言。C# 源码经过编译器后不会直接变成机器码,而是先生成 IL 和元数据,打包到程序集里。程序运行时,CLR 会加载程序集,验证 IL 的类型安全,再通过 JIT 或 AOT 把 IL 转换成本机机器码执行。IL 的意义是让不同 .NET 语言都能编译到统一的中间表示,从而实现跨语言互操作和托管运行。

Mono 和 IL2CPP 区别是什么?

一句话讲清楚

MonoIL2CPP 都是 Unity 的脚本后端。区别是:Mono 更偏运行时执行 IL,适合开发调试;IL2CPP 会把 IL 转成 C++,再编译成本机代码,适合正式发布。

mono-il2cpp-difference

Mono 是什么

Unity 里的 C# 脚本会先编译成 IL。 使用 Mono 后端时,这些 IL 会交给 Mono Runtime 来加载和执行。

你可以简单理解为:

C# 源码ILMono Runtime执行

Mono 的优点是:编译快、调试方便、开发迭代速度快。 缺点是:运行性能、平台适配、安全性通常不如 IL2CPP 适合正式包。

所以开发阶段经常用 Mono,因为改代码、看报错、调试都更方便。

IL2CPP 是什么

IL2CPP 的意思是:IL to C++

它会先把 C# 编译出来的 IL 转成 C++ 代码,然后再用平台的 C++ 编译器编译成本机机器码。

流程是:

C# 源码ILC++机器码执行

IL2CPP 的优点是:运行时更稳定,适合正式发布,跨平台适配更好,代码也更难被直接反编译。 缺点是:构建速度慢,包体可能更大,调试比 Mono 麻烦。

iOS 平台通常会用 IL2CPP,因为 iOS 对运行时 JIT 有限制。

重点对比

对比点MonoIL2CPP
执行方式运行时加载和执行 ILIL 转 C++,再编译成本机代码
编译速度
调试体验相对麻烦
运行性能一般适合开发期正式包通常更合适
平台支持某些平台受限移动端、主机、iOS 更常用
反编译难度相对容易相对更难
常用阶段开发调试发布上线

常见误区

IL2CPP 不等于没有 GC。 C# 里的托管对象还是托管对象,字符串、List、闭包、装箱这些依然可能产生 GC Alloc。

IL2CPP 也不等于一定所有代码都更快。 它通常适合正式发布,但具体性能还要看代码逻辑、平台、编译优化和运行场景。

IL2CPP 对动态能力更敏感。 比如运行时动态生成代码、某些反射、泛型 AOT 问题,都要注意。Unity 里如果类型被裁剪了,可能还需要 link.xml 或保留引用。

面试高分回答

NOTE

Mono 和 IL2CPP 是 Unity 的两种脚本后端。C# 脚本都会先编译成 IL,Mono 是运行时由 Mono Runtime 加载执行 IL,开发调试方便,编译速度快;IL2CPP 是把 IL 转成 C++,再通过平台 C++ 编译器生成本机代码,构建更慢,但更适合正式发布,尤其是 iOS 这类限制 JIT 的平台。需要注意的是,IL2CPP 只是改变执行和编译链路,并不代表没有 GC,托管内存问题依然需要优化。

Unity 为什么移动端常用 IL2CPP?

一句话讲清楚

Unity 移动端常用 IL2CPP,主要是因为移动端发布更重视平台兼容、运行稳定、性能可控和代码保护,尤其是 iOS 通常不允许运行时 JIT,所以更适合用 AOT 思路的 IL2CPP

unity-mobile-il2cpp

IL2CPP 的流程

Unity C# 脚本大概会走这个流程:

C#ILC++平台机器码

也就是说,IL2CPP 会把 C# 编译后的 IL 转成 C++,再交给 iOS、Android 对应的平台编译器生成本机代码。

为什么移动端常用

第一,`iOS` 对运行时动态生成机器码限制很严格,`JIT` 不适合正式发布。IL2CPP 是提前编译成本机代码,更符合 iOS 平台要求。

第二,移动端对启动、发热、帧率、内存都比较敏感。IL2CPP 少了运行时 JIT 编译这一步,正式包运行时表现更稳定,首次调用方法时不容易因为 JIT 编译产生额外抖动。

第三,IL2CPP 通过平台 C++ 编译器生成本机代码,更适合不同 CPU 架构和平台 ABI,比如 ARM、ARM64、iOS、Android 等。

第四,IL2CPP 相比直接带 IL 更难被反编译,对代码保护有一定帮助。当然它不是绝对安全,真要防破解还要配合混淆、加固、服务端校验。

开发期和发布期怎么选

开发阶段更喜欢 Mono,因为编译快、调试方便、迭代效率高。 正式移动端发布更常用 IL2CPP,因为它更适合上线包,尤其 iOS 基本绕不开。

常见误区

IL2CPP 不等于没有 GC。

C# 里的托管对象依然存在,比如字符串拼接、闭包、装箱、频繁 new List、LINQ,这些仍然可能产生 GC Alloc。IL2CPP 改变的是代码执行链路,不是把 C# 变成完全手动内存管理。

IL2CPP 也不等于所有代码一定更快。

它通常更适合正式发布,但具体性能还要看算法、资源加载、对象池、渲染、内存分配和平台编译优化。

面试高分回答

IMPORTANT

Unity 移动端常用 IL2CPP,是因为移动端正式发布更需要稳定和平台兼容。C# 代码先编译成 IL,IL2CPP 再把 IL 转成 C++,最后由平台编译器生成本机代码。这样可以避免运行时 JIT 的限制和开销,尤其 iOS 平台通常不允许 JIT,所以 IL2CPP 更适合移动端发布。同时它在代码保护和跨平台适配上也比 Mono 更适合正式包。但要注意,IL2CPP 不代表没有 GC,托管对象分配依然需要优化。

IL2CPP 后还能不能反射?

一句话讲清楚

IL2CPP 后还能反射,但会更容易遇到限制:类型或成员可能被代码裁剪掉,某些泛型代码可能没有被 AOT 提前生成,而且不能依赖 Reflection.Emit 这种运行时生成代码的能力。

il2cpp-reflection

为什么还能反射

反射主要依赖的是元数据,比如类名、方法名、字段名、属性、Attribute 等。

IL2CPP 会把 IL 转成 C++,再编译成本机代码,但它并不是把所有元数据都完全抹掉。所以只要元数据和相关代码被保留下来,还是可以用:

typeof()GetType()GetMethod()GetField()GetCustomAttributes()MethodInfo.Invoke()

为什么有时候会出问题

Unity 打包时会做代码裁剪。 如果某个类、字段、方法只通过反射使用,没有被代码直接引用,Unity 可能认为它“没用”,然后把它裁掉。

所以你可能会遇到:

Editor 下正常。 Mono 下正常。 IL2CPP 真机包里反射失败。 某些类型找不到。 某些方法 Invoke 报错。 某些泛型方法运行时报 AOT 相关错误。

怎么解决

常见做法有三个。

第一,用 link.xml 保留类型。

第二,用 [Preserve] 标记类型或方法。

第三,提前显式引用或调用一下相关泛型,让 IL2CPP 知道这些代码要生成。

c
using UnityEngine.Scripting; // 引入 Unity 的 Preserve 特性命名空间
[Preserve] // 告诉 Unity 打包裁剪时保留这个类
public class PlayerConfig // 定义一个可能通过反射创建的配置类
{ // 类开始
    [Preserve] // 告诉 Unity 保留这个字段
    public int Hp; // 玩家血量字段
    [Preserve] // 告诉 Unity 保留这个方法
    public void Print() // 定义一个可能被反射调用的方法
    { // 方法开始
    } // 方法结束
} // 类结束

不能做什么

IL2CPP 是 AOT,不是 JIT。

所以它不适合依赖运行时生成机器码的玩法,比如:

Reflection.Emit 运行时动态生成新类型 运行时动态生成方法体 某些依赖 JIT 的动态代理库

简单说:查已有类型可以,调用保留下来的方法可以;但运行时凭空生成代码不行。

面试高分回答

NOTE

IL2CPP 后仍然可以使用反射,因为反射主要依赖程序集元数据,而不是 C# 源码本身。但 IL2CPP 是 AOT 模式,不能像 JIT 那样运行时生成代码,所以 Reflection.Emit 这类能力不能依赖。另外 Unity 打包时会做代码裁剪,如果某些类型或成员只通过反射访问,没有显式引用,可能会被裁掉,导致真机包反射失败。实际项目里通常用 link.xmlPreserve 或提前引用泛型类型来保证反射需要的代码和元数据被保留。

泛型在 IL2CPP 下有什么注意点?

一句话讲清楚

IL2CPP 下泛型不是不能用,而是要注意:IL2CPP 是 AOT,运行前就要生成代码;如果某个泛型组合只在运行时通过反射、配置、热更动态出现,构建期没看到它,真机包里就可能出问题。

il2cpp-generics-notes

为什么泛型会有坑

Mono 或 Editor 环境下,很多东西可以运行时处理。 但 IL2CPP 是 AOT,它会在构建阶段把 IL 转成 C++,再编译成本机代码。

问题就在这里:如果某个泛型类型或泛型方法在构建期没有被明确引用,IL2CPP 可能不会给它生成对应代码。

比如你运行时才通过反射拼出:

List<int>Dictionary<int, PlayerData>Deserialize<MyConfig>()CreateHandler<SomeMessage>()

如果这些泛型组合没有提前被代码引用过,真机 IL2CPP 包里就可能找不到对应实现。

常见坑 1:反射调用泛型

比如你用配置表或协议号,通过反射动态调用泛型方法:

c
using System; // 引入 Type 类型
using System.Reflection; // 引入 MethodInfo 反射类型
public static class GenericReflectExample // 定义泛型反射示例类
{ // 类开始
    public static object CreateByReflection(Type type) // 根据运行时 Type 创建对象
    { // 方法开始
        MethodInfo method = typeof(GenericReflectExample).GetMethod(nameof(CreateGeneric)); // 通过反射获取泛型方法
        MethodInfo genericMethod = method.MakeGenericMethod(type); // 运行时拼出具体泛型方法
        return genericMethod.Invoke(null, null); // 通过反射调用这个泛型方法
    } // 方法结束
    public static T CreateGeneric<T>() where T : new() // 定义泛型创建方法
    { // 方法开始
        return new T(); // 创建并返回 T 类型对象
    } // 方法结束
} // 类结束

这段在 Editor 里可能正常,但 IL2CPP 真机包里,如果某个 T 从来没有被直接引用过,就可能出问题。

常见坑 2:代码裁剪

Unity 打包时会做代码裁剪。 如果某些类型只通过反射访问,没有直接引用,Unity 可能觉得它没用,然后裁掉。

解决方式一般是:

[Preserve] 标记。 用 link.xml 保留。 写一个 AOT 注册类显式引用。 减少运行时动态拼泛型。

推荐做法:AOT 注册

可以专门写一个类,把项目里可能通过反射或热更用到的泛型组合提前“碰一下”。

c
using System.Collections.Generic; // 引入 List 和 Dictionary 集合
using UnityEngine.Scripting; // 引入 Preserve 特性
[Preserve] // 告诉 Unity 不要裁剪这个类
public static class AotGenericRegister // 定义 AOT 泛型注册类
{ // 类开始
    [Preserve] // 告诉 Unity 不要裁剪这个方法
    public static void Register() // 显式注册 IL2CPP 需要提前生成的泛型组合
    { // 方法开始
        TouchListInt(new List<int>()); // 触发 List<int> 的泛型代码生成
        TouchListFloat(new List<float>()); // 触发 List<float> 的泛型代码生成
        TouchDictionary(new Dictionary<int, PlayerData>()); // 触发 Dictionary<int, PlayerData> 的泛型代码生成
        TouchCreator<PlayerData>(); // 触发 PlayerData 泛型方法代码生成
    } // 方法结束
    private static void TouchListInt(List<int> value) // 显式引用 List<int>
    { // 方法开始
    } // 方法结束
    private static void TouchListFloat(List<float> value) // 显式引用 List<float>
    { // 方法开始
    } // 方法结束
    private static void TouchDictionary(Dictionary<int, PlayerData> value) // 显式引用 Dictionary<int, PlayerData>
    { // 方法开始
    } // 方法结束
    private static T TouchCreator<T>() where T : new() // 显式引用泛型创建方法
    { // 方法开始
        return new T(); // 返回一个 T 类型实例
    } // 方法结束
} // 类结束
[Preserve] // 告诉 Unity 不要裁剪这个数据类
public class PlayerData // 定义玩家数据类
{ // 类开始
    public int Level; // 玩家等级字段
} // 类结束

值类型和引用类型也要注意

IL2CPP 对泛型有一定共享机制,但值类型和引用类型并不完全一样。

比如:

List<object>List<string>List<PlayerData>

这些都是引用类型相关,很多情况下可以共享一些泛型代码。

但:

List<int>List<float>List<Vector3>

这些是值类型,布局不同,更容易需要生成不同的泛型实例。所以值类型泛型组合要特别注意。

面试高分回答

WARNING

IL2CPP 下泛型可以正常使用,但要注意 AOT 限制。因为 IL2CPP 会在构建期把 IL 转成 C++ 并生成本机代码,运行时不能像 JIT 那样临时生成新的泛型代码。如果某个泛型类型或泛型方法只通过反射、配置或热更动态出现,构建期没有直接引用,就可能没有生成对应实现,真机包里会报错。实际项目里通常会用 Preservelink.xml 或 AOT 注册类提前引用这些泛型组合,尤其是值类型泛型和反射调用的泛型方法要重点处理。

C# 代码如何变成机器码?

一句话讲清楚

C# 代码通常不是直接变成机器码,而是先编译成 IL 和元数据,再由运行时通过 JIT,或者构建阶段通过 AOT,最终变成 CPU 能执行的机器码。csharp-to-machine-code

完整流程

C# 源代码C# 编译器IL + 元数据CLR / Mono / IL2CPP机器码CPU 执行

第一步,你写 .cs 文件。

第二步,C# 编译器检查语法、类型、泛型、访问修饰符等,然后把源码编译成 IL。同时还会生成元数据,比如类、方法、字段、属性、特性信息。

第三步,程序运行时,运行时环境加载这些程序集。普通 .NET 里通常是 CLR,Unity Mono 后端里是 Mono Runtime

第四步,把 IL 变成机器码。这里有两种常见路线:

JIT:运行时编译。方法第一次被调用时,把对应 IL 编译成本机机器码。 AOT:运行前编译。构建阶段提前把代码编译成本机机器码。

Unity 里怎么理解

Unity 的 C# 脚本也是先编译成 IL

如果使用 Mono,大概是:

C#ILMono Runtime执行

如果使用 IL2CPP,大概是:

C#ILC++平台机器码执行

所以 IL2CPP 不是直接把 C# 变机器码,而是先把 IL 转成 C++,再通过平台 C++ 编译器生成机器码。

面试高分回答

CAUTION

C# 源码会先经过 C# 编译器编译成 IL 和元数据,打包到程序集里。程序运行时,CLR 会加载程序集、验证类型安全,然后通过 JIT 把 IL 编译成本机机器码执行;如果是 AOT 模式,则会在运行前提前生成机器码。Unity 中 Mono 后端偏运行时执行 IL,而 IL2CPP 会把 IL 转成 C++,再编译成平台机器码。核心就是:C# 到机器码中间隔着 IL、元数据和运行时编译过程。

什么是程序集 Assembly?

一句话讲清楚

程序集 Assembly.NET 的基本打包单位、部署单位、加载单位和版本管理单位。常见形式就是 .dll.exe

assembly-explanation

程序集里面有什么

一个程序集不只是代码,它通常包含:

IL:C# 编译后的中间语言。 Metadata:类、方法、字段、属性、特性等类型信息。 Manifest:程序集名称、版本、依赖关系、入口点等清单信息。 Resources:可能包含字符串、图片、本地化资源等。

所以你可以把程序集理解成:.NET 程序运行所需信息的一个包

为什么需要程序集

第一,它是代码组织单位。 比如你写一个工具库,编译成 GameFramework.dll,别的项目就可以引用它。

第二,它是加载单位。 CLR 加载程序时,会按程序集加载类型、IL 和元数据。

第三,它是访问边界。 比如 internal 修饰的类或成员,只能在同一个程序集内部访问,出了这个程序集就不能直接访问。

第四,它是版本管理单位。 程序集可以有自己的名称、版本号、依赖关系,这样运行时能知道要加载哪个版本。

Unity 里怎么理解

Unity 默认会把很多 C# 脚本编译进:

c
Assembly-CSharp.dll

如果你创建了 asmdef,Unity 就会把代码拆成不同程序集。

这样有几个好处:

减少编译范围。 模块依赖更清晰。 可以控制哪些代码能互相访问。 更适合大型项目拆模块,比如 CoreUIBattleEditor

小代码理解 Assembly

c
using System; // 引入 Console 类型
using System.Reflection; // 引入 Assembly 反射类型
public static class AssemblyExample // 定义程序集示例类
{ // 类开始
    public static void PrintAssemblyName() // 打印当前代码所在程序集名称
    { // 方法开始
        Assembly assembly = typeof(AssemblyExample).Assembly; // 获取 AssemblyExample 这个类型所在的程序集
        Console.WriteLine(assembly.GetName().Name); // 输出程序集名称
    } // 方法结束
} // 类结束

面试高分回答

TIP

Assembly 是 .NET 的基本部署和加载单位,常见形式是 DLL 或 EXE。C# 源码编译后会生成程序集,里面包含 IL、元数据、清单和资源。CLR 会按程序集加载代码和类型信息,反射也可以读取程序集中的元数据。它同时也是访问边界,比如 internal 只能在同一程序集内访问。在 Unity 中,默认脚本会进入 Assembly-CSharp.dll,而 asmdef 可以把项目拆成多个程序集,减少编译时间并让模块依赖更清晰。

Assembly Definition 在 Unity 中有什么用?

一句话讲清楚

Assembly Definition,也就是 .asmdef,作用是把 Unity 的 C# 脚本拆成多个程序集,用来加快编译、控制模块依赖、隔离 Editor 代码、管理平台差异

unity-assembly-definition

默认情况下的问题

如果不建 .asmdef,Unity 大部分脚本会被编译进:

c
Assembly-CSharp.dll

项目小的时候没什么问题,但项目大了以后,改一个脚本可能导致很多代码重新编译,编译时间会越来越长,而且模块之间的依赖关系也不清楚。

用了 asmdef 后有什么变化

比如你可以把代码拆成:

Game.CoreGame.UIGame.BattleGame.ConfigGame.Editor

每个 .asmdef 会生成一个独立程序集。 当你只改 Game.UI 的代码时,Unity 主要重编译 Game.UI 和依赖它的程序集,不一定要把整个项目都重编译。

主要作用

第一,加快编译。 大型项目里,拆分程序集后,修改一个模块不一定影响所有代码,迭代会更快。

第二,控制依赖。 有了 .asmdef 后,一个程序集想访问另一个程序集,必须显式添加引用。这样能避免 UI 随便调用 BattleBattle 又反过来调用 UI 的混乱结构。

第三,隔离 Editor 和 Runtime。 编辑器代码应该放在 Editor 专用程序集里,避免运行时程序集引用 UnityEditor,否则打包时容易报错。

第四,管理平台差异。 .asmdef 可以设置只在某些平台生效,也可以配合 Define Constraints 控制编译条件,比如只在 UNITY_ANDROID 或某些自定义宏下编译。

第五,配合测试和插件。 Unity Test Framework、Package、插件框架通常都会用 .asmdef 来隔离测试代码、运行时代码和编辑器代码。

注意事项

拆了 .asmdef 后,访问边界会变严格。

比如 internal 只能在同一个程序集内部访问。以前都在 Assembly-CSharp.dll 里能访问,拆成两个程序集后可能就访问不到了。

程序集之间不能循环引用。 比如 Game.UI 引用 Game.Battle,同时 Game.Battle 又引用 Game.UI,这种结构 Unity 不允许,也说明模块设计有问题。

也不要拆得太碎。 拆太碎会导致引用配置复杂、维护成本变高。一般按稳定边界拆,比如核心框架、战斗、UI、配置、编辑器工具、测试。

面试高分回答

NOTE

Assembly Definition 是 Unity 用来拆分 C# 程序集的配置文件。默认情况下很多脚本会进入 Assembly-CSharp.dll,项目大了以后编译慢、依赖混乱。使用 asmdef 后,可以把代码拆成 Core、UI、Battle、Editor 等独立程序集,从而减少重编译范围,提高开发效率,同时通过显式引用控制模块依赖,还能把 Editor 代码和 Runtime 代码隔离开,避免打包时引用 UnityEditor。需要注意的是,拆分后 internal 的访问边界会变严格,程序集之间也不能循环引用。

文章评价

读完这篇,留下你的看法

暂无审核通过的评价。

登录账号后才能评价。

本站访客数0总站访问量0本页访问量0