Partager via


Améliorer les performances des applications

Les performances médiocres de l’application se présentent de nombreuses façons. Il peut rendre une application non réactive, provoquer un défilement lent et réduire la durée de vie de la batterie de l'appareil. Toutefois, l’optimisation des performances implique plus que d’implémenter du code efficace. L’expérience utilisateur des performances des applications doit également être prise en compte. Par exemple, s’assurer que les opérations s’exécutent sans empêcher l’utilisateur d’effectuer d’autres activités peut contribuer à améliorer l’expérience de l’utilisateur.

Il existe de nombreuses techniques pour améliorer les performances et la perception des applications .NET MAUI (Interface Utilisateur Multiplateforme .NET). 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 permettant de déterminer où les optimisations du code auront le plus grand effet 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 Windows, et avec PerfView sur Windows. Pour plus d’informations, consultez Profilage des Applications .NET MAUI.

Les meilleures pratiques suivantes sont recommandées lors du profilage d’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 divers appareils, car la prise de mesures de performances sur un appareil n’affiche pas toujours les caractéristiques de performances d’autres appareils. Toutefois, au minimum, le profilage doit être effectué sur un appareil dont la spécification prévue est la plus faible.
  • 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 rapide que l’utilisation d’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 facilement être défini de manière statique. Lier des données qui n'ont pas besoin d'être liées n'apporte aucun avantage, car les liaisons ne sont pas rentables. Par exemple, définir Button.Text = "Accept" a moins de surcharge que la liaison Button.Text à une propriété viewmodel string avec la valeur « Accept ».

Choisir la disposition correcte

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

<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 en utilisant des combinaisons d’autres dispositions, car cela entraîne des calculs de disposition inutiles en cours d’exécution. Par exemple, ne tentez pas de reproduire une disposition Grid à l’aide d’une combinaison d’éléments HorizontalStackLayout. L’exemple suivant 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 est inutile, car les calculs de disposition 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’image

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. Il est inutile de décoder une image haute résolution en mémoire lorsqu’elle sera réduite à une taille plus petite pour l’affichage. 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 affichée dans un affichage de liste doit probablement être une résolution inférieure à une image affichée 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 ce faire, créez le flux lors de la création de la page ou lorsque l’événement Page.Appearing se déclenche, puis supprimez le flux lorsque l’événement Page.Disappearing se déclenche.

Lors du téléchargement d’une image à afficher avec la méthode ImageSource.FromUri(Uri), assurez-vous que l’image téléchargée est mise en cache pendant une durée appropriée. Pour plus d'informations, voir mise en cache des images.

Réduire le nombre d’éléments d’une page

La réduction du nombre d’éléments d’une page accélère le rendu de la page. Il existe deux techniques principales pour y parvenir. 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 être visible à l’écran. Si un élément n’est pas visible, car il est masqué derrière d’autres éléments, supprimez l’élément ou définissez sa propriété IsVisible sur false. La définition de la propriété IsVisible sur un élément pour false conserve l’élément dans l’arborescence visuelle, mais l’exclut du rendu et des calculs de disposition.

La deuxième 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 d’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 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 Applications en stylisant avec XAML.

Réduire 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 Lier une application Android, Lier une application iOSet Lier une application Mac Catalyst.

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

Toutes les applications disposent d’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 initiale, l’écran de démarrage doit être utilisé pour informer l’utilisateur de la progression pendant la période d’activation, afin de rassurer que l’application n’est pas bloquée. Cette réassurance peut être une barre de progression ou un 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. La période d’activation peut être réduite en veillant à ce que les ressources requises soient empaquetées dans l’application, au lieu d’être récupérées à distance. Par exemple, dans certaines circonstances, il peut être approprié pendant la période d’activation de charger des données d’espace réservé stockées localement. Ensuite, une fois l’interface utilisateur initiale 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. Cela peut être utile pour retarder le chargement d'assemblages supplémentaires, car ces assemblages sont chargés la première fois qu'ils sont utilisés.

Choisir un conteneur d’injection de dépendances soigneusement

Les conteneurs d’injection de dépendances introduisent des contraintes de performances supplémentaires dans les applications mobiles. L'enregistrement et la résolution des types avec un conteneur ont un coût en termes de performance en raison de l'utilisation par le conteneur de la réflexion pour créer chaque type, surtout si les dépendances sont reconstruites à chaque navigation de page dans l'application. S’il existe de nombreuses dépendances ou profondes, le coût de la création peut augmenter considérablement. 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 l’injection de dépendances.

Alternativement, l’injection de dépendances peut être optimisée en l’implémentant manuellement à l’aide d’usines.

Créer des applications Shell

Les applications .NET MAUI Shell offrent une expérience de navigation structurée basée sur les panneaux 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 expérience de démarrage médiocre, 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 plus d’informations, consultez Vue d’ensemble de Shell.

Optimiser les performances de ListView

Lorsque vous utilisez ListView, il existe un certain nombre d’expériences utilisateur qui doivent être optimisées :

  • initialisation : l'intervalle de temps qui commence lors de la création du contrôle et se termine lorsque les éléments sont affichés à l'écran.
  • défilement : possibilité de faire défiler la liste et de s’assurer que l’interface utilisateur ne retarde pas les mouvements tactiles.
  • interaction pour ajouter, supprimer et sélectionner des éléments.

Le contrôle ListView nécessite une application pour fournir des données et des modèles de cellule. La manière dont cela est réalisé aura un impact considérable sur les performances du contrôle. Pour plus d’informations, consultez les données en 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 des 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.

Principes 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 tâche.
  • Utilisez la méthode Task.WhenAll pour attendre de façon asynchrone la fin de plusieurs opérations asynchrones, plutôt que d'await individuellement une série d’opérations asynchrones. Pour plus d’informations, consultez Task.WhenAll.
  • Utilisez la méthode Task.WhenAny pour attendre asynchrone l'achèvement de l'une des nombreuses opérations asynchrones. Pour plus d’informations, 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 des scénarios tels que l’interrogation de données et le différé du traitement des entrées utilisateur pendant un temps prédéterminé. Pour plus d’informations, 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 plus d’informations, 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 wrapper de tâches pour les opérations asynchrones existantes, qui n’utilisent pas le TAP, en créant des objets TaskCompletionSource<T>. Ces objets tirent profit de la programmabilité Task et vous permettent de contrôler la durée de vie et l'achèvement de l’élément Taskassocié. 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 de moins de basculements de contexte effectués.
  • 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 dataflow (bibliothèque parallèle de tâches).

interface utilisateur

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 apportées à 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 transférées vers le fil d’exécution de l’interface utilisateur.

Gestion des erreurs

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

  • Découvrez la gestion asynchrone des exceptions. Les exceptions non gérées levées par le code qui s'exécute 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 concerne les gestionnaires d’événements asynchrones, qui doivent renvoyer void. Pour plus d’informations, consultez Avoid Async Void.
  • Ne mélangez pas de code bloquant et asynchrone en appelant le Task.Wait, Task.Resultou les méthodes 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 ConfigureAwait dans la mesure du possible pour créer du code 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 les tâches de continuation pour des fonctionnalités comme gérer les exceptions levées par l’opération asynchrone précédente et annuler une continuation avant son démarrage ou pendant son exécution. Pour plus d’informations, consultez Chaînage de tâches à l'aide de tâches continues.
  • Utilisez une implémentation de 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 différée peut être utilisée pour différer la création d’un objet jusqu’à ce qu’il soit utilisé pour la première fois. Cette technique est principalement utilisée pour améliorer les performances, éviter le calcul 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 effectuées avant la création de l’objet.

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 différée se produit la première fois que la propriété Lazy<T>.Value est appelée. Le type encapsulé est créé et retourné lors du premier accès et stocké pour tout accès ultérieur.

Pour plus d’informations sur l’initialisation différée, consultez initialisation différée.

Libérer des ressources IDisposables

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

  • Lorsque la classe possède des ressources non managées. Les ressources non managées classiques qui nécessitent la publication incluent des fichiers, des flux et des connexions réseau.
  • Lorsque la classe possède des ressources de IDisposable managées.

Les consommateurs de types peuvent ensuite appeler l’implémentation IDisposable.Dispose pour libérer des ressources lorsque l’instance n’est plus nécessaire. Il existe deux approches pour y parvenir :

  • En encapsulant l’objet IDisposable dans une instruction using.
  • En encapsulant l’appel à IDisposable.Dispose dans un bloc try/finally.

Encapsuler l’objet IDisposable dans une instruction using

L’exemple suivant montre comment encapsuler 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 pratique qui appelle la méthode StreamReader.Dispose sur l’objet StreamReader avant de sortir de portée. Dans le bloc using, l’objet StreamReader est en lecture seule et ne peut pas être réaffecté. L’instruction using garantit également que la méthode Dispose est appelée même si une exception se produit, car le compilateur implémente le langage intermédiaire (IL) pour un bloc try/finally.

Encapsuler l’appel à IDisposable.Dispose dans un bloc try/finally

L’exemple suivant montre comment encapsuler 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.

Se désabonner des événements

Pour éviter les fuites de mémoire, les événements doivent être désinscrits avant la suppression de l’objet abonné. Tant que l’événement n’est pas annulé, le délégué de l’événement dans l’objet de publication a 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, la collecte des ordures ne libère pas la mémoire de l’objet 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;
    }
}

Dans sa méthode Dispose, la classe Subscriber se désabonne de l’événement.

Les cycles de référence peuvent également se produire lors de l’utilisation de gestionnaires d’événements et de syntaxe lambda, car les expressions lambda peuvent référencer et maintenir les 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 aux événements et se désabonner.

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

Dans certaines situations, il est possible de créer des cycles de référence forts qui pourraient empêcher les objets d’avoir leur mémoire récupérée par le garbage collector. 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 NSObjectet 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.

En outre, 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 de GCHandle pour conserver l’objet MyView dans le code 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 AddSubview(UIView) si ce n'était pas à cause de GCHandle.

L’objet MyView non managé aura un GCHandle pointant vers l’objet managé, appelé lien fort. L’objet managé contient une référence à l’instance de Container. À son tour, l’instance Container aura une référence managée à l’objet MyView.

Dans les circonstances où un objet contenu conserve un lien vers son conteneur, il existe plusieurs options 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.
  • Arrêtez manuellement le cycle en définissant le lien vers le conteneur sur null.
  • Supprimez manuellement l’objet contenu du conteneur.

Utiliser des références faibles

Une façon d’empêcher un cycle consiste à 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 maintiendra pas le parent en vie. Toutefois, le parent maintient l’enfant en vie 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 la DataSource dans la classe UITableView.

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

Supprimer des objets avec des références fortes

S'il existe une référence forte et qu'il est difficile de supprimer la dépendance, utilisez la 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 Dispose :

class MyChild : UIView
{
    MyContainer _container;

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

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