Partager via


Découper une application .NET MAUI

Quand elle génère votre application, l’interface utilisateur d’application multiplateforme .NET (.NET MAUI) peut utiliser un éditeur de liens appelé ILLink pour réduire la taille globale de l’application avec une technique appelée découpage. ILLink réduit la taille en analysant le code intermédiaire produit par le compilateur. Il supprime les méthodes, propriétés, champs, événements, structs et classes inutilisés pour produire une application qui contient uniquement le code et les dépendances d’assembly nécessaires pour exécuter l’application.

Pour empêcher les modifications de comportement lors de la suppression d’applications, .NET fournit une analyse statique de la compatibilité de la suppression par le biais d’avertissements de suppression. Le découpage produit des avertissements lorsqu'il trouve du code qui pourrait ne pas être compatible avec le découpage. S’il existe des avertissements de découpage, ils doivent être corrigés et l’application doit être soigneusement testée après le découpage pour s’assurer qu’il n’y a aucune modification de comportement. Pour plus d’informations, consultez Présentation des avertissements de suppression.

Comportement de découpage

Le comportement de découpage peut être contrôlé en définissant la propriété de build sur l’une $(TrimMode) ou l’autre partial des valeurs fullsuivantes :

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

Important

La $(TrimMode) propriété de build ne doit pas être conditionnée par la configuration de build. Cela est dû au fait que les commutateurs de fonctionnalités sont activés ou désactivés en fonction de la valeur de la $(TrimMode) propriété de build, et que les mêmes fonctionnalités doivent être activées ou désactivées dans toutes les configurations de build afin que votre code se comporte de manière identique.

Le full mode de découpage supprime tout code qui n’est pas utilisé par votre application. Le partial mode trim supprime la bibliothèque de classes de base (BCL), les assemblys pour les plateformes sous-jacentes (telles que Mono.Android.dll et Microsoft.iOS.dll) et tous les autres assemblys qui ont choisi de découper avec l’élément $(TrimmableAsssembly) de build :

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

Cela équivaut à définir [AssemblyMetadata("IsTrimmable", "True")] lors de la génération de l’assembly.

Remarque

Il n’est pas nécessaire de définir la propriété true de build dans le $(PublishTrimmed) fichier projet de votre application, car elle est définie par défaut.

Pour plus d’options de découpage, consultez les options de découpage.

Suppression des valeurs par défaut

Par défaut, les builds Android et Mac Catalyst utilisent un découpage partiel lorsque la configuration de build est définie sur une build de mise en production. iOS utilise le découpage partiel pour toutes les builds d’appareils, quelle que soit la configuration de build et n’utilise pas le découpage pour les builds de simulateur.

Incompatibilités de découpage

Les fonctionnalités .NET MAUI suivantes sont incompatibles avec le découpage complet et seront supprimées par le découpage :

  • Expressions de liaison où ce chemin de liaison est défini sur une chaîne. Utilisez plutôt des liaisons compilées. Pour plus d’informations, consultez Liaisons compilées.
  • Opérateurs de conversion implicite, lors de l’affectation d’une valeur d’un type incompatible à une propriété en XAML, ou lorsque deux propriétés de types différents utilisent une liaison de données. Au lieu de cela, vous devez définir un TypeConverter type pour votre type et l’attacher au type à l’aide du TypeConverterAttribute. Pour plus d’informations, consultez Définir un TypeConverter pour remplacer un opérateur de conversion implicite.
  • Chargement de XAML au moment de l’exécution avec la méthode d’extension LoadFromXaml . Ce code XAML peut être rendu sécurisé en annotant tous les types qui peuvent être chargés au moment de l’exécution avec l’attribut DynamicallyAccessedMembers ou l’attribut DynamicDependency . Toutefois, cela est très sujette à des erreurs et n’est pas recommandé.
  • Réception de données de navigation à l’aide du QueryPropertyAttribute. Au lieu de cela, vous devez implémenter l’interface IQueryAttributable sur les types qui doivent accepter les paramètres de requête. Pour plus d’informations, consultez Traiter les données de navigation à l’aide d’une méthode unique.
  • La propriété SearchHandler.DisplayMemberName. Au lieu de cela, vous devez fournir un ItemTemplate pour définir l’apparence des résultats SearchHandler. Pour plus d’informations, consultez Définir l’apparence de l’élément de résultats de recherche.

Vous pouvez également utiliser des commutateurs de fonctionnalités afin que le découpage conserve le code de ces fonctionnalités. Pour plus d’informations, consultez Découpage des commutateurs de fonctionnalités.

Pour connaître les incompatibilités de découpage .NET, consultez les incompatibilités de découpage connues.

Définir un TypeConverter pour remplacer un opérateur de conversion implicite

Il n’est pas possible de s’appuyer sur des opérateurs de conversion implicite lors de l’affectation d’une valeur d’un type incompatible à une propriété en XAML, ou lorsque deux propriétés de types différents utilisent une liaison de données, lorsque le découpage complet est activé. Cela est dû au fait que les méthodes d’opérateur implicites peuvent être supprimées par le découpage s’ils ne sont pas utilisés dans votre code C#. Pour plus d’informations sur les opérateurs de conversion implicite, consultez les opérateurs de conversion explicites et implicites définis par l’utilisateur.

Par exemple, considérez le type suivant qui définit des opérateurs de conversion implicite entre SizeRequest et 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);
}

Avec la suppression complète activée, les opérateurs de conversion implicite entre SizeRequest et Size peuvent être supprimés par le découpage s’ils ne sont pas utilisés dans votre code C#.

Au lieu de cela, vous devez définir un TypeConverter type pour votre type et l’attacher au type à l’aide des TypeConverterAttributeéléments suivants :

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();
        }
    }
}

Découpage des commutateurs de fonctionnalités

.NET MAUI a des directives de découpage, appelées commutateurs de fonctionnalités, qui permettent de conserver le code pour les fonctionnalités qui ne sont pas coupées en toute sécurité. Ces directives de découpage peuvent être utilisées lorsque la propriété de $(TrimMode) build est définie fullsur , ainsi que pour NativeAOT :

Propriété MSBuild Description
MauiEnableVisualAssemblyScanning Lorsque la valeur est true, .NET MAUI recherche dans les assemblys les types implémentant les attributs IVisual et [assembly:Visual(...)], et enregistre ces types. Par défaut, cette propriété de build est définie sur false.
MauiShellSearchResultsRendererDisplayMemberNameSupported Lorsqu’elle est définie sur false, la valeur de SearchHandler.DisplayMemberName est ignorée. Au lieu de cela, vous devez fournir un ItemTemplate pour définir l’apparence des résultats SearchHandler. Par défaut, cette propriété de build est définie sur true.
MauiQueryPropertyAttributeSupport Lorsqu’ils sont définis sur false, les attributs [QueryProperty(...)] ne sont pas utilisés pour définir les valeurs des propriétés lors de la navigation. Au lieu de cela, vous devez implémenter l’interface IQueryAttributable pour accepter les paramètres de requête. Par défaut, cette propriété de build est définie sur true.
MauiImplicitCastOperatorsUsageViaReflectionSupport Lorsqu’il est défini falsesur , .NET MAUI ne recherche pas d’opérateurs de conversion implicite lors de la conversion de valeurs d’un type vers un autre. Cela peut affecter les liaisons entre des propriétés de types différents et la définition de la valeur d’une propriété d’un objet pouvant être lié avec une valeur d’un type différent. Au lieu de cela, vous devez définir un TypeConverter pour votre type et l’attacher au type à l’aide de l’attribut TypeConverterAttribute. Par défaut, cette propriété de build est définie sur true.
_MauiBindingInterceptorsSupport Lorsque la valeur est false, .NET MAUI n’intercepte pas les appels aux méthodes SetBinding et n’essaie pas de les compiler. Par défaut, cette propriété de build est définie sur true.
MauiEnableXamlCBindingWithSourceCompilation Lorsque la valeur est définie true, .NET MAUI compile toutes les liaisons, y compris celles où la Source propriété est utilisée. Si vous activez cette fonctionnalité, vérifiez que toutes les liaisons sont correctes x:DataType afin qu’elles soient compilées, ou effacez le type de données avec x:Data={x:Null}} lequel la liaison ne doit pas être compilée. Par défaut, cette propriété de build est définie true uniquement lorsque le découpage complet ou le déploiement AOT natif est activé.

Ces propriétés MSBuild ont également des commutateurs équivalents AppContext :

  • La MauiEnableVisualAssemblyScanning propriété MSBuild a un commutateur équivalent AppContext nommé Microsoft.Maui.RuntimeFeature.IsIVisualAssemblyScanningEnabled.
  • La MauiShellSearchResultsRendererDisplayMemberNameSupported propriété MSBuild a un commutateur équivalent AppContext nommé Microsoft.Maui.RuntimeFeature.IsShellSearchResultsRendererDisplayMemberNameSupported.
  • La MauiQueryPropertyAttributeSupport propriété MSBuild a un commutateur équivalent AppContext nommé Microsoft.Maui.RuntimeFeature.IsQueryPropertyAttributeSupported.
  • La MauiImplicitCastOperatorsUsageViaReflectionSupport propriété MSBuild a un commutateur équivalent AppContext nommé Microsoft.Maui.RuntimeFeature.IsImplicitCastOperatorsUsageViaReflectionSupported.
  • La _MauiBindingInterceptorsSupport propriété MSBuild a un commutateur équivalent AppContext nommé Microsoft.Maui.RuntimeFeature.AreBindingInterceptorsSupported.
  • La MauiEnableXamlCBindingWithSourceCompilation propriété MSBuild a un commutateur équivalent AppContext nommé Microsoft.Maui.RuntimeFeature.MauiEnableXamlCBindingWithSourceCompilationEnabled.

Le moyen le plus simple de consommer un commutateur de fonctionnalité consiste à placer la propriété MSBuild correspondante dans le fichier projet de votre application (*.csproj), ce qui entraîne la suppression du code associé à partir des assemblys MAUI .NET.

Conserver le code

Lorsque vous utilisez le découpage, il supprime parfois le code que vous avez appelé dynamiquement, même indirectement. Vous pouvez indiquer au découpage de conserver les membres en les annotant avec l’attribut DynamicDependency . Cet attribut peut être utilisé pour exprimer une dépendance sur un type et un sous-ensemble de membres, ou à des membres spécifiques.

Important

Chaque membre de la BCL dont l’utilisation par l’application ne peut pas être déterminée de manière statique est soumis à la suppression.

L’attribut DynamicDependency peut être appliqué aux constructeurs, aux champs et aux méthodes :

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

Dans cet exemple, l’attribut DynamicDependency assure que la méthode Helper est conservée. Sans l’attribut, le découpage supprime ou supprime Helper MyAssembly MyAssembly complètement s’il n’est pas référencé ailleurs.

L’attribut spécifie le membre à conserver via une string ou via l’attribut DynamicallyAccessedMembers. Le type et l’assembly sont implicites dans le contexte d’attribut ou spécifiés explicitement dans l’attribut (par Type, ou par string pour le type et le nom de l’assembly).

Les chaînes de type et de membre utilisent une variante du format de chaîne d’ID de commentaire de documentation C#, sans préfixe de membre. La chaîne de membre ne doit pas inclure le nom du type déclarant et peut omettre des paramètres pour conserver tous les membres du nom spécifié. Les exemples suivants montrent des utilisations valides :

[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<>))]

Conserver les assemblys

Il est possible de spécifier des assemblys qui doivent être exclus du processus de découpage, tout en permettant à d’autres assemblys d’être supprimés. Cette approche peut être utile lorsque vous ne pouvez pas facilement utiliser l’attribut DynamicDependency ou ne contrôlez pas le code en cours de suppression.

Quand il supprime tous les assemblys, vous pouvez indiquer au découpage d’ignorer un assembly en définissant un TrimmerRootAssembly élément MSBuild dans le fichier projet :

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

Remarque

L’extension .dll n’est pas nécessaire lors de la définition de la propriété MSBuild TrimmerRootAssembly.

Si le découpage ignore un assembly, il est considéré comme rooté, ce qui signifie que toutes ses dépendances comprises statiquement sont conservées. Vous pouvez ignorer des assemblys supplémentaires en ajoutant d’autres propriétés MSBuild TrimmerRootAssembly à la balise <ItemGroup>.

Conserver des assemblys, des types et des membres

Vous pouvez passer le découpage d’un fichier de description XML qui spécifie quels assemblys, types et membres doivent être conservés.

Pour exclure un membre du processus de découpage lors de la suppression de tous les assemblys, définissez l’élément TrimmerRootDescriptor MSBuild dans le fichier projet sur le fichier XML qui définit les membres à exclure :

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

Le fichier XML utilise ensuite le format de descripteur de découpage pour définir les membres à exclure :

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

Dans cet exemple, le fichier XML spécifie une méthode accessible dynamiquement par l’application, qui est exclue du découpage.

Lorsqu’un assembly, un type ou un membre est répertorié dans le code XML, l’action par défaut est conservée, ce qui signifie que, indépendamment du fait que le découpage pense qu’il est utilisé ou non, il est conservé dans la sortie.

Remarque

Les balises de conservation sont inclusives de manière ambiguë. Si vous ne fournissez pas le niveau de détail suivant, il inclut tous les enfants. Si un assembly est répertorié sans type, tous les types et membres de l’assembly sont conservés.

Marquer un assembly comme étant sécurisé

Si vous disposez d’une bibliothèque dans votre projet ou si vous êtes développeur d’une bibliothèque réutilisable et que vous souhaitez que le découpage traite votre assembly comme rognable, vous pouvez marquer l’assembly comme étant supprimé en ajoutant la IsTrimmable propriété MSBuild au fichier projet pour l’assembly :

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

Cela marque votre assembly comme « découpable » et active les avertissements de découpage pour ce projet. Être « découpable » signifie que votre assembly est considéré comme compatible avec le découpage et qu’il ne doit pas avoir d’avertissements de découpage lorsqu’il est généré. Lorsqu’il est utilisé dans une application découpée, les membres inutilisés de l’assembly sont supprimés dans la sortie finale.

La définition de la propriété MSBuild IsTrimmable sur true dans votre fichier projet insère l’attribut AssemblyMetadata dans votre assembly :

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

Vous pouvez également ajouter l’attribut AssemblyMetadata dans votre assembly sans avoir ajouté la propriété MSBuild IsTrimmable au fichier projet de votre assembly.

Remarque

Si la propriété MSBuild IsTrimmable est définie pour un assembly, cela remplace l’attribut AssemblyMetadata("IsTrimmable", "True"). Cela vous permet d’opter pour un assembly dans le découpage même s’il n’a pas l’attribut ou de désactiver le découpage d’un assembly qui a l’attribut.

Supprimer les avertissements d’analyse

Lorsque le découpage est activé, il supprime l’il qui n’est pas accessible de manière statique. Les applications qui utilisent la réflexion ou d’autres modèles qui créent des dépendances dynamiques peuvent être rompues en conséquence. Pour avertir de tels modèles, lors du marquage d’un assembly comme sécurisé, les auteurs de bibliothèque doivent définir la SuppressTrimAnalysisWarnings propriété MSBuild sur false:

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

La suppression des avertissements d’analyse de découpage inclut des avertissements sur l’ensemble de l’application, y compris votre propre code, le code de la bibliothèque et le code du Kit de développement logiciel (SDK).

Afficher les avertissements détaillés

L’analyse de découpage génère au plus un avertissement pour chaque assembly provenant d’un PackageReference, ce qui indique que les éléments internes de l’assembly ne sont pas compatibles avec le découpage. En tant qu’auteur de bibliothèque, lorsque vous marquez un assembly comme sécurisé, vous devez activer des avertissements individuels pour tous les assemblys en définissant la TrimmerSingleWarn propriété MSBuild sur false:

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

Ce paramètre affiche tous les avertissements détaillés, au lieu de les réduire à un seul avertissement par assembly.

Voir aussi