Prevención de fugas de memoria
Al usar controles Win2D en aplicaciones XAML administradas, se debe tener cuidado para evitar ciclos de recuento de referencias que podrían impedir que el recolector de elementos no utilizados recupere estos controles.
Tienes un problema si…
- Estás usando Win2D desde un lenguaje .NET, como C# (no C++nativo)
- Usa uno de los controles XAML de Win2D:
- Se suscribe a eventos del control Win2D (por ejemplo
Draw
,CreateResources
,SizeChanged
...) - La aplicación se mueve hacia atrás y hacia delante entre más de una página XAML
Si se cumplen todas estas condiciones, un ciclo de recuento de referencias mantendrá el control Win2D de la recolección de elementos no utilizados. Los nuevos recursos win2D se asignan cada vez que la aplicación se mueve a una página diferente, pero las antiguas nunca se liberan, por lo que se pierde memoria. Para evitar esto, debes agregar el código para interrumpir explícitamente el ciclo.
Cómo solucionarlo
Para interrumpir el ciclo de recuento de referencias y permitir que la página se recopile como elementos no utilizados:
- Enlazar el evento
Unloaded
de la página XAML que contiene el control Win2D - En el controlador
Unloaded
, llama alRemoveFromVisualTree
en el control Win2D. - En el controlador
Unloaded
, libere (al configurar ennull
) las referencias explícitas al control Win2D.
Código de ejemplo:
void page_Unloaded(object sender, RoutedEventArgs e)
{
this.canvas.RemoveFromVisualTree();
this.canvas = null;
}
Para ver ejemplos de trabajo, consulta cualquiera de las páginas de demostración de la Galería de ejemplos.
Cómo probar las pérdidas de ciclo
Para probar si la aplicación está interrumpiendo correctamente los ciclos de recuento de referencias, agrega un método de finalización a las páginas XAML que contengan controles Win2D:
~MyPage()
{
System.Diagnostics.Debug.WriteLine("~" + GetType().Name);
}
En el constructor App
, configura un temporizador que garantice que la recolección de elementos no utilizados se produzca a intervalos regulares:
var gcTimer = new DispatcherTimer();
gcTimer.Tick += (sender, e) => { GC.Collect(); };
gcTimer.Interval = TimeSpan.FromSeconds(1);
gcTimer.Start();
Dirígete a la página y, a continuación, sal de ella dirigiéndote a otra página. Si se han interrumpido todos los ciclos, verás la salida Debug.WriteLine
en el panel de salida de Visual Studio dentro de un segundo o dos.
Tenga en cuenta que las llamadas GC.Collect
son disruptivas y perjudican el rendimiento, por lo que debe eliminar este código de prueba tan pronto como termine de probar si hay fugas.
Detalles del gory
Un ciclo se produce cuando un objeto A tiene una referencia a B, al mismo tiempo que B también tiene una referencia a A. También sucede cuando A hace referencia a B y B hace referencia a C, mientras que C hace referencia a A, etc.
Al suscribirse a eventos de un control XAML, este tipo de ciclo es bastante inevitable:
- La página XAML contiene referencias a todos los controles incluidos en ella.
- Los controles contienen referencias a los delegados del controlador que se han suscrito a sus eventos.
- Cada delegado contiene una referencia a su instancia de destino.
- Los controladores de eventos suelen ser métodos de instancia de la clase de página XAML, por lo que sus referencias de instancia de destino apuntan a la página XAML, creando un ciclo.
Si todos los objetos implicados se implementan en .NET, estos ciclos no son un problema porque .NET es recolector de elementos no utilizados y el algoritmo de recolección de elementos no utilizados puede identificar y reclamar grupos de objetos, incluso si están vinculados en un ciclo.
A diferencia de .NET, C++ administra la memoria mediante el recuento de referencias, que no puede detectar y reclamar ciclos de objetos. A pesar de esta limitación, las aplicaciones de C++ que usan Win2D no tienen ningún problema porque los controladores de eventos de C++ tienen como valor predeterminado mantener referencias débiles en lugar de referencias seguras a su instancia de destino. Por lo tanto, la página hace referencia al control y el control hace referencia al delegado del controlador de eventos, pero este delegado no hace referencia a la página para que no haya ningún ciclo.
El problema se produce cuando una aplicación .NET usa un componente de WinRT de C++ como Win2D:
- La página XAML forma parte de la aplicación, por lo que usa la recolección de elementos no utilizados.
- El control Win2D se implementa en C++, por lo que usa el recuento de referencias.
- El delegado del controlador de eventos forma parte de la aplicación, por lo que usa la recolección de elementos no utilizados y contiene una referencia segura a su instancia de destino.
Hay un ciclo presente, pero los objetos Win2D que participan en este ciclo no usan la recolección de elementos no utilizados de .NET. Esto significa que el recolector de elementos no utilizados no puede ver toda la cadena, por lo que no puede detectar ni reclamar los objetos. Cuando esto ocurre, la aplicación debe contribuir interrumpiendo explícitamente el ciclo. Esto se puede hacer liberando todas las referencias de la página al control (como se recomienda anteriormente) o liberando todas las referencias del control a los delegados del controlador de eventos que podrían apuntar a la página (mediante el evento Página descargada para cancelar la suscripción a todos los controladores de eventos).