剪裁 .NET MAUI 应用

生成应用时,.NET 多平台应用 UI(.NET MAUI)可以使用调用 ILLink 的链接器,通过称为剪裁的技术来减小应用的整体大小。 ILLink 通过分析编译器生成的中间代码来减小大小。 这会删除未使用的方法、属性、字段、事件、结构和类,以生成仅包含运行应用所需的代码和程序集依赖项的应用。

为了防止在剪裁应用时发生行为更改,.NET 通过剪裁警告提供剪裁兼容性的静态分析。 当发现可能与剪裁不兼容的代码时,剪裁器会产生剪裁警告。 如果有任何剪裁警告,则应修复这些警告,并且应用应在修整后进行彻底测试,以确保没有任何行为更改。 有关详细信息,请参阅 剪裁警告简介。

剪裁行为

可以通过将$(TrimMode)生成属性设置为以下任一或full以下两者partial来控制剪裁行为:

<PropertyGroup>
  <TrimMode>full</TrimMode>
</PropertyGroup>

重要

$(TrimMode)生成属性不应按生成配置进行条件。 这是因为功能开关基于生成属性的值 $(TrimMode) 启用或禁用,并且应在所有生成配置中启用或禁用相同的功能,以便代码的行为完全相同。

full剪裁模式删除应用未使用的任何代码。 partial剪裁模式剪裁基础类库(BCL)、基础平台的程序集(如 Mono.Android.dllMicrosoft.iOS.dll),以及选择使用$(TrimmableAsssembly)生成项剪裁的任何其他程序集:

<ItemGroup>
  <TrimmableAssembly Include="MyAssembly" />
</ItemGroup>

这相当于在生成程序集时设置 [AssemblyMetadata("IsTrimmable", "True")]

注意

无需在 $(PublishTrimmed) 应用的项目文件中将生成属性设置为 true ,因为默认情况下会设置此属性。

有关更多剪裁选项,请参阅 剪裁选项

剪裁默认值

默认情况下,当生成配置设置为发布版本时,Android 和 Mac Catalyst 版本使用部分修整。 无论生成配置如何,iOS 都会对任何设备生成使用部分修整,也不对模拟器生成使用剪裁。

剪裁不兼容

以下 .NET MAUI 功能与完整修整不兼容,将由修整程序删除:

或者,可以使用功能开关,以便剪裁器保留这些功能的代码。 有关详细信息,请参阅 剪裁功能开关

有关 .NET 修整不兼容,请参阅 已知修整不兼容

定义 TypeConverter 以替换隐式转换运算符

在为 XAML 中的属性分配不兼容类型的值时,或者当启用完全修整时,不同类型的两个属性使用数据绑定时,不可能依赖于隐式转换运算符。 这是因为剪裁器可以在 C# 代码中使用隐式运算符方法时将其删除。 有关隐式转换运算符的详细信息,请参阅 用户定义的显式转换运算符和隐式转换运算符

例如,请考虑以下类型,该类型定义两者之间SizeRequest的隐式转换运算符:Size

namespace MyMauiApp;

public struct SizeRequest : IEquatable<SizeRequest>
{
    public Size Request { get; set; }
    public Size Minimum { get; set; }

    public SizeRequest(Size request, Size minimum)
    {
        Request = request;
        Minimum = minimum;
    }

    public SizeRequest(Size request)
    {
        Request = request;
        Minimum = request;
    }

    public override string ToString()
    {
        return string.Format("{{Request={0} Minimum={1}}}", Request, Minimum);
    }

    public bool Equals(SizeRequest other) => Request.Equals(other.Request) && Minimum.Equals(other.Minimum);

    public static implicit operator SizeRequest(Size size) => new SizeRequest(size);
    public static implicit operator Size(SizeRequest size) => size.Request;
    public override bool Equals(object? obj) => obj is SizeRequest other && Equals(other);
    public override int GetHashCode() => Request.GetHashCode() ^ Minimum.GetHashCode();
    public static bool operator ==(SizeRequest left, SizeRequest right) => left.Equals(right);
    public static bool operator !=(SizeRequest left, SizeRequest right) => !(left == right);
}

启用完全修整后,如果 C# 代码中未使用隐式转换运算符,则剪裁器之间 SizeRequest 可以 Size 删除隐式转换运算符。

相反,应使用以下命令TypeConverterAttribute为类型定义一个TypeConverter类型并将其附加到该类型:

using System.ComponentModel;
using System.Diagnostics.CodeAnalysis;
using System.Globalization;

namespace MyMauiApp;

[TypeConverter(typeof(SizeRequestTypeConverter))]
public struct SizeRequest : IEquatable<SizeRequest>
{
    public Size Request { get; set; }
    public Size Minimum { get; set; }

    public SizeRequest(Size request, Size minimum)
    {
        Request = request;
        Minimum = minimum;
    }

    public SizeRequest(Size request)
    {
        Request = request;
        Minimum = request;
    }

    public override string ToString()
    {
        return string.Format("{{Request={0} Minimum={1}}}", Request, Minimum);
    }

    public bool Equals(SizeRequest other) => Request.Equals(other.Request) && Minimum.Equals(other.Minimum);

    public static implicit operator SizeRequest(Size size) => new SizeRequest(size);
    public static implicit operator Size(SizeRequest size) => size.Request;
    public override bool Equals(object? obj) => obj is SizeRequest other && Equals(other);
    public override int GetHashCode() => Request.GetHashCode() ^ Minimum.GetHashCode();
    public static bool operator ==(SizeRequest left, SizeRequest right) => left.Equals(right);
    public static bool operator !=(SizeRequest left, SizeRequest right) => !(left == right);

    private sealed class SizeRequestTypeConverter : TypeConverter
    {
        public override bool CanConvertFrom(ITypeDescriptorContext? context, Type sourceType)
            => sourceType == typeof(Size);

        public override object? ConvertFrom(ITypeDescriptorContext? context, CultureInfo? culture, object value)
            => value switch
            {
                Size size => (SizeRequest)size,
                _ => throw new NotSupportedException()
            };

        public override bool CanConvertTo(ITypeDescriptorContext? context, [NotNullWhen(true)] Type? destinationType)
            => destinationType == typeof(Size);

        public override object? ConvertTo(ITypeDescriptorContext? context, CultureInfo? culture, object? value, Type destinationType)
        {
            if (value is SizeRequest sizeRequest)
            {
                if (destinationType == typeof(Size))
                    return (Size)sizeRequest;
            }
            throw new NotSupportedException();
        }
    }
}

剪裁功能开关

.NET MAUI 具有剪裁程序指令(称为功能开关),因此可以保留非剪裁安全功能的代码。 当生成属性设置为 full 以及 NativeAOT 时$(TrimMode),可以使用这些剪裁器指令:

MSBuild 属性 说明
MauiEnableVisualAssemblyScanning 设置为 true 时,.NET MAUI 将扫描程序集以获取实现 IVisual 的类型和 [assembly:Visual(...)] 属性,并将注册这些类型。 默认情况下,此生成属性设置为 false
MauiShellSearchResultsRendererDisplayMemberNameSupported 设置为 false 时,将忽略 SearchHandler.DisplayMemberName 的值。 相反,应提供 ItemTemplate 来定义 SearchHandler 结果的外观。 默认情况下,此生成属性设置为 true
MauiQueryPropertyAttributeSupport 当设置为 false 时,导航时将不会使用 [QueryProperty(...)] 属性来设置属性值。 相反,应实现 IQueryAttributable 接口以接受查询参数。 默认情况下,此生成属性设置为 true
MauiImplicitCastOperatorsUsageViaReflectionSupport 设置为 false“设置为”时,.NET MAUI 在将值从一种类型转换为另一种类型时,不会查找隐式转换运算符。 这可能会影响具有不同类型的属性之间的绑定,以及设置具有不同类型值的可绑定对象的属性值。 相反,应为类型定义 TypeConverter,并使用 TypeConverterAttribute 特性将其附加到该类型。 默认情况下,此生成属性设置为 true
_MauiBindingInterceptorsSupport 设置为 false 时,.NET MAUI 不会截获对 SetBinding 方法的任何调用,也不会尝试编译它们。 默认情况下,此生成属性设置为 true
MauiEnableXamlCBindingWithSourceCompilation 设置为 true.NET MAUI 时,将编译所有绑定,包括使用该属性的 Source 绑定。 如果启用此功能,请确保所有绑定都具有正确的 x:DataType 编译方式,或者清除 x:Data={x:Null}} 数据类型(如果不应编译绑定)。 默认情况下,此生成属性仅在启用完全修整或本机 AOT 部署时设置为 true

这些 MSBuild 属性还具有等效 AppContext 开关:

  • MauiEnableVisualAssemblyScanning MSBuild 属性具有一个名为 AppContext Microsoft.Maui.RuntimeFeature.IsIVisualAssemblyScanningEnabled..
  • MauiShellSearchResultsRendererDisplayMemberNameSupported MSBuild 属性具有一个名为 AppContext Microsoft.Maui.RuntimeFeature.IsShellSearchResultsRendererDisplayMemberNameSupported..
  • MauiQueryPropertyAttributeSupport MSBuild 属性具有一个名为 AppContext Microsoft.Maui.RuntimeFeature.IsQueryPropertyAttributeSupported..
  • MauiImplicitCastOperatorsUsageViaReflectionSupport MSBuild 属性具有一个名为 AppContext Microsoft.Maui.RuntimeFeature.IsImplicitCastOperatorsUsageViaReflectionSupported..
  • _MauiBindingInterceptorsSupport MSBuild 属性具有一个名为 AppContext Microsoft.Maui.RuntimeFeature.AreBindingInterceptorsSupported..
  • MauiEnableXamlCBindingWithSourceCompilation MSBuild 属性具有一个名为 AppContext Microsoft.Maui.RuntimeFeature.MauiEnableXamlCBindingWithSourceCompilationEnabled..

使用功能开关的最简单方法是将相应的 MSBuild 属性放入应用的项目文件(*.csproj),这会导致从 .NET MAUI 程序集中剪裁相关代码。

保留代码

使用剪裁器时,它有时会删除你可能已动态调用的代码,甚至间接调用。 可以通过使用 DynamicDependency 特性批注这些成员来指示剪裁器保留成员。 此特性可用于表示与成员类型和子集或特定成员的依赖关系。

重要说明

BCL 中的无法以静态方式确定会由应用使用的所有成员都将被删除。

DynamicDependency 特性可以应用于构造函数、字段和方法:

[DynamicDependency("Helper", "MyType", "MyAssembly")]
static void RunHelper()
{
    var helper = Assembly.Load("MyAssembly").GetType("MyType").GetMethod("Helper");
    helper.Invoke(null, null);
}

在此示例中,DynamicDependency 可确保保留 Helper 方法。 如果没有属性,剪裁将删除MyAssembly或完全删除HelperMyAssembly(如果未在其他位置引用)。

该特性可指定要通过 stringDynamicallyAccessedMembers 特性保留的成员。 类型和程序集要么在属性上下文中隐式指定,要么在属性中显式指定(对于类型和程序集名称,通过 Typestring 指定)。

类型和成员字符串使用 C# 文档注释 ID 字符串格式的变体,不带成员前缀。 成员字符串不应包含声明类型的名称,并且可以省略参数以保留指定名称的所有成员。 以下示例展示了有效的用途:

[DynamicDependency("Method()")]
[DynamicDependency("Method(System,Boolean,System.String)")]
[DynamicDependency("MethodOnDifferentType()", typeof(ContainingType))]
[DynamicDependency("MemberName")]
[DynamicDependency("MemberOnUnreferencedAssembly", "ContainingType", "UnreferencedAssembly")]
[DynamicDependency("MemberName", "Namespace.ContainingType.NestedType", "Assembly")]
// generics
[DynamicDependency("GenericMethodName``1")]
[DynamicDependency("GenericMethod``2(``0,``1)")]
[DynamicDependency("MethodWithGenericParameterTypes(System.Collections.Generic.List{System.String})")]
[DynamicDependency("MethodOnGenericType(`0)", "GenericType`1", "UnreferencedAssembly")]
[DynamicDependency("MethodOnGenericType(`0)", typeof(GenericType<>))]

保留程序集

可以指定应从剪裁过程中排除的程序集,同时允许剪裁其他程序集。 当无法轻松使用 DynamicDependency 属性或不控制正在剪裁的代码时,此方法非常有用。

当它剪裁所有程序集时,可以通过在项目文件中设置 TrimmerRootAssembly MSBuild 项来指示修整程序跳过程序集:

<ItemGroup>
  <TrimmerRootAssembly Include="MyAssembly" />
</ItemGroup>

注意

设置 TrimmerRootAssembly MSBuild 属性时不需要 .dll 扩展。

如果修整程序跳过程序集,则它被视为 目录,这意味着它及其所有静态理解的依赖项都会保留。 可以通过将更多 TrimmerRootAssembly MSBuild 属性添加到 <ItemGroup>,来跳过其他程序集。

保留程序集、类型和成员

可以传递剪裁器 XML 说明文件,该文件指定需要保留哪些程序集、类型和成员。

若要在剪裁所有程序集时从剪裁过程中排除成员,请将项目文件中的 MSBuild 项设置为 TrimmerRootDescriptor 定义要排除的成员的 XML 文件:

<ItemGroup>
  <TrimmerRootDescriptor Include="MyRoots.xml" />
</ItemGroup>

然后,XML 文件使用剪裁器 描述符格式 来定义要排除的成员:

<linker>
  <assembly fullname="MyAssembly">
    <type fullname="MyAssembly.MyClass">
      <method name="DynamicallyAccessedMethod" />
    </type>
  </assembly>
</linker>

在此示例中,XML 文件指定了应用动态访问的方法,该方法被排除在修整之外。

当 XML 中列出程序集、类型或成员时,默认操作将保留,这意味着无论剪裁器认为它是否使用,它都会保留在输出中。

注意

保留标记所包含的内容并不确定。 如果未提供下一级的详细信息,它将包括所有子级。 如果列出的程序集没有任何类型,则将保留该程序集的所有类型和成员。

将程序集标记为剪裁安全

如果你项目中有一个库,或者你是可重用库的开发人员,并且希望修整程序将程序集视为可修整的,则可以通过将 IsTrimmable MSBuild 属性添加到程序集的项目文件中来将程序集标记为安全剪裁:

<PropertyGroup>
    <IsTrimmable>true</IsTrimmable>
</PropertyGroup>

这会将程序集标记为“可剪裁”,并为该项目启用剪裁警告。 “可剪裁”意味着程序集被视为与剪裁兼容,生成程序集时不应显示剪裁警告。 在经过剪裁的应用中使用时,会在最终输出中删除程序集未使用的成员。

将项目文件中的 IsTrimmable MSBuild 属性设置为 true 会将 AssemblyMetadata 属性插入程序集:

[assembly: AssemblyMetadata("IsTrimmable", "True")]

或者,可以将 AssemblyMetadata 特性添加到程序集中,而无需将 IsTrimmable MSBuild 属性添加到程序集的项目文件中。

注意

如果为程序集设置了 IsTrimmable MSBuild 属性,则会替代 AssemblyMetadata("IsTrimmable", "True") 特性。 这使你可以选择为程序集启用剪裁(即使程序集没有该特性),也可以禁止裁剪具有该特性的程序集。

抑制分析警告

启用剪裁器后,它会删除不可静态访问的 IL。 因此,使用反射或其他模式创建动态依赖项的应用可能会中断运行。 若要警告此类模式,在将程序集标记为剪裁安全时,库作者应将 SuppressTrimAnalysisWarnings MSBuild 属性设置为 false

<PropertyGroup>
  <SuppressTrimAnalysisWarnings>false</SuppressTrimAnalysisWarnings>
</PropertyGroup>

不抑制剪裁分析警告将包括有关整个应用的警告,其中包括你自己的代码、库代码以及 SDK 代码。

显示详细警告

对于每个来自 PackageReference 的程序集,剪裁分析最多为其生成一个警告,以指示程序集的内部机制与剪裁功能不兼容。 作为库作者,将程序集标记为剪裁安全时,应通过将 MSBuild 属性设置为TrimmerSingleWarnfalse

<PropertyGroup>
  <TrimmerSingleWarn>false</TrimmerSingleWarn>
</PropertyGroup>

该设置显示所有详细警告,而不是将其折叠为每个程序集的单个警告。

另请参阅