剪裁 .NET MAUI 应用

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

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

剪裁行为

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

<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 删除隐式转换运算符。

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

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 具有剪裁程序指令(称为功能开关),因此可以保留非剪裁安全功能的代码。 当生成属性设置为 $(TrimMode) 和 Native AOT 时full,可以使用这些剪裁器指令:

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

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

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

使用功能开关的最简单方法是将相应的 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 方法。 如果没有属性,剪裁将删除Helper或完全删除MyAssemblyMyAssembly(如果未在其他位置引用)。

该特性可指定要通过 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>

注意

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

如果修整程序跳过程序集,则它被视为 目录,这意味着它及其所有静态理解的依赖项都会保留。 可以通过将更多 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>

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

在 .NET 9+ 中使用本机 AOT 部署时,将 IsAotCompatible MSBuild 属性设置为 true 还将 true 的值分配给 IsTrimmable 属性,并启用其他 AOT 分析器生成属性。 有关 AOT 分析器的详细信息,请参阅 AOT 兼容性分析器。 有关 .NET MAUI 的本机 AOT 部署的详细信息,请参阅 本机 AOT 部署

将项目文件中的 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>

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

另请参阅