问小白 wenxiaobai
资讯
历史
科技
环境与自然
成长
游戏
财经
文学与艺术
美食
健康
家居
文化
情感
汽车
三农
军事
旅行
运动
教育
生活
星座命理

Unity跨平台开发详解:IL2CPP优化打包方案

创作时间:
2025-03-14 09:35:18
作者:
@小白创作中心

Unity跨平台开发详解:IL2CPP优化打包方案

引用
CSDN
1.
https://blog.csdn.net/qq_36303853/article/details/145182958

Unity作为一款广泛使用的跨平台游戏开发引擎,其在不同平台上的性能和兼容性一直是开发者关注的重点。本文将详细介绍Unity如何利用.NET技术实现跨平台开发,并重点讲解IL2CPP这一优化打包方案的优势、配置方法及常见问题解决方案。

一、Unity如何实现跨平台

1. 什么是 .NET

.NET是微软推出的一整套技术体系,它不是一个编程语言也不是一个框架,而是用来开发应用程序的技术平台。你可以把它想象成一个大工具箱,里面有许多不同的工具(如编程语言、库和工具),帮助开发者创建各种类型的应用程序。这个平台支持多种编程语言(如C#、VB.NET等),并且可以让这些语言之间相互协作。

2. .NET 的跨语言特性

为了让不同语言编写的代码能够一起工作,.NET定义了一组规则,确保所有语言都能遵循这些规则来编写代码。这就好比是制定一套交通规则,让所有的车辆在路上安全行驶。在.NET中,有以下几个关键概念:

  • CLS (Common Language Specification):公共语言规范,是一组语言互操作性的标准,保证不同语言的代码可以互相调用。
  • CTS (Common Type System):公共类型系统,定义了所有语言必须遵守的数据类型和结构,使得不同语言中的数据可以相互通信。
  • CLI (Common Language Infrastructure):公共语言基础结构,包含了CTS和其他必要的组件,是一个工业标准,确保.NET应用可以在任何实现了CLI的平台上运行。

3. .NET 的跨平台特性

早期的.NET主要用于Windows操作系统设计的(即.NET Framework)。后来为了实现跨平台,微软推出了.NET Core,这是一个完全开源且能够在多个操作系统上运行的新版本。此外,还有一个叫做Mono的项目,在.NET Core出现之前就已经实现了跨平台的功能。Mono是由第三方公司Xamarin开发的,后来被微软收购了。

  • .NET Framework:主要用于Windows上的应用开发。
  • .NET Core:用于跨平台应用开发,支持Windows、macOS和Linux。
  • Mono:提供了一个额外的选择,允许.NET应用在更多类型的设备上运行,包括游戏主机等。

4. Unity 和 .NET 的关系

Unity使用了.NET技术栈作为其脚本后端。具体来说,Unity的底层是由C++编写的引擎核心,而上层逻辑则主要通过C#来编写。为了使C#代码能够在不同的平台上运行,Unity使用了Mono或者后来引入的IL2CPP技术。这两种技术都是基于.NET的公共语言基础结构(CLI)来工作的。

  • Mono:这是Unity最初采用的方式,它将C#代码编译为中间语言(IL),然后在目标平台上使用虚拟机(VM)将其转换为本地机器码执行。
  • IL2CPP:这是一种较新的方法,它会将C#代码先编译为C++代码,再由C++编译器生成针对特定平台优化后的二进制文件。这种方法通常能带来更好的性能,并且更容易集成到不同的操作系统中。

5. IL2CPP 的优势与挑战

IL2CPP提供了一些显著的优点,比如更高的运行效率和更小的应用体积。然而,它也有一些局限性,例如无法像Mono那样动态生成代码,这意味着你必须提前确定所有要用到的类型。如果某些类型是在运行时才决定使用的(例如通过反射或泛型),那么你需要采取特别措施来确保它们不会被裁剪掉。

6. IL2CPP和Mono性能对比

IL2CPP的代码执行效率是高于Mono的。主要原因:

  • Mono是JIT即时编译,IL2CPP是AOT提前编译。
  • AOT的优势是在程序运行前编译,可以避免在运行时的编译性能消耗和内存消耗。
  • 可以在程序运行初期就达到最高性能,可以显著的加快程序的启动。
  • 再加上IL2CPP的原生C++代码加持,整体而言IL2CPP的效率在Unity下是高于Mono的。

7. 总结

对于新手来说,最重要的是理解Unity如何利用.NET技术来实现跨平台功能。无论是选择Mono还是IL2CPP,都是为了让你的游戏可以在尽可能多的不同设备上顺利运行。随着Unity不断改进其构建流程和技术栈,IL2CPP已经成为推荐的打包方式,因为它提供了更好的性能和更广泛的平台支持。

二、设置IL2CPP

1. 修改打包配置


ProjectSetting->Player->OtherSetting->Configuration->Scripting Backend
把脚本后端设置成IL2CPP

2. 安装IL2CPP模块

假如没有下载对应平台的IL2CPP包 BuildSetting会报错
要先安装对应的IL2CPP模块,比如我们安装windows的IL2CPP模块,安装完成后重启工程

三、IL2CPP打包时的类型裁剪问题

IL2CPP是Unity用来将C#代码转换为C++代码的技术,在打包时,它会对你项目中的代码进行“裁剪”,也就是去掉那些在代码中没有被用到的部分。这么做是为了减小游戏包的大小,提高运行效率。但有时候,一些你并没有直接调用的类型或者代码会被错误地删除,导致运行时出现找不到某个类型的错误,特别是在用到反射等动态调用时。

如何解决?

1. 调整剥离级别

在Unity的设置中,有一个叫做“Managed Stripping Level”的选项,可以选择不同的级别,控制IL2CPP的裁剪程度:

  • Minimal(最小):这是最安全的选择。好像是unity6新增的选项,本来最低只有Low。默认选择这个。
  • Low(低):尽量避免删除重要代码,只有最不常用的代码会被删掉。
  • Medium(中):中等程度的裁剪,可能会删掉一些不常用的代码,但不会删掉核心代码。
  • High(高):最激进的裁剪,尽量删除所有未用代码,能有效减小包的大小,但需要小心可能会删掉你需要的代码。
2. 使用Link.xml文件

你可以通过在Unity项目中(或其任何子目录中)创建一个
Link.xml
文件,告诉Unity哪些类型不能被删掉,确保它们在打包时不会被裁剪掉。
link.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>

用的最多应该就是下面这种,比如保留MyGame程序集下的A整个类型


<?xml version="1.0" encoding="UTF-8"?>
<!--在程序集中保留类型和成员-->
<assembly fullname="Assembly-CSharp">
  <!--保留整个类型-->
  <type fullname="MyGame.A" preserve="all"/>
</assembly>
3. 最佳实战

我们可以把代码剥离等级设置为

,打包出去,出现报错时,再在裁剪类link.xml中添加保留对应报错类的代码。
但是这可能比较考验测试人员能力,假如没测出来有一定风险。

四、IL2CPP打包时的泛型问题

在IL2CPP中,由于它在编译时必须知道所有需要的类型和代码,如果你没有在打包前明确地使用某些泛型类型,它们可能会被裁剪掉,导致运行时找不到相关类型。例如,假设你有两个泛型列表:

List

List
,其中
A

B
是你自定义的类。
如果你在代码中没有显式地使用这些泛型(比如没有写出
List


List
),那么在打包时,这些类型可能会被裁剪掉。如果你后续在热更新时想使用
List
,但是之前并没有显示使用过它,程序就会出错。主要就是因为JIT和AOT两个编译模式的不同造成的。

1. 解决方案

显示使用泛型类型:在代码中显式地声明并使用泛型类型,确保它们在编译时被处理。例如,可以在代码中声明一个包含
List


List
的类,或者编写一个泛型方法,在其中使用这些类型。这样做的目的是告诉IL2CPP,你会在运行时使用这些泛型类型,避免它们被裁剪掉。

2. 泛型类:

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


public class IL2CPP_Info
{
    public List<A> list;
    public List<B> list2;
    public List<C> list3;
    public Dictionary<int, string> dic = new Dictionary<int, string>();
}

3. 泛型方法:

随便写一个静态方法,在将这个泛型方法在其中调用一下。这个静态方法无需被调用,这样做的目的其实就是在预编译之前让IL2CPP知道我们需要使用这个内容


public class IL2CPP_Info
{
    public void Test<T>(T info)
    {
    }
    public static void StaticMethod()
    {
        IL2CPP_Info info = new IL2CPP_Info();
        info.Test<int>(1);
        info.Test<float>(1);
        info.Test<bool>(true);
    }
}

总结

  • 对于新项目,建议使用IL2CPP打包方式,因为它比Mono打包更高效,生成的包也会更小。
  • 如果你遇到类型找不到的问题,可以通过调整剥离级别或者使用
    Link.xml
    文件来确保所需的类型不会被裁剪掉。
  • 如果你使用了泛型,记得在代码中显式地调用这些泛型类型,以确保它们不会被错误地裁剪。
© 2023 北京元石科技有限公司 ◎ 京公网安备 11010802042949号