Share via


Améliorer les performances de l’application

Les performances médiocres de l’application se présentent de nombreuses façons. Il peut rendre une application qui ne répond pas, peut provoquer un défilement lent et peut réduire la durée de vie de la batterie de l’appareil. Toutefois, l’optimisation des performances implique davantage de choses que l’implémentation d’un code efficace. L’expérience utilisateur des performances des applications doit également être prise en compte. Par exemple, pour contribuer à améliorer l’expérience utilisateur, vous devez vérifier que les opérations s’exécutent sans empêcher l’utilisateur d’effectuer d’autres activités.

Il existe de nombreuses techniques pour augmenter les performances et les performances perçues, des applications .NET multi-plateforme App UI (.NET MAUI). Collectivement, ces techniques peuvent réduire considérablement la quantité de travail effectuée par un processeur et la quantité de mémoire consommée par une application.

Utiliser un profileur

Lors du développement d’une application, il est important de ne tenter d’optimiser le code qu’une fois qu’elle a été profilée. Le profilage est une technique qui permet de déterminer les endroits où les optimisations de code seront les plus efficaces pour réduire les problèmes de performances. Le profileur suit l’utilisation de la mémoire de l’application et enregistre l’heure d’exécution des méthodes dans l’application. Ces données permettent de parcourir les chemins d’exécution de l’application et le coût d’exécution du code, afin que les meilleures opportunités d’optimisation puissent être découvertes.

Les applications .NET MAUI peuvent être profilées à l’aide de dotnet-trace sur Android, iOS et Mac, et de PerfView sur Windows. Pour plus d’informations, consultez Profilage des applications .NET MAUI.

Les bonnes pratiques suivantes sont recommandées lorsque vous profilez une application :

  • Évitez de profiler une application dans un simulateur, car le simulateur peut fausser les performances de l’application.
  • Dans l’idéal, le profilage doit être effectué sur plusieurs types d’appareils. En effet, les performances mesurées sur un appareil ne seront pas forcément les mêmes sur un autre appareil. Cependant, le profilage doit au moins être effectué sur un appareil dont les spécifications anticipées sont les moins élevées.
  • Fermez toutes les autres applications pour vous assurer que l’impact total de l’application profilée est mesuré, plutôt que les autres applications.

Utiliser des liaisons compilées

Les liaisons compilées améliorent les performances de liaison de données dans les applications .NET MAUI en résolvant les expressions de liaison au moment de la compilation, plutôt qu’au moment de l’exécution avec réflexion. La compilation d’une expression de liaison génère du code compilé qui résout généralement une liaison 8 à 20 fois plus rapidement qu’avec une liaison classique. Pour plus d’informations, consultez Liaisons compilées.

Réduire les liaisons inutiles

N’utilisez pas de liaisons pour le contenu qui peut être aisément défini de manière statique. Il n’existe aucun avantage à lier des données qui n’ont pas besoin de l’être, car les liaisons ne sont pas rentables. Par exemple, le paramètre Button.Text = "Accept" a moins de surcharge que la liaison Button.Text à une propriété string viewmodel avec la valeur « Accepter ».

Choisir la disposition correcte

Une disposition qui est capable d’afficher plusieurs enfants, mais qui n’en a qu’un seul, est inutile. Par exemple, l’exemple suivant montre un VerticalStackLayout avec un enfant unique :

<ContentPage xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
             xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
             x:Class="MyMauiApp.MainPage">
    <VerticalStackLayout>
        <Image Source="waterfront.jpg" />
    </VerticalStackLayout>
</ContentPage>

Il s’agit d’un gaspillage et l’élément VerticalStackLayout doit être supprimé, comme illustré dans l’exemple suivant :

<ContentPage xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
             xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
             x:Class="MyMauiApp.MainPage">
    <Image Source="waterfront.jpg" />
</ContentPage>

En outre, n’essayez pas de reproduire l’apparence d’une disposition spécifique à l’aide de combinaisons d’autres dispositions, car cela aboutirait à des calculs de dispositions inutiles. Par exemple, ne tentez pas de reproduire une disposition Grid à l’aide d’une combinaison d’éléments HorizontalStackLayout. L’exemple suivant vous montre un exemple de cette mauvaise pratique :

<ContentPage xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
             xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
             x:Class="MyMauiApp.MainPage">
    <VerticalStackLayout>
        <HorizontalStackLayout>
            <Label Text="Name:" />
            <Entry Placeholder="Enter your name" />
        </HorizontalStackLayout>
        <HorizontalStackLayout>
            <Label Text="Age:" />
            <Entry Placeholder="Enter your age" />
        </HorizontalStackLayout>
        <HorizontalStackLayout>
            <Label Text="Occupation:" />
            <Entry Placeholder="Enter your occupation" />
        </HorizontalStackLayout>
        <HorizontalStackLayout>
            <Label Text="Address:" />
            <Entry Placeholder="Enter your address" />
        </HorizontalStackLayout>
    </VerticalStackLayout>
</ContentPage>

Cela ne sert à rien, car des calculs de dispositions inutiles sont effectués. Au lieu de cela, la disposition souhaitée peut être améliorée à l’aide d’un Grid, comme illustré dans l’exemple suivant :

<ContentPage xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
             xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
             x:Class="MyMauiApp.MainPage">
    <Grid ColumnDefinitions="100,*"
          RowDefinitions="30,30,30,30">
        <Label Text="Name:" />
        <Entry Grid.Column="1"
               Placeholder="Enter your name" />
        <Label Grid.Row="1"
               Text="Age:" />
        <Entry Grid.Row="1"
               Grid.Column="1"
               Placeholder="Enter your age" />
        <Label Grid.Row="2"
               Text="Occupation:" />
        <Entry Grid.Row="2"
               Grid.Column="1"
               Placeholder="Enter your occupation" />
        <Label Grid.Row="3"
               Text="Address:" />
        <Entry Grid.Row="3"
               Grid.Column="1"
               Placeholder="Enter your address" />
    </Grid>
</ContentPage>

Optimiser les ressources d’images

Les images sont certaines des ressources les plus coûteuses que les applications utilisent et sont souvent capturées à des résolutions élevées. Bien que cela crée des images dynamiques pleines de détails, les applications qui affichent ces images nécessitent généralement davantage d’utilisation du processeur pour décoder l’image et plus de mémoire pour stocker l’image décodée. Le décodage d’une image haute résolution dans la mémoire peut être vu comme un gaspillage de ressources lorsque l’on sait que l’image va être réduite pour son affichage dans l’application. Au lieu de cela, réduisez l’utilisation du processeur et l’empreinte mémoire en créant des versions d’images stockées proches des tailles d’affichage prédites. Par exemple, une image qui doit s’afficher dans une liste aura probablement besoin d’une résolution inférieure à celle d’une image devant s’afficher en plein écran.

En outre, les images ne doivent être créées qu’une fois requises et publiées dès que l’application ne les requiert plus. Par exemple, si une application affiche une image en lisant ses données à partir d’un flux, assurez-vous que le flux est créé uniquement lorsqu’il est nécessaire et vérifiez que le flux est libéré lorsqu’il n’est plus nécessaire. Pour cela, créez le flux quand la page est créée ou que l’événement Page.Appearing se déclenche, puis supprimez-le quand l’événement Page.Disappearing se déclenche.

Lors du téléchargement d’une image pour l’affichage avec la méthode ImageSource.FromUri(Uri), vérifiez que l’image téléchargée est mise en cache pendant une durée appropriée. Pour plus d’informations, consultez l’article Mise en cache de l’image.

Réduire la taille de l’arborescence d’éléments visuels

La réduction du nombre d’éléments dans une page accélère l’affichage de la page. Pour ce faire, il existe deux techniques principales. La première consiste à masquer les éléments qui ne sont pas visibles. La propriété IsVisible de chaque élément détermine si l’élément doit faire partie de l’arborescence d’éléments visuels ou pas. Par conséquent, si un élément n’est pas visible parce qu’il est masqué par d’autres éléments, supprimez l’élément ou affectez à sa propriété IsVisible la valeur false.

La seconde technique consiste à supprimer les éléments inutiles. Par exemple, la mise en page suivante montre une mise en page contenant plusieurs éléments Label :

<VerticalStackLayout>
    <VerticalStackLayout Padding="20,20,0,0">
        <Label Text="Hello" />
    </VerticalStackLayout>
    <VerticalStackLayout Padding="20,20,0,0">
        <Label Text="Welcome to the App!" />
    </VerticalStackLayout>
    <VerticalStackLayout Padding="20,20,0,0">
        <Label Text="Downloading Data..." />
    </VerticalStackLayout>
</VerticalStackLayout>

La même mise en page peut être conservée avec un nombre d’éléments réduit, comme illustré dans l’exemple suivant :

<VerticalStackLayout Padding="20,35,20,20"
                     Spacing="25">
    <Label Text="Hello" />
    <Label Text="Welcome to the App!" />
    <Label Text="Downloading Data..." />
</VerticalStackLayout>

Réduire la taille du dictionnaire de ressources de l’application

Toutes les ressources utilisées dans toute l’application doivent être stockées dans le dictionnaire de ressources de l’application pour éviter la duplication. Cela permet de réduire la quantité de XAML qui doit être analysée dans l’ensemble de l’application. L’exemple suivant montre la ressource HeadingLabelStyle, qui est utilisée à l’échelle de l’application, et elle est donc définie dans le dictionnaire de ressources de l’application :

<Application xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
             xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
             x:Class="MyMauiApp.App">
     <Application.Resources>
        <Style x:Key="HeadingLabelStyle"
               TargetType="Label">
            <Setter Property="HorizontalOptions"
                    Value="Center" />
            <Setter Property="FontSize"
                    Value="Large" />
            <Setter Property="TextColor"
                    Value="Red" />
        </Style>
     </Application.Resources>
</Application>

Toutefois, le code XAML spécifique à une page ne doit pas être inclus dans le dictionnaire de ressources de l’application, car les ressources seront ensuite analysées au démarrage de l’application au lieu d’être requises par une page. Si une ressource est utilisée par une page qui n’est pas la page de démarrage, elle doit être placée dans le dictionnaire de ressources de cette page, ce qui permet de réduire le code XAML analysé au démarrage de l’application. L’exemple suivant montre la ressource HeadingLabelStyle, qui se trouve uniquement sur une seule page, et est donc définie dans le dictionnaire de ressources de la page :

<ContentPage xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
             xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
             x:Class="MyMauiApp.MainPage">
    <ContentPage.Resources>
        <Style x:Key="HeadingLabelStyle"
                TargetType="Label">
            <Setter Property="HorizontalOptions"
                    Value="Center" />
            <Setter Property="FontSize"
                    Value="Large" />
            <Setter Property="TextColor"
                    Value="Red" />
        </Style>
    </ContentPage.Resources>
    ...
</ContentPage>

Pour plus d’informations sur les ressources d’application, consultez Style des applications à l’aide de XAML.

Réduisez la taille de l'application

Lorsque .NET MAUI génère votre application, un éditeur de liens appelé ILLink peut être utilisé pour réduire la taille globale de l’application. 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 du code et des dépendances d’assembly nécessaires pour exécuter l’application.

Pour plus d’informations sur la configuration du comportement de l’éditeur de liens, consultez Liaison d’une application Android, Liaison d’une application iOS etLiaison d’une application Mac Catalyst.

Réduire la période d’activation de l’application

Toutes les applications ont une période d’activation, c’est-à-dire entre le démarrage de l’application et le moment où l’application est prête à être utilisée. Cette période d’activation fournit aux utilisateurs leur première impression de l’application, et il est donc important de réduire la période d’activation et la perception de l’utilisateur, afin qu’ils obtiennent une première impression favorable de l’application.

Avant qu’une application affiche son interface utilisateur initiale, elle doit fournir un écran de démarrage pour indiquer à l’utilisateur que l’application démarre. Si l’application ne peut pas afficher rapidement son interface utilisateur initiale, l’écran de démarrage doit être utilisé pour informer l’utilisateur de la progression pendant la période d’activation, afin d’offrir une réassurance que l’application n’a pas suspendu. Pour cela, vous pouvez utiliser une barre de progression ou un autre contrôle similaire.

Pendant la période d’activation, les applications exécutent une logique d’activation, qui inclut souvent le chargement et le traitement des ressources. Pour réduire la période d’activation, empaquetez les ressources nécessaires dans l’application, au lieu de les récupérer à distance. Par exemple, dans certains cas, il peut être judicieux de charger des données d’espace réservé stockées localement pendant la période d’activation. Ensuite, une fois que l’interface utilisateur initiale est affichée et que l’utilisateur peut interagir avec l’application, les données d’espace réservé peuvent être remplacées progressivement à partir d’une source distante. En outre, la logique d’activation de l’application ne doit effectuer que le travail nécessaire pour permettre à l’utilisateur de commencer à utiliser l’application. Il peut être utile de différer le chargement des assemblys supplémentaires, car les assemblys sont chargés lors de leur première utilisation.

Choisir un conteneur d’injection de dépendances avec précaution

Les conteneurs d’injection de dépendances introduisent des contraintes de performances supplémentaires dans les applications mobiles. L’inscription et la résolution des types avec un conteneur ont un coût en termes de performances en raison de l’utilisation par le conteneur de la réflexion pour créer chaque type, et plus particulièrement si les dépendances sont reconstruites pour chaque navigation entre les pages de l’application. S’il existe de nombreuses dépendances ou des dépendances profondes, le coût de création peut augmenter de manière significative. En outre, l’inscription de type, qui se produit généralement pendant le démarrage de l’application, peut avoir un impact notable sur le temps de démarrage, en fonction du conteneur utilisé. Pour plus d’informations sur l’injection de dépendances dans les applications .NET MAUI, consultez Injection de dépendances.

En guise d’alternative, l’injection de dépendances peut être rendue plus performante en l’implémentant manuellement à l’aide de fabriques.

Créer des applications Shell

Les applications .NET MAUI Shell offrent une expérience de navigation avisée basée sur les menus volants et les onglets. Si l’expérience utilisateur de votre application peut être implémentée avec Shell, il est utile de le faire. Les applications Shell permettent d’éviter une mauvaise expérience de démarrage, car les pages sont créées à la demande en réponse à la navigation plutôt qu’au démarrage de l’application, ce qui se produit avec des applications qui utilisent un TabbedPage. Pour en savoir plus, consultez Présentation de Shell.

Optimiser les performances de ListView

Lorsque vous utilisez ListView, un certain nombre d’expériences utilisateur doit être optimisé :

  • Initialisation : intervalle de temps qui commence quand le contrôle est créé et se termine quand les éléments sont affichés à l’écran.
  • Défilement : possibilité de faire défiler la liste et de vérifier que l’interface utilisateur est synchrone avec les entrées tactiles.
  • Interaction pour l’ajout, la suppression et la sélection d’éléments.

Le contrôle ListView nécessite une application pour fournir des données et des modèles de cellule. Cette opération aura un impact important sur les performances du contrôle. Pour plus d’informations, consultez Données de cache.

Utiliser la programmation asynchrone

La réactivité globale de votre application peut être améliorée et les goulots d’étranglement des performances sont souvent évités à l’aide de la programmation asynchrone. Dans .NET, le Modèle asynchrone basé sur les tâches (TAP) est le modèle de conception recommandé pour les opérations asynchrones. Toutefois, une utilisation incorrecte du TAP peut entraîner des applications non performantes.

Notions de base

Les instructions générales suivantes doivent être suivies lors de l’utilisation du TAP :

  • Comprendre le cycle de vie des tâches, représenté par l’énumération TaskStatus. Pour plus d’informations, consultez La signification de TaskStatus et État de la tâche.
  • Utilisez la méthode Task.WhenAll pour attendre de façon asynchrone que plusieurs opérations asynchrones se terminent, plutôt que await individuellement une série d’opérations asynchrones. Pour en savoir plus, consultez Task.WhenAll.
  • Utilisez la méthode Task.WhenAny pour attendre de façon asynchrone la fin de l’une des opérations asynchrones. Pour en savoir plus, consultez Task.WhenAny.
  • Utilisez la méthode Task.Delay pour produire un objet Task qui se termine après l’heure spécifiée. Cela est utile pour les scénarios tels que l’interrogation des données et le retard de gestion des entrées utilisateur pendant un temps prédéterminé. Pour en savoir plus, consultez Task.Delay.
  • Exécutez des opérations intensives de processeur synchrone sur le pool de threads avec la méthode Task.Run. Cette méthode est un raccourci pour la méthode TaskFactory.StartNew, avec les arguments les plus optimaux définis. Pour en savoir plus, consultez Task.Run.
  • Évitez d’essayer de créer des constructeurs asynchrones. Utilisez plutôt des événements de cycle de vie ou une logique d’initialisation distincte pour await correctement toute initialisation. Pour plus d’informations, consultez Constructeurs asynchrones sur blog.stephencleary.com.
  • Utilisez le modèle de tâche différé pour éviter d’attendre la fin des opérations asynchrones pendant le démarrage de l’application. Pour plus d'informations, consultez AsyncLazy.
  • Créez un enveloppeur de tâches pour les opérations asynchrones existantes, qui n’utilisent pas le TAP, en créant des objets TaskCompletionSource<T>. Ces objets bénéficient des avantages de la programmabilité Task et vous permettent de contrôler la durée de vie et l’achèvement du Task associé. Pour plus d’informations, consultez La nature de TaskCompletionSource.
  • Retourne un objet Task, au lieu de renvoyer un objet Task attendu, lorsqu’il n’est pas nécessaire de traiter le résultat d’une opération asynchrone. Cela est plus performant en raison d’un basculement de contexte inférieur en cours d’exécution.
  • Utilisez la bibliothèque de flux de données TPL (Task Parallel Library) dans des scénarios tels que le traitement des données à mesure qu’elles sont disponibles ou lorsque vous avez plusieurs opérations qui doivent communiquer entre elles de manière asynchrone. Pour plus d’informations, consultez Flux de données (bibliothèque parallèle de tâches).

UI

Les instructions suivantes doivent être suivies lors de l’utilisation du TAP avec des contrôles d’interface utilisateur :

  • Appelez une version asynchrone d’une API, si elle est disponible. Cela permet de débloquer le thread d’interface utilisateur, ce qui permettra d’améliorer l’expérience de l’utilisateur avec l’application.

  • Mettez à jour les éléments de l’interface utilisateur avec des données provenant d’opérations asynchrones sur le thread d’interface utilisateur, afin d’éviter les exceptions levées. Toutefois, les mises à jour de la propriété ListView.ItemsSource sont automatiquement marshalées vers le thread d’interface utilisateur. Pour plus d’informations sur la détermination du code en cours d’exécution sur le thread d’interface utilisateur, consultez Créer un thread sur le thread d’interface utilisateur.

    Important

    Toutes les propriétés de contrôle mises à jour via la liaison de données sont automatiquement marshalées sur le thread d’interface utilisateur.

Gestion des erreurs

Les instructions de gestion des erreurs suivantes doivent être suivies lors de l’utilisation du TAP :

  • En savoir plus sur la gestion des exceptions asynchrones. Les exceptions non gérées levées par le code qui s’exécutent de manière asynchrone sont propagées vers le thread appelant, sauf dans certains scénarios. Pour plus d’informations, consultez Gestion des exceptions (bibliothèque parallèle de tâches).
  • Évitez de créer des méthodes async void et créez plutôt des méthodes async Task. Celles-ci facilitent la gestion des erreurs, la composabilité et la testabilité. L’exception à cette directive est des gestionnaires d’événements asynchrones, qui doivent retourner void. Pour plus d’informations, consultez Éviter Async Void.
  • Ne mélangez pas le blocage et le code asynchrone en appelant les méthodes Task.Wait, Task.Result ou GetAwaiter().GetResult, car elles peuvent entraîner un blocage. Toutefois, si cette directive doit être violée, l’approche recommandée consiste à appeler la méthode GetAwaiter().GetResult, car elle conserve les exceptions de tâche. Pour plus d’informations, consultez Async All the Way et Gestion des exceptions de tâche dans .NET 4.5.
  • Utilisez la méthode dans la mesure du possible pour créer du code ConfigureAwait sans contexte. Le code sans contexte offre de meilleures performances pour les applications mobiles et est une technique utile pour éviter l’interblocage lors de l’utilisation d’une base de code partiellement asynchrone. Pour plus d’informations, consultez Configurer le contexte.
  • Utilisez des tâches de continuation pour des fonctionnalités telles que la gestion des exceptions levées par l’opération asynchrone précédente et l’annulation d’une continuation avant son démarrage ou pendant son exécution. Pour plus d’informations, voir Chaînage des tâches à l’aide de tâches continues.
  • Utilisez une implémentation ICommand asynchrone lorsque des opérations asynchrones sont appelées à partir du ICommand. Cela garantit que toutes les exceptions de la logique de commande asynchrone peuvent être gérées. Pour plus d’informations, consultez Programmation asynchrone : Modèles pour les applications MVVM asynchrones : Commandes.

Retarder le coût de création d’objets

L’initialisation tardive peut être utilisée pour retarder la création d’un objet jusqu’à sa première utilisation. Cette technique est principalement utilisée pour améliorer les performances, éviter les calculs et réduire les besoins en mémoire.

Envisagez d’utiliser l’initialisation différée pour les objets coûteux à créer dans les scénarios suivants :

  • L’application peut ne pas utiliser l’objet.
  • D’autres opérations coûteuses doivent être terminées avant que l’objet ne puisse être créé.

La classe Lazy<T> est utilisée pour définir un type initialisé paresseux, comme illustré dans l’exemple suivant :

void ProcessData(bool dataRequired = false)
{
    Lazy<double> data = new Lazy<double>(() =>
    {
        return ParallelEnumerable.Range(0, 1000)
                     .Select(d => Compute(d))
                     .Aggregate((x, y) => x + y);
    });

    if (dataRequired)
    {
        if (data.Value > 90)
        {
            ...
        }
    }
}

double Compute(double x)
{
    ...
}

L’initialisation tardive se produit lors du premier accès à la propriété Lazy<T>.Value. Le type encapsulé est créé et retourné lors du premier accès, puis il est stocké en vue d’une utilisation ultérieure.

Pour plus d’informations sur l’initialisation tardive, consultez Initialisation tardive.

Libérer des ressources IDisposable

L’interface IDisposable fournit un mécanisme permettant de libérer des ressources. Elle fournit une méthode Dispose qui doit être implémentée pour libérer explicitement des ressources. IDisposable n’est pas un destructeur et doit être implémenté seulement dans les circonstances suivantes :

  • Lorsque la classe contient des ressources non managées. Les ressources non managées qui nécessitent le plus souvent d’être libérées sont les fichiers, les flux et les connexions réseau.
  • Lorsque la classe contient des ressources IDisposable managées

Les consommateurs de type peuvent ensuite appeler l’implémentation IDisposable.Dispose pour libérer des ressources lorsque l’instance n’est plus nécessaire. Il existe, pour cela, deux méthodes :

  • L’encapsulation de l’objet IDisposable dans une instruction using
  • L’encapsulation de l’appel à IDisposable.Dispose dans un bloc try/finally

Encapsuler l’objet IDisposable dans une instruction d’utilisation

L'exemple suivant montre comment envelopper un objet IDisposable dans une instruction using :

public void ReadText(string filename)
{
    string text;
    using (StreamReader reader = new StreamReader(filename))
    {
        text = reader.ReadToEnd();
    }
    ...
}

La classe StreamReader implémente IDisposable, et l’instruction using fournit une syntaxe qui appelle la méthode StreamReader.Dispose sur l’objet StreamReader avant qu’il ne devienne hors de portée. Dans le bloc using, l’objet StreamReader est en lecture seule et ne peut pas être réassigné. L’instruction using garantit également que la méthode Dispose sera appelée même si une exception se produit, car le compilateur implémente le langage intermédiaire pour un bloc try/finally.

Encapsuler l’appel à IDisposable.Dispose dans un bloc essayer/finalement

L'exemple suivant montre comment envelopper l’appel à IDisposable.Dispose dans un bloc try/finally :

public void ReadText(string filename)
{
    string text;
    StreamReader reader = null;
    try
    {
        reader = new StreamReader(filename);
        text = reader.ReadToEnd();
    }
    finally
    {
        if (reader != null)
            reader.Dispose();
    }
    ...
}

La classe StreamReader implémente IDisposable, et le bloc finally appelle la méthode StreamReader.Dispose pour libérer la ressource. Pour plus d’informations, consultez Interface IDisposable.

Désinscription des événements

Pour éviter les fuites de mémoire, vous devez vous désabonner des événements avant que l’objet d’abonné ne soit supprimé. Tant que vous ne vous êtes pas désabonné de l’événement, le délégué de cet événement situé dans l’objet de publication comporte une référence au délégué qui encapsule le gestionnaire d’événements de l’abonné. Tant que l’objet de publication contient cette référence, le nettoyage de la mémoire ne va pas récupérer la mémoire de l’objet d’abonné.

L'exemple suivant montre comment se désabonner d’un événement :

public class Publisher
{
    public event EventHandler MyEvent;

    public void OnMyEventFires()
    {
        if (MyEvent != null)
            MyEvent(this, EventArgs.Empty);
    }
}

public class Subscriber : IDisposable
{
    readonly Publisher _publisher;

    public Subscriber(Publisher publish)
    {
        _publisher = publish;
        _publisher.MyEvent += OnMyEventFires;
    }

    void OnMyEventFires(object sender, EventArgs e)
    {
        Debug.WriteLine("The publisher notified the subscriber of an event");
    }

    public void Dispose()
    {
        _publisher.MyEvent -= OnMyEventFires;
    }
}

La classe Subscriber se désabonne de l’événement dans sa méthode Dispose.

Les cycles de référence peuvent également se produire si vous utilisez des gestionnaires d’événements et la syntaxe lambda, car les expressions lambda peuvent référencer et maintenir des objets actifs. Par conséquent, une référence à la méthode anonyme peut être stockée dans un champ et utilisée pour se désabonner de l’événement, comme illustré dans l’exemple suivant :

public class Subscriber : IDisposable
{
    readonly Publisher _publisher;
    EventHandler _handler;

    public Subscriber(Publisher publish)
    {
        _publisher = publish;
        _handler = (sender, e) =>
        {
            Debug.WriteLine("The publisher notified the subscriber of an event");
        };
        _publisher.MyEvent += _handler;
    }

    public void Dispose()
    {
        _publisher.MyEvent -= _handler;
    }
}

Le champ _handler conserve la référence à la méthode anonyme, et est utilisé pour l’abonnement et le désabonnement aux événements.

Éviter les références circulaires fortes sur iOS et Mac Catalyst

Dans certaines situations, vous pouvez créer des cycles de références fortes qui peuvent empêcher le récupérateur de mémoire de récupérer la mémoire des objets. Par exemple, considérez le cas où une sous-classe dérivée de NSObject, telle qu’une classe qui hérite de UIView, est ajoutée à un conteneur dérivé de NSObject et est fortement référencée à partir de Objective-C, comme illustré dans l’exemple suivant :

class Container : UIView
{
    public void Poke()
    {
        // Call this method to poke this object
    }
}

class MyView : UIView
{
    Container _parent;

    public MyView(Container parent)
    {
        _parent = parent;
    }

    void PokeParent()
    {
        _parent.Poke();
    }
}

var container = new Container();
container.AddSubview(new MyView(container));

Lorsque ce code crée l’instance Container, l’objet C# aura une référence forte à un objet Objective-C. De même, l’instance MyView aura également une référence forte à un objet Objective-C.

De plus, l’appel à container.AddSubview augmente le nombre de références sur l’instance non managée de MyView. Lorsque cela se produit, le runtime .NET pour iOS crée une instance GCHandle pour conserver l’objet dans le code MyView managé actif, car il n’existe aucune garantie que les objets managés conservent une référence à celui-ci. Du point de vue du code managé, l’objet MyView serait récupéré après l’appel de AddSubview(UIView), s’il n’y avait pas GCHandle.

L’objet MyView non managé a un GCHandle qui pointe vers l’objet managé. Cela s’appelle un lien fort. L’objet managé contient une référence à l’instance de Container. À son tour, l’instance de Container a une référence managée à l’objet MyView.

Dans les cas où un objet contenu conserve un lien vers son conteneur, plusieurs options sont disponibles pour traiter la référence circulaire :

  • Évitez la référence circulaire en conservant une référence faible au conteneur.
  • Appelez Dispose sur les objets.
  • Rompez manuellement le cycle en affectant au lien vers le conteneur la valeur null.
  • Supprimez manuellement l’objet contenu du conteneur.

Utilisez des références faibles

Pour empêcher un cycle, vous pouvez, entre autres, utiliser une référence faible de l’enfant au parent. Par exemple, le code ci-dessus peut être illustré dans l’exemple suivant :

class Container : UIView
{
    public void Poke()
    {
        // Call this method to poke this object
    }
}

class MyView : UIView
{
    WeakReference<Container> _weakParent;

    public MyView(Container parent)
    {
        _weakParent = new WeakReference<Container>(parent);
    }

    void PokeParent()
    {
        if (weakParent.TryGetTarget (out var parent))
            parent.Poke();
    }
}

var container = new Container();
container.AddSubview(new MyView container));

Ici, l’objet contenu ne maintient pas le parent actif. Toutefois, le parent maintient l’enfant vivant par le biais de l’appel à container.AddSubView.

Cela se produit également dans les API iOS qui utilisent le modèle de délégué ou de source de données, où une classe homologue contient l’implémentation. Par exemple, lors de la définition de la propriété Delegate ou de DataSource dans la classe UITableView.

Dans le cas de classes créées uniquement pour l’implémentation d’un protocole, par exemple IUITableViewDataSource, au lieu de créer une sous-classe, vous pouvez implémenter simplement l’interface dans la classe et remplacer la méthode, puis affecter à la propriété DataSource la valeur this.

Supprimer des objets avec des références fortes

Si une référence forte existe et qu’il est difficile de supprimer la dépendance, utilisez une méthode Dispose pour effacer le pointeur parent.

Pour les conteneurs, remplacez la méthode Dispose pour supprimer les objets contenus, comme illustré dans l’exemple suivant :

class MyContainer : UIView
{
    public override void Dispose()
    {
        // Brute force, remove everything
        foreach (var view in Subviews)
        {
              view.RemoveFromSuperview();
        }
        base.Dispose();
    }
}

Pour un objet enfant qui conserve une référence forte à son parent, effacez la référence au parent dans l’implémentation de Dispose :

class MyChild : UIView
{
    MyContainer _container;

    public MyChild(MyContainer container)
    {
        _container = container;
    }

    public override void Dispose()
    {
        _container = null;
    }
}