CS5L4——IL2CPP模式可能存在的问题处理

对于我们目前开发的新项目,都建议大家使用IL2CPP脚本后处理模式来进行打包
主要原因是因为它的效率相对 Mono 较高,同时由于它自带裁剪功能,包的大小也会小一些
但是如果在测试时出现类型无法识别等问题,需要用到我们这节课学习的知识点来解决这些问题

安装IL2CPP打包工具

Edit——Project Settings——Player——Scripting Backend里选择IL2CPP即可将IL2CPP作为脚本后处理方式

image

但是在此之前,我们需要先到Unity Hub里下载IL2CPP模块才能使用IL2CPP打包

image

image

IL2CPP打包存在的问题

类型裁剪

IL2CPP在打包时会自动对Unity工程的DLL进行裁剪,将代码中没有引用到的类型裁剪掉,以达到减小发布后包的尺寸的目的。
然而在实际使用过程中,很多类型有可能会被意外剪裁掉,造成运行时抛出找不到某个类型的异常
特别是通过反射等方式在编译时无法得知的函数调用,在运行时都很有可能遇到问题

解决方案:

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

    • Disabled: Unity不做任何的代码裁剪。
      这个配置只在我们使用Mono的时候可以选择,并且是默认配置
    • Minimal: 最小剥离,Unity只会搜索Unity引擎未使用的.Net类库,不会删除任何用户编写的代码,该设置基本不会出现意外剥离,在使用IL2CPP模式后,该模式是默认模式,此配置是IL2CPP的默认配置(21版以后)
      这个配置最不可能会导致意料之外的运行时表现,此配置多用于可用性优先级高于包体积的产品中。 (21版以后才有该选项)
    • Low: Unity搜索部分用户程序集以及所有的UnityEngine和.NET代码。此配置是IL2CPP的默认配置(20版以前)
      此设置应用一组规则,删除一些未使用的代码,但最大限度地减少出现意外后果的可能性,例如使用反射的运行时代码的行为变化。
    • Medium: Unity 部分地搜索所有程序集以查找无法访问的代码。
      不如低级别剥离谨慎,也不会达到高级别的极端
      此设置应用一组规则,该规则去除更多类型的代码模式,以减少生成大小。尽管Unity不会去掉所有可能的无法访问的代码,但此设置确实增加了不希望或意外行为更改的风险。
    • High: Unity对所有的程序集进行最广泛的检测,Unity 优先考虑缩小代码的大小,而不是代码的稳定性,并且尽可能多地删除代码。
      如果选择该模式一般需要配合link.xml使用

    image

  2. 通过Unity提供的 link.xml 方式来告诉Unity引擎,哪些类型是不能够被剪裁掉的
    在Unity工程的Assets目录中(或其任何子目录中)建立一个叫link.xml的XML文件

    link.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
    84
    <?xml version="1.0" encoding="UTF-8"?>
    <linker>

    <!--保存整个程序集-->
    <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和Mono最大的区别是 不能在运行时动态生成代码和类型
就是说 泛型相关的内容,如果你在打包生成前没有把之后想要使用的泛型类型显示使用一次
那么之后如果使用没有被编译的类型,就会出现找不到类型的报错

举例:List<A>​ 和 List<B>​ 中A和B是我们自定义的类,
我能必须在代码中显示的调用过,IL2CPP才能保留 List<A>​ 和 List<B>​ 两个类型。
如果在热更新时我们调用 List<C>​,但是它之前并没有在代码中显示调用过,
那么这时就会出现报错等问题。主要就是因为JIT和AOT两个编译模式的不同造成的

1
2
3
4
5
6
7
8
9
10
11
public class A { }
public class B { }
public class C { }

public class Lesson1 : MonoBehaviour
{
//这里就显式的声明并调用了List<A>与List<B>两种类型
List<A> list = new List<A>();
List<B> list = new List<B>();
//但我们没有声明和调用List<C>这种类型,这意味着在IL2CPP编译后一旦我们想通过热更新或者反射来构建List<C>就会报错
}

解决方案

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

1
2
3
4
5
6
7
8
9
10
11
12
public class GenericityClass<T> 
{
public void Test<T> (T info) { }
}

public partial class IL2CPP_Info
{
public List<A> listA;
public List<B> listB;
public list<C> listC;
public GenericityClass<A> genericityClassA;
}

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

1
2
3
4
5
6
7
8
9
10
public partial class IL2CPP_Info
{
public static void Test()
{
GenericityClass<A> classInfo = new GenericityClass<A>();
classInfo.Test<int>(1);
classInfo.Test<float>(1f);
classInfo.Test<bool>(true);
}
}