Unity跨平台开发详解:IL2CPP优化打包方案
Unity跨平台开发详解:IL2CPP优化打包方案
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<System.Int32> field1" />
<field signature="System.Collections.Generic.List`1<T> field2" />
<!--保留带有泛型的方法-->
<method signature="System.Void Method1(System.Collections.Generic.List`1<System.Int32>)" />
<!--保留带有泛型的事件-->
<event signature="System.EventHandler`1<System.EventArgs> 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
文件来确保所需的类型不会被裁剪掉。 - 如果你使用了泛型,记得在代码中显式地调用这些泛型类型,以确保它们不会被错误地裁剪。