Compartir a través de


Mejora del rendimiento de las aplicaciones

El rendimiento deficiente de una aplicación se manifiesta de muchas formas. Puede hacer que una aplicación parezca que deja de responder, puede ocasionar un desplazamiento lento y puede reducir la duración de la batería del dispositivo. La optimización del rendimiento conlleva mucho más que la mera implementación de código eficaz. También debe tenerse en cuenta la experiencia del usuario del rendimiento de la aplicación. Por ejemplo, asegurarse de que las operaciones se ejecuten sin evitar que el usuario realice otras actividades puede ayudar a mejorar su experiencia.

Hay muchas técnicas para aumentar el rendimiento, y el rendimiento percibido, de aplicaciones .NET Multi-platform App UI (.NET MAUI). En conjunto, estas técnicas pueden reducir considerablemente la cantidad de trabajo que está realizando una CPU y la cantidad de memoria consumida por una aplicación.

Uso de un generador de perfiles

Al desarrollar una aplicación, es importante intentar optimizar el código únicamente cuando ya se ha generado un perfil. La generación de perfiles es una técnica para determinar dónde tendrán más efecto las optimizaciones de código a la hora de reducir problemas de rendimiento. El generador de perfiles realiza el seguimiento del consumo de memoria de la aplicación y registra el tiempo de ejecución de los métodos de la aplicación. Estos datos ayudan a navegar por las rutas de ejecución de la aplicación y el coste de ejecución del código, por lo que permiten detectar las mejores oportunidades de optimización.

Se pueden generar perfiles de las aplicaciones de .NET MAUI mediante dotnet-trace en Android, iOS y Mac, y con PerfView en Windows. Para obtener más información, consulta Generación de perfiles de aplicaciones .NET MAUI.

Al generar perfiles de una aplicación, estos son los procedimientos recomendados:

  • Evita la generación de perfiles de una aplicación en un simulador, ya que este puede distorsionar el rendimiento de la aplicación.
  • Lo ideal sería generar perfiles en varios dispositivos, ya que la toma de medidas de rendimiento en un dispositivo no siempre muestra las características de rendimiento de otros dispositivos. Como mínimo, la generación de perfiles debe realizarse en un dispositivo con la menor especificación prevista.
  • Cierra las demás aplicaciones para garantizar la medición del impacto total de la aplicación cuyo perfil se está generando, y no del de las otras.

Uso de enlaces compilados

Los enlaces compilados mejoran el rendimiento del enlace de datos en las aplicaciones de .NET MAUI mediante la resolución de expresiones de enlace en tiempo de compilación, en lugar de en tiempo de ejecución con reflexión. La compilación de una expresión de enlace genera código compilado que normalmente resuelve un enlace entre 8 y 20 veces más rápido que usando un enlace clásico. Para obtener más información, consulta Enlaces compilados.

Reducción de enlaces innecesarios

No use enlaces para el contenido que se puede establecer fácilmente de forma estática. No hay ninguna ventaja en enlazar datos que no necesitan ser enlazados, ya que los enlaces no son rentables. Por ejemplo, establecer Button.Text = "Accept" tiene una menor sobrecarga que enlazar Button.Text a una propiedad string del modelo de vista con el valor "Accept".

Elección del diseño adecuado

Un diseño capaz de mostrar varios elementos secundarios, pero que solo tiene un elemento secundario, es poco rentable. Por ejemplo, el siguiente ejemplo muestra un VerticalStackLayout con un único elemento secundario:

<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>

Esto es excesivo y el elemento VerticalStackLayout debería eliminarse, como se muestra en el ejemplo siguiente:

<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>

Además, no intente reproducir el aspecto de un diseño específico mediante combinaciones de otros diseños, dado que como resultado se realizarían cálculos de diseño innecesarios. Por ejemplo, no intentes reproducir un diseño Grid mediante una combinación de elementos HorizontalStackLayout. En el ejemplo de código siguiente se muestra un ejemplo de esta práctica incorrecta:

<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>

Es una pérdida de tiempo porque se realizan cálculos de diseño innecesarios. En su lugar, el diseño deseado puede lograrse mejor mediante un Grid, como se muestra en el ejemplo siguiente:

<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>

Optimizar los recursos de imagen

Las imágenes son uno de los recursos con más consumo usados por las aplicaciones y se suelen capturar en altas resoluciones. Aunque esto crea imágenes vibrantes llenas de detalles, las aplicaciones que muestran dichas imágenes normalmente necesitan usar más CPU para descodificar la imagen y más memoria para almacenar la imagen descodificada. Es un desperdicio descodificar una imagen de alta resolución en memoria cuando se reducirá a un tamaño menor para su presentación. En su lugar, reduce el uso de CPU y la superficie de memoria mediante la creación de versiones de imágenes almacenadas que se aproximen a los tamaños de presentación previstos. Por ejemplo, una imagen que aparece en una vista de lista probablemente deba tener menos resolución que una imagen que aparece en pantalla completa.

Por tanto, solo se deberían crear imágenes cuando sea necesario y deberían liberarse en cuanto la aplicación ya no las necesite. Por ejemplo, si una aplicación muestra una imagen mediante la lectura de sus datos desde una secuencia, asegúrate de que esa secuencia se crea solo cuando sea necesario y que se libera cuando ya no es necesaria. Esto se consigue mediante la creación de la secuencia cuando se crea la página, o cuando se desencadena el evento Page.Appearing y, después, mediante la eliminación de la secuencia cuando se desencadena el evento Page.Disappearing.

Al descargar una imagen para mostrar con el método ImageSource.FromUri(Uri), asegúrate de que la imagen descargada se almacena en caché durante un período de tiempo adecuado. Para obtener más información, consulta Almacenamiento en caché de imágenes.

Reducción del tamaño del árbol visual

Reducir el número de elementos en una página hará que la página se procese más rápido. Hay dos técnicas principales para conseguir esto. La primera es ocultar los elementos que no están visibles. La propiedad IsVisible de cada elemento determina si el elemento debe ser parte del árbol visual o no. Por tanto, si un elemento no es visible porque está oculto detrás de otros elementos, quite el elemento o establezca su propiedad IsVisible en false.

La segunda técnica es quitar los elementos innecesarios. Por ejemplo, a continuación se muestra un diseño de página que contiene varios elementos 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>

Se puede mantener el mismo diseño de página con un número reducido de elementos, como se muestra en el ejemplo siguiente:

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

Reducción del tamaño del diccionario de recursos de aplicación

Todos los recursos que se usan en la aplicación se deben almacenar en el diccionario de recursos de la aplicación para evitar la duplicación. Esto ayuda a reducir la cantidad de código XAML que tiene que analizarse en la aplicación. El siguiente ejemplo muestra el recurso HeadingLabelStyle, que se usa en toda la aplicación, por lo que se define en el diccionario de recursos de la aplicación:

<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>

Pero el código XAML que es específico de una página no debería incluirse en el diccionario de recursos de la aplicación, dado que los recursos se analizarán en el inicio de la aplicación en lugar de cuando los solicite una página. Si una página que no es la página de inicio usa un recurso, se debe colocar en el diccionario de recursos de esa página, para así reducir el código XAML que se analiza cuando se inicia la aplicación. El siguiente ejemplo muestra el recurso HeadingLabelStyle, que solo se usa en una página, por lo que se define en el diccionario de recursos de la página:

<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>

Para más información sobre los recursos de aplicación, consulta Aplicaciones de estilo con de XAML.

Reducir el tamaño de la aplicación

Cuando .NET MAUI compila la aplicación, se puede usar un enlazador denominado ILLink para reducir el tamaño general de la aplicación. ILLink reduce el tamaño mediante el análisis del código intermedio generado por el compilador. Elimina métodos, propiedades, campos, eventos, estructuras y clases sin usar para generar una aplicación que contenga solo las dependencias de código y ensamblado necesarias para ejecutar la aplicación.

Para más información sobre cómo configurar el comportamiento del enlazador, consulta Vinculación de una aplicación de Android, Vinculación de una aplicación de iOS y Vinculación de una aplicación de Mac Catalyst.

Reducir el período de activación de la aplicación

Todas las aplicaciones tienen un período de activación, que es el tiempo entre su inicio y el momento en que están listas para su uso. Este período de activación proporciona a los usuarios la primera impresión de la aplicación, por lo que es importante reducirlo, así como la percepción que los usuarios tienen de él, para que tengan una primera impresión favorable de la aplicación.

Antes de que una aplicación muestre su interfaz de usuario inicial, debe proporcionar una pantalla de presentación para indicar al usuario que se está iniciando. Si la aplicación no puede mostrar rápidamente su interfaz de usuario inicial, se debe usar la pantalla de presentación para informar al usuario del progreso durante el período de activación, a fin de ofrecer la seguridad de que la aplicación no se ha bloqueado. Podría hacerse mediante una barra de progreso o un control similar.

Durante el período de activación, las aplicaciones ejecutan lógica de activación, que suele incluir la carga y el procesamiento de los recursos. El período de activación se puede reducir asegurándose de que los recursos necesarios estén empaquetados en la aplicación en lugar de recuperarse de forma remota. Por ejemplo, en algunas circunstancias puede ser adecuado durante el período de activación cargar datos de marcador de posición almacenados localmente. Luego, una vez que aparece la interfaz de usuario inicial y el usuario puede interactuar con la aplicación, se pueden reemplazar los datos de marcador de posición progresivamente desde un origen remoto. Además, la lógica de activación de la aplicación solo debe realizar el trabajo necesario para que el usuario pueda empezar a usar la aplicación. Esto puede ayudar si retrasa la carga de ensamblados adicionales, ya que los ensamblados se cargan la primera vez que se usan.

Elección cuidadosa de un contenedor de inyección de dependencia

Los contenedores de inyección de dependencia proporcionan restricciones de rendimiento adicionales a las aplicaciones móviles. El registro y la resolución de tipos con un contenedor supone un costo de rendimiento, que los contenedores usan reflexión para crear todos los tipos, especialmente si se reconstruyen las dependencias para cada navegación de página en la aplicación. Si hay muchas dependencias, o estas son muy amplias, el costo de la creación puede aumentar significativamente. Además, el registro de tipos, que normalmente se produce durante el inicio de la aplicación, puede afectar notablemente al tiempo de inicio, según el contenedor que se esté usando. Para obtener más información sobre la inserción de dependencias en aplicaciones MAUI de .NET, vea Inserción de dependencias.

Como alternativa, la inyección de dependencia puede optimizarse implementándola manualmente mediante factorías.

Creación de aplicaciones Shell

Las aplicaciones .NET MAUI de Shell proporcionan una experiencia de navegación bien fundamentada basada en controles flotantes y pestañas. Si la experiencia de usuario de la aplicación se puede implementar con Shell, es recomendable hacerlo. Las aplicaciones Shell ayudan a evitar una experiencia de inicio deficiente, ya que las páginas se crean bajo demanda en respuesta a la navegación, en lugar de durante el inicio de la aplicación. Esto sucede con las aplicaciones que usan una TabbedPage. Para más información, consulta la Introducción a Shell.

Optimización del rendimiento de ListView

Al usar ListView hay una serie de experiencias de usuario que deben optimizarse:

  • Inicialización: el intervalo de tiempo que comienza cuando se crea el control y que termina cuando los elementos se muestran en pantalla.
  • Desplazamiento: la capacidad de desplazarse por la lista y garantizar que la interfaz de usuario no se retrasa con los gestos de toque.
  • Interacción para agregar, eliminar y seleccionar elementos.

El control ListView requiere una aplicación para suministrar datos y plantillas de celda. La forma de conseguirlo tendrá un gran impacto en el rendimiento del control. Para obtener más información, consulta Datos de caché.

uso de la programación asincrónica

La capacidad de respuesta general de la aplicación se puede mejorar y, normalmente, evitar los cuellos de botella de rendimiento mediante la programación asincrónica. En .NET, el Modelo asincrónico basado en tareas (TAP) es el modelo de diseño recomendado para las operaciones asincrónicas. Pero el uso incorrecto de TAP puede dar lugar a aplicaciones no ejecutables.

Aspectos básicos

Por tanto, al usar TAP, se deben seguir las instrucciones generales siguientes:

  • Entender el ciclo de vida de la tarea, que se representa mediante la enumeración TaskStatus. Para más información, vea El significado de TaskStatus y Estado de la tarea.
  • Use el método Task.WhenAll para esperar de forma asincrónica a que finalicen varias operaciones asincrónicas, en lugar de utilizar await de manera individual en una serie de operaciones asincrónicas. Para más información, vea Task.WhenAll.
  • Use el método Task.WhenAny para esperar de forma asincrónica a que finalice una de varias operaciones asincrónicas. Para más información, vea Task.WhenAny.
  • Use el método Task.Delay para crear un objeto Task que finaliza tras el tiempo especificado. Esto resulta útil para escenarios como los de sondeo de datos y para retrasar el control de la entrada del usuario durante un tiempo predeterminado. Para más información, vea Task.Delay.
  • Ejecute operaciones de CPU sincrónicas intensivas en el grupo de subprocesos con el método Task.Run. Este método es un acceso directo para el método TaskFactory.StartNew, con los argumentos óptimos establecidos. Para más información, vea Task.Run.
  • Evite intentar crear constructores asincrónicos. En su lugar, use eventos de ciclo de vida o lógica de inicialización independiente para ejecutar await de forma correcta en cualquier inicialización. Para más información, vea Constructores asincrónicos en blog.stephencleary.com.
  • Usa el patrón de tareas en diferido para evitar esperar a que se completen las operaciones asincrónicas durante el inicio de la aplicación. Para más información, vea AsyncLazy.
  • Cree un contenedor de tareas para las operaciones asincrónicas existentes, que no usan TAP, mediante la creación de objetos TaskCompletionSource<T>. Estos objetos obtienen las ventajas de la programación de Task y permiten controlar la duración y la finalización del objeto Task asociado. Para más información, vea La naturaleza de TaskCompletionSource.
  • Devuelva un objeto Task, en lugar de devolver un objeto Task en espera, cuando no sea necesario procesar el resultado de una operación asincrónica. Esto es más eficaz debido a que se realizan menos cambios de contexto.
  • Use la biblioteca Flujo de datos de la biblioteca de procesamiento paralelo basado en tareas (TPL) en escenarios como el procesamiento de datos a medida que estén disponibles, o bien cuando tenga varias operaciones que se deban comunicar entre sí de forma asincrónica. Para más información, vea Flujo de datos (biblioteca TPL).

UI

Por tanto, al usar TAP, se deben seguir las instrucciones generales siguientes:

  • Llame a una versión asincrónica de una API, si está disponible. Esto mantendrá desbloqueado al subproceso de IU, lo que te ayudará a mejorar la experiencia del usuario con la aplicación.

  • Actualice los elementos de la interfaz de usuario con los datos de las operaciones asincrónicas en el subproceso de la interfaz de usuario, para evitar que se inicien excepciones. Pero las actualizaciones de la propiedad ListView.ItemsSource se calcularán de forma automática en el subproceso de la interfaz de usuario. Para obtener información sobre cómo determinar si el código se ejecuta en el subproceso de la interfaz de usuario, ve Crear un subproceso en el subproceso de interfaz de usuario. :

    Importante

    Todas las propiedades de control que se actualicen a través del enlace de datos se serializarán de forma automática en el subproceso de la interfaz de usuario.

Control de errores

Se deben seguir las instrucciones de control de errores siguientes al usar TAP:

  • Obtenga información sobre el control de excepciones asincrónico. Las excepciones no controladas que se inician desde código que se ejecuta de forma asincrónica se propagan de vuelta al subproceso que realiza la llamada, excepto en escenarios concretos. Para más información, vea Control de excepciones (biblioteca TPL).
  • Evite crear métodos async void y, en su lugar, cree métodos async Task. Estos facilitan el control de errores, la redacción y la capacidad de prueba. La excepción a esta instrucción son los controladores de eventos asincrónicos, que deben devolver void. Para más información, vea Evite async void.
  • No mezcle código de bloqueo y asincrónico mediante una llamada a los métodos Task.Wait, Task.Result o GetAwaiter().GetResult, ya que pueden dar lugar a un interbloqueo. Pero si se debe incumplir esta instrucción, el enfoque preferido consiste en llamar al método GetAwaiter().GetResult porque conserva las excepciones de la tarea. Para más información, vea Async hasta el infinito y Control de excepciones de tareas en .NET 4.5.
  • Use el método ConfigureAwait siempre que sea posible para crear código sin contexto. El código sin contexto tiene un mejor rendimiento en las aplicaciones para dispositivos móviles y es una técnica útil para evitar los interbloqueos cuando se trabaja con una base de código parcialmente asincrónica. Para más información, vea Configuración del contexto.
  • Use tareas de continuación para obtener funcionalidad como el control de excepciones iniciadas por la operación asincrónica anterior y la cancelación de una continuación antes de iniciarse o mientras se ejecuta. Para más información, vea Encadenamiento de tareas mediante tareas de continuación.
  • Use una implementación de ICommand asincrónica cuando se invoquen operaciones asincrónicas desde ICommand. Esto garantiza que se puedan controlar las excepciones de la lógica de comandos asincrónica. Para obtener más información, consulta Patrones para aplicaciones MVVM asincrónicas: comandos.

Retrasar el costo de crear objetos

Se puede usar la inicialización diferida para aplazar la creación de un objeto hasta que se use por primera vez. Esta técnica se usa principalmente para mejorar el rendimiento, evitar el cálculo y reducir los requisitos de memoria.

Considera el uso de la inicialización diferida para aquellos objetos que son caros de crear en los escenarios siguientes:

  • Es posible que la aplicación no use el objeto.
  • Antes de crear el objeto, deben completarse otras operaciones que consumen muchos recursos.

La clase Lazy<T> se usa para definir un tipo con inicialización diferida, como se muestra en el ejemplo siguiente:

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)
{
    ...
}

La inicialización diferida se produce la primera vez que se accede a la propiedad Lazy<T>.Value. El tipo encapsulado se crea y se devuelve en el primer acceso y se almacena para cualquier acceso futuro.

Para más información sobre la inicialización diferida, vea Lazy Initialization (Inicialización diferida).

Liberar recursos IDisposable

La interfaz IDisposable proporciona un mecanismo para liberar recursos. Proporciona un método Dispose que debe implementarse para liberar recursos de forma explícita. IDisposable no es un destructor y solo se debe implementar en los siguientes casos:

  • Si la clase tiene recursos no administrados. Los recursos típicos no administrados que exigen liberación incluyen los archivos, las secuencias y las conexiones de red.
  • Si la clase tiene recursos IDisposable administrados.

Los consumidores del tipo pueden llamar a la implementación IDisposable.Dispose para liberar recursos cuando ya no se necesite la instancia. Existen dos enfoques para conseguirlo:

  • Ajustar el objeto IDisposable en una instrucción using.
  • Ajustar la llamada a IDisposable.Dispose en un bloque try/finally.

Ajuste del objeto IDisposable en una instrucción using

En el ejemplo siguiente se muestra cómo encapsular un objeto IDisposable en una instrucción using:

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

La clase StreamReader implementa IDisposable y la instrucción using proporciona una sintaxis adecuada que llama al método StreamReader.Dispose en el objeto StreamReader antes de quede fuera del ámbito. Dentro del bloque using, el objeto StreamReader es de solo lectura y no se puede reasignar. La instrucción using también garantiza que se llame al método Dispose aun cuando se produzca una excepción, ya que el compilador implementa el lenguaje intermedio (IL) para un bloque try/finally.

Ajuste de la llamada a IDisposable.Dispose en un bloque try/finally

En el ejemplo siguiente se muestra cómo ajustar la llamada IDisposable.Dispose en un bloque 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 clase StreamReader implementa IDisposable y el bloque finally llama al método StreamReader.Dispose para liberar el recurso. Para más información, vea Interfaz IDisposable.

Cancelación de la suscripción de eventos

Para evitar fugas de memoria, se debería cancelar la suscripción a eventos antes de que se elimine el objeto de suscriptor. Hasta que se cancela la suscripción al evento, el delegado del evento en el objeto de publicación tiene una referencia al delegado que encapsula el controlador de eventos del suscriptor. Mientras el objeto de publicación contenga esta referencia, la recolección de elementos no utilizados no recuperará la memoria del objeto de suscriptor.

En el ejemplo siguiente se muestra cómo cancelar la suscripción de un evento:

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 clase Subscriber cancela la suscripción al evento en su método Dispose.

También se pueden producir ciclos de referencia al usar controladores de eventos y sintaxis lambda, ya que las expresiones lambda pueden hacer referencia a objetos activos y conservarlos. Por lo tanto, se puede almacenar una referencia al método anónimo en un campo y usarla para cancelar la suscripción al evento, como se muestra en el ejemplo siguiente:

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

El campo _handler mantiene la referencia al método anónimo y se usa para la suscripción a eventos y su cancelación.

Evitar referencias circulares fuertes en iOS y Mac Catalyst

En algunas situaciones es posible crear referencias circulares seguras que podrían impedir que el recolector de elementos no utilizados reclame memoria de los objetos. Por ejemplo, considera el caso en el que una subclase derivada de NSObject, como una clase que hereda de UIView, se agrega a un contenedor derivado de NSObject y se hace referencia firmemente a esta desde Objective-C, como se muestra en el ejemplo siguiente:

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

Cuando este código cree la instancia Container, el objeto de C# tendrá una referencia fuerte a un objeto de Objective-C. De forma similar, la instancia MyView también tendrá una referencia fuerte a un objeto de Objective-C.

Además, la llamada a container.AddSubview aumentará el recuento de referencias en la instancia MyView no administrada. Cuando esto sucede, el entorno de ejecución de .NET para iOS crea una instancia de GCHandle para mantener activo el objeto MyView en código administrado, ya que no hay ninguna garantía de que los objetos administrados conserven una referencia a él. Desde una perspectiva de código administrado, el objeto MyView se reclamaría tras la llamada a AddSubview(UIView) si no fuera por el GCHandle.

El objeto MyView no administrado tendrá un GCHandle que apunta al objeto administrado, conocido como vínculo fuerte. El objeto administrado contendrá una referencia a la instancia Container. A su vez, la instancia Container tendrá una referencia administrada al objeto MyView.

En casos donde un objeto contenido mantiene un vínculo a su contenedor, hay varias opciones disponibles para solucionar la referencia circular:

  • Evitar la referencia circular manteniendo una referencia débil al contenedor.
  • Llamar a Dispose en los objetos.
  • Interrumpir manualmente el ciclo estableciendo el vínculo al contenedor en null.
  • Quitar manualmente el objeto contenido del contenedor.

Usar referencias débiles

Una manera de evitar un ciclo es usar una referencia débil del elemento secundario al primario. Por ejemplo, el código anterior podría ser como se muestra en el ejemplo siguiente:

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

Aquí, el objeto contenido no mantiene activo el elemento primario. Pero el elemento primario mantiene activo el elemento secundario durante la llamada realizada a container.AddSubView.

Esto también ocurre en las API de iOS que usan el patrón de delegado o de origen de datos, donde una clase del mismo nivel contiene la implementación; Por ejemplo, al establecer la propiedad Delegate o DataSource en la clase UITableView.

En el caso de las clases que se crean exclusivamente para la implementación de un protocolo, por ejemplo, IUITableViewDataSource, en lugar de crear una subclase, puede implementar la interfaz de la clase, invalidar el método y asignar la propiedad DataSource a this.

Eliminación de objetos con referencias fuertes

Si existe una referencia fuerte y la dependencia es difícil de quitar, haga que un método Dispose borre el puntero primario.

Para los contenedores, invalida el método Dispose para quitar los objetos contenidos, como se muestra en el ejemplo de siguiente:

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

Para un objeto secundario que mantiene una referencia fuerte a su elemento primario, borre la referencia al elemento primario en la implementación de Dispose:

class MyChild : UIView
{
    MyContainer _container;

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

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