Unity 进阶 - C# 知识补充

任务 2-1:了解.Net 相关知识 知识点

.NET 相关概念_NRatel 的博客-CSDN 博客

微软的.Net 是什么?

微软的.Net

微软的.Net 既不是编程语言也不是框架
是类似于互联网时代、次时代、21 世纪、信息时代之类的宣传口号它是一整套技术体系的统称,或者说它是微软提供的技术平台的代号包含的内容有

框架体系: .Net Framework、.Net Core、Mono 等等

开发语言:C#、VB、F#等等(C#是.Net 平台主推的开发语言)

开发工具: Visual Studio、Visual Studio Code 等等

你可以简单理解.Net 本质上就是微软为自己的一系列产品取的一个代号。

微软做.Net 平台的目的

  1. 跨语言

    只要是面向.NET 平台的编程语言(C#、VB、C++、F#等等),用其中一种语言编写的内容可以无缝地用在另一种语言编写的应用程序中

  2. 跨平台
    一次编译,不需要任何代码修改,应用程序就可以运行在任意有.NET 框架实现的操作系统上,即代码不依赖于操作系统,也不依赖硬件环境

.Net 跨语言的实现

如何实现的跨语言?

CLS (Common Language Specification)公共语言规范

.Net 专门参考每种语言并找出了语言间的共性,定义了一组规则。与其说是规则,不如说它是一组语言互操作的标准规范

只要开发者都遵守这个规则来编码,那么代码就能被任意.Net 平台支持的语言所通用,即可以通过不同的编程语言(C#、VB、J#等等)来创建应用程序。

CTS (Common Type System)

公共类型系统
当你需要设计面向.Net 的语言时需要遵循一个体系,这个体系就是 CTS

刚才提到的 CLS 公共语言规范就是是 CTS 公共类型系统的子级

一个编程语言,如果它能够支持 CTS,那么我们就称它为面向.NET 平台的语言

CLl (Common Language Infrastructure)

公共语言基础结构
是微软将 CTS 等内容提交给国际组织计算机制造联合会 ECMA 的一个工业标准。

总结

微软为了实现跨语言,制定了一些规范。
$\textcolor{OrangeRed}{只要一门语言支持CTS(公共类型系统)的规则
,那么我们就能够使用它在.Net平台下开发应用程序}$

CLS(公共语言规范)是 CTS 的一个子级,是一组语言互操作的标准规范我们经常可以看到的 CLI 公共语言基础结构,它包含 CTS 公共类型系统
它是微软将 CTS 等内容提交给国际组织计算机制造联合会 ECMA 的一个工业标准

.Net 跨平台的实现

如何实现的跨平台?

通过上面对跨语言的讲解,大家很容易发现
.Net 的跨语言是让各种语言支持.Net 的规范 CLI(也就是 CTS 等规范)

早期的.Net 系列产品主要是为了给 Windows 操作系统服务的
并没有跨平台的特性,即使跨语言了
也只是可以使用多语言开发 Windows 上的应用程序。

那么这里就不得不提几个个概念
.Net Framework 、.Net Core 和 Mono 我们先来认识下他们

.Net Framework

.Net Framework 在 2002 年推出 1.0 版本
.NETFramework 是一个可以快速开发、部署网站服务及应用程序的开发框架
是 Windows 中的一个组件,部分开源,主要用于开发 Windows 下应用程序

包括

  • 公共语言运行时(Common Language Runtime, CLR)
  • 虚拟执行系统
  • .NET Framework 类库等

.Net Framework 的体系结构

image-20230412111430761

image-20230412111610615

image-20230412112133687

总结

.Net Framework
是一个主要用于跨语言开发 Windows 操作系统下的应用程序的框架结构他并不支持跨平台

.Net Core

.Net Core 又是什么?

image-20230412112608327

.Net FrameWork 到.Net Core

从前面.Net FrameWork 和.Net Core 相关内容我们了解到,从并不跨平台的.Net FrameWork 在 2002 年正式问世,到 2016 年跨平台的.Net Core 的诞生。

中间.Net 平台有 14 年的并不跨平台的空窗期,而在这段时间内,难道.Net 就并不支持跨平台了吗?

答案当然是否定的,那么我们就不得不提 Mono 了。

Mono 又是什么?

Mono 是一个由 Xamarin 公司(已被微软收购)所赞助的开源项目。

它基于.Net 的 CLI (Common Language Infrastructure)公共语言基础结构,提供了微软.Net FrameWork 的另一种实现。

它相对.Net FrameWork 最大的区别就是具备跨平台的能力,它不仅可以运行在 Windows、MacOS、Linux 等操作系统,甚至还可以运行在 PS3、XBOX、Wii 等主机平台上。

Mono 的 1.0 版本出现在 2004 年,也就是说在.Net Core 出现之前,Mono 是.Net 平台实现跨平台的不二之选

Mono 如何实现快平台?

Mono 利用.Net 平台制定的 CLI 公共语言基础结构规则。

利用它我们可以把很多种语言编译成通用规范的 CIL 公共中间语言,再利用 CLR 公共语言运行时,将这些 CIL 公共中间语言转换为操作系统的原生代码。

(主要做的就是在各种操作系统上实现了对应的 CLR 内容)

这样用各种不同语言编写的逻辑就能够在指定操作系统上运行了。

它的这一套规则是在.Net Framework 规则上进行的修改和添加

总结 如何实现跨平台的?

  • .Net Framework (2002 年发布)∶

    部分开源,主要用于开发 Windows 平台下应用,包含 Windows 平台的所有特性

  • .Net Core (2016 年发布)︰
    完全开源,可以针对多个平台开发应用,包含.Net Framework 部分特性

  • Mono (2004 年发布)︰
    完全开源,早期乃至现在也是.Net 的跨平台解决方案

总体而言

在.Net Core 出现之前开发者都通过 Mono 来实现.Net 的跨平台

我们可以使用基于.Net 平台的 Mono 开发

便可以发布可以在主流的各种操作系统上运行的应用程序

主要内容

  1. 微软的.Net 是什么

    是一个包含编程语言、框架、IDE 等产品的系列产品统称

  2. .Net 跨语言的实现

    CLI 公共语言基础结构(CTS 公共类型系统)

    CLS 公共语言规范是它的子级

  3. .Net 跨平台的实现

    基于.Net Framework 的 Mono 和.Net Core

他们两都基于 CLI 公共语言基础结构和 CLR 公共语言运行时实现跨语言和跨平台

练习题

请简要说明.Net 跨语言和跨平台的原理(往往是面试时的口头询问)。

.Net 跨语言主要通过 CTS(公共类型系统),CTS 是.Net 语言应该遵循的体系。 CLS(公共语言规范)是 CTS 的一个子集,是一种规则,只要开发者都遵循这个规则来编码,那么代码就能被任意的.Net 平台支持的语言所通用。

跨平台在.NetFramework 不支持,但是可以使用 Mono(第三方开源项目)实现跨平台。而 2016 年发布的.Net Core,通过为不同的操作系统实现对应的 CLR,在不同平台上将 IL 翻译为机器码,最终实现跨平台。

参考答案:

1.跨语言

.Net 制定了了 CLI 公共语言基础结构的规则

只要是按照该规则设计的语言在进行.Net 相关开发时

编译器会将源代码(C#、VB 等等)编译为 CIL 通用中间代码。

也就是说不管什么原因进行开发,最终都会统一规范变为中间代码

最终通过 CLR(公共语言运行时或者称为.Net 虚拟)将中间代码翻译为对应操作系统的原生代码(机器码)

在操作系统上运行

2.跨平台

由于.Net Framework 中利用 CLI 和 CLR 实现了跨语言,CLR 主要起到一个翻译、运行、管理中间代码的作用

.Net Cor 和 Mono 就是利用了 CLR 的这一特点,为不同操作系统实现对应 CLR(公共语言运行时或.Net 虚拟机)

那么不同操作系统对应的 CLR 就会将 IL 中间代码翻译为对应系统可以执行的原生代码(机器码)

达到跨平台的目的

任务 4-1:Unity 跨平台的基本原理(Mono) 知识点

【Unity 游戏开发】Mono 和 IL2CPP 的区别 - 知乎 (zhihu.com)

Unity 和 Mono 的关系

我们上节课了解了.Net 的相关知识,我们先回顾一下其中的两个关键点。

  • Mono 是基于.Net 的跨平台方案,同时它具备.Net 平台的跨语言特点
  • Mono 的第一个版本是在 2004 年发布的

Unity 公司于 2004 年成立,Unity 的底层是通过 C/C++来完成的。

但是为了更方便的让开发者使用,Mono 在当时成为了不二之选。

它同时具备跨平台和跨语言的两个特性。

总结

Unity 希望能有更多的开发者使用其进行游戏开发并且一次开发一劳永逸。

所以跨语言和跨平台对于他们来说是很重要的

虽然 C++本身跨平台,但是如果使用 C++作为上层逻辑开发语言,那么作为开发
者来说选择性相对较少,并且对于初学者来说学习难度也较大。

所以当时的 Mono 是非常满足 Unity 需求的,不仅支持跨语言还支持跨平台。

Unity 跨平台的必备概念

Unity 主要包括两个部分:

Unity Engine(引擎):
提供 UnityEngine.dll 动态库,各平台不同,C/C++编写,包含平台相关代码、图形 API、物理引擎、灯光等等所有游戏引擎底层内容

Unity Editor(编辑器):
提供 UnityEditor.dll 动态库,大部分由 C#编写,用户脚本最初可以使用 C#、JavaScript、Boo 语言编写,项目代码最后由 Mono 编译

回顾 Mono 跨平台基本原理

Mono 利用.Net 平台制定的 CLI 公共语言基础结构规则。

利用它我们可以把很多种语言编译成通用规范的 CIL 公共中间语言,再利用 CLR 公共语言运行时,将这些 CIL 公共中间语言转换为对应操作系统的原生代码。

这样用各种不同语言编写的逻辑就能够在指定操作系统上运行了。

它的这一套规则是在.Net Framework 规则上进行的修改和添加。

Mono 主要构成部分

  1. C#编译器(mcs)

  2. Mono Runtime 类似 CLR 公共语言运行时(虚拟机)

    包括 JIT(Just in time)即时编译器、AOT(Ahead of time)提前编译器、GC、类库加载器 等等

  3. BCL 基础类库

  4. Mono 类库

    提供很多超出.Net 的一些额外功能,主要用于构建各种操作系统上的应用

Unity 跨平台的基本原理(Mono)

image-20230616102632094

在 Unity 下使用各种语言进行逻辑实现,这些语言在发布时会被编译成,IL 中间代码。

最终这些中间代码在对应操作系统上,通过 Mono VM(虚拟机),真正翻译成机器码运行起来。

image-20230616102655442

基于 Mono 跨平台的优缺点

优点:
只要在不同操作系统上实现 Mono VM(虚拟机)
那我们能够支持的平台就会“无限”多

缺点:
维护工作耗时耗力,当 Unity 版本更新时,Mono VM 也需要维护和更新
那多对于 N 多个平台来说,工作量是非常大的。(Unity 的工作量)
低版本 Mono 无法支持新版本 C#的强大新特性

总结

主要内容

  1. Unity 和 Mono 的关系
    利用 Mono 实现 Unity 上层逻辑的跨语言和跨平台
  2. Unity 跨平台的必备概念
    Unity 底层是 C/C++,上层逻辑支持多语言(目前只用 C#)
    Mono 的基本构成有 C#编译器、Mono 运行时(虚拟机)、各种类库等
  3. Unity 跨平台的基本原理(Mono)
    将基于 CLI 公共语言基础规则的语言编译成 CIL 公共中间语言,再通过 Mono VM(虚拟机)
    将其在各操作系统中转译为原生机器码进行运行
  4. 基于 Mono 跨平台的优缺点
    无限跨平台,维护工作量大,低版本的 Mono 无法支持新版 C#新功能

练习题

请简要描述 Unity 如何利用 Mono 实现跨平台的?(面试时可能会问 Unity 为什么可以跨平台)

通过将 C#代码用 Mono C#编译器翻译为 IL 代码,再通过不同平台上的 Mono 虚拟机翻译为对应平台的机器码来运行。

参考答案:

Mono 跨平台的原理是:

我们编写的 C#代码——>

会通过 Mono C#编译器(mcs)——>

编译为 IL 中间代码(基于 CLI 规则的字节码)——>

Mono VM(Mono Runtime)也就是 Mono 虚拟机会把 IL 中间代码转译为——>

操作系统的原生代码(机器码)最终运行起来

通过 Mono 达到了跨语言和跨平台的特性

只不过目前跨语言的特性在 Unity 当中已经不常见了,因为 UnityScript(JavaScript)和 Boo 语言已经被 C#给淘汰了

任务 6-1:Unity 跨平台的基本原理(I L 2 C P P) 知识点

【Unity 游戏开发】Mono 和 IL2CPP 的区别 - 知乎 (zhihu.com)

IL2CPP 是什么

IL2CPP 是在 Unity4.6.1 p5 之后的版本中,加入的脚本后处理方式。你可以把它简单理解为是继 Mono 之后的一种跨平台解决方案。

顾名思义,就是把 IL 中间代码转译为 CPP 代码(C++)。

想要了解它我们可以先回顾一下 Mono。

Mono 跨平台回顾

在 Unity 下使用各种语言进行逻辑实现,这些语言在发布时会被编译成,IL 中间代码。

最终这些中间代码在对应操作系统上,通过 Mono VM(虚拟机),真正翻译成机器码运行起来。

Mono 优缺点回顾

优点:

只要在不同操作系统上实现 Mono VM(虚拟机),那我们能够支持的平台就会“无限”多。

缺点:

维护工作耗时耗力,当 Unity 版本更新时,Mono VM 也需要维护和更新,那么对于 N 多个平台来说,工作量是非常大的。(Unity 的工作量)。低版本 Mono 无法支持新版本 C#的强大新特性。

IL2CPP 跨平台原理

image-20230616103638688

通过 IL2CPP 我们可以将编译好的 IL 中间代码转译成 C++代码

再利用各平台优化过的编译器编译为对应平台的目标代码

IL2CPP 和 Mono 的区别就在于

当生成了 IL 中间代码后,Mono 是直接通过虚拟机转译运行。

而 IL2CPP 的步骤多了一些,会将 IL 中间代码转译为 C++代码,再通过各平台的 C++编译器直接编译
为可执行的原生汇编代码。

image-20230616103718162

需要注意的是

虽然中间代码变为了 C++,但是内存管理还是遵循 C#中 GC 的方式。

这也是为什么有一个 IL2CPP VM(虚拟机),存在的原因,它主要是用来完成 GC 管理,线程创建等服务工作的。

Mono 和 IL2CPP 的区别

Mono

  1. 构建(最终打包时)速度快
  2. Mono 编译机制是 JIT 即时编译,所以支持更多类库
  3. 必须将代码发布为托管程序集(.dll 文件)
  4. Mono VM 虚拟机平台维护麻烦,且部分平台不支持(WebGL)
  5. 由于 Mono 版本授权原因,C#很多新特性无法使用
  6. IOS 支持 Mono,但不在允许 32 位的 Mono 应用提交到应用商店

IL2CPP

  1. 相对 Mono 构建(最终打包时)速度慢
  2. 只支持 AOT 提前编译
  3. 可以启用引擎代码剥离来减少代码的大小
  4. 程序的运行效率比 Mono 高,运行速度快
  5. 多平台移植更加方便

Mono 和 IL2CPP 的最大区别就是

IL2CPP 不能在运行时动态生成代码和类型,所以必须在编译时就完全确定需要用到的类型。

举例:List<A>和 List<B>中 A 和 B 是我们自定义的类,我能必须在代码中显示的调用过,IL2CPP 才能保留 List<A>和 List<B>两个类型。

如果在热更新时我们调用 List<C>,但是它之前并没有在代码中显示调用过,那么这时就会出现报错等
问题。主要就是因为 JIT 和 AOT 两个编译模式的不同造成的。

具体的解决方案我们在下节课中讲解

Mono 和 IL2CPP 的使用建议

由于 IL2CPP 的运行效率有很大优势,所以建议大家在实际开发中,直接使用 IL2CPP 模式进行打包。

总结

主要内容

  1. IL2CPP 是什么
    是 Unity4.6.1 版本之后加入的新的一种跨平台解决方案
  2. Mono 跨平台回顾
    C#代码—>Mono C#编译器—>IL 中间代码—>Mono VM—>操作系统的原生代码
  3. IL2CPP 跨平台原理
    C#代码—>Mono C#编译器—>IL 中间代码—>IL2CPP—>C++—>C++编译器—>原生汇编代码—>IL2CPP VM
  4. Mono 和 IL2CPP 的区别
    IL2CPP 效率高于 Mono,跨平台也更好维护。
    Mono 是 JIT 即时编译,IL2CPP 是 AOT 提前编译
  5. Mono 和 IL2CPP 两种方式的使用建议
    建议使用效率更高的 IL2CPP

练习题

请简要描述 Unity 是如何利用 IL2CPP 进行跨平台的?(面试时可能会问 Unity 为什么可以跨平台)

通过 IL2Cpp 程序,将 IL 中间语言代码翻译为 Cpp 代码,再其他平台的 Cpp 编译器编译为各平台本地代码。

主要是利用了 Cpp 本身就跨平台的特性。

需要注意的是,虽然代码是 Cpp,但是 GC 还是 C#,所以需要一个 IL2Cpp VM 用来完成 GC 管理,线程创建等服务 。

参考答案:

IL2CPP 跨平台的原理是:

我们编写的 C#代码——>

会通过 Mono C#编译器(mcs)——>

编译为 IL 中间代码(基于 CLI 规则的字节码)——>

Unity 会利用 IL2CPP.exe 运行程序将代码转译为 C++代码——>

然后优化过的各平台 C++编译器会把 C++代码编译为原生汇编代码(机器码)——>

最终在各操作系统上会通过 IL2CPP VM 将这些原生代码运行管理起来

通过 IL2CPP 达到了跨语言和跨平台的特性

只不过目前跨语言的特性在 Unity 当中已经不常见了,因为 UnityScript(JavaScript)和 Boo 语言已经被 C#给淘汰了

任务 8-1:IL2CPP 模式可能存在的问题处理 知识点

【Unity 游戏开发】Mono 和 IL2CPP 的区别 - 知乎 (zhihu.com)

知识点一 —— 安装 Unity IL2CPP 打包工具

在 Unityhub 中下载 IL2CPP 打包相关工具

知识点二 —— IL2CPP 打包存在的问题——类型裁剪

IL2CPP 在打包时会自动对 Unity 工程的 DLL 进行裁剪,将代码中没有引用到的类型裁剪掉,以达到减小发布后包的尺寸的目的。

然而在实际使用过程中,很多类型有可能会被意外剪裁掉,造成运行时抛出找不到某个类型的异常。

特别是通过反射等方式在编译时无法得知的函数调用,在运行时都很有可能遇到问题

解决方案:

  1. IL2CPP 处理模式时,将 PlayerSetting->Other Setting->Managed Stripping Level(代码剥离)设置为 Low

    Disable:Mono 模式下才能设置为不删除任何代码

    Low:默认低级别,保守的删除代码,删除大多数无法访问的代码,同时也最大程度减少剥离实际使用的代码的可能性

    Medium:中等级别,不如低级别剥离谨慎,也不会达到高级别的极端

    Hight:高级别,尽可能多的删除无法访问的代码,有限优化尺寸减小。如果选择该模式一般需要配合 link.xml 使用

  2. 通过 Unity 提供的 link.xml 方式来告诉 Unity 引擎,哪些类型是不能够被剪裁掉的

    在 Unity 工程的 Assets 目录中(或其任何子目录中)建立一个叫 link.xml 的 XML 文件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
<?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 打包存在的问题——泛型问题

我们上节课提到了 IL2CPP 和 Mono 最大的区别是 不能在运行时动态生成代码和类型。

就是说 泛型相关的内容,如果你在打包生成前没有把之后想要使用的泛型类型显示使用一次。

那么之后如果使用没有被编译的类型,就会出现找不到类型的报错。

举例:List<A>List<B>中 A 和 B 是我们自定义的类,我能必须在代码中显示的调用过,IL2CPP 才能保留List<A>List<B>两个类型。

如果在热更新时我们调用List<C>,但是它之前并没有在代码中显示调用过,那么这时就会出现报错等问题。主要就是因为 JIT 和 AOT 两个编译模式的不同造成的。

1
2
List<A> list = new List<A>();
List<B> list2 = new List<B>();

解决方案:

泛型类:声明一个类,然后在这个类中声明一些 public 的泛型类变量

泛型方法:随便写一个静态方法,在将这个泛型方法在其中调用一下。这个静态方法无需被调用。

这样做的目的其实就是在预言编译之前让 IL2CPP 知道我们需要使用这个内容

总结

对于我们目前开发的新项目,都建议大家使用 IL2CPP 脚本后处理模式来进行打包。

主要原因是因为它的效率相对 Mono 较高,同时由于它自带裁剪功能,包的大小也会小一些。

但是如果在测试时出现 类型无法识别等问题,需要用到我们这节课学习的知识点来解决这些问题。