Share via


Liberar memoria cuando la aplicación pasa a segundo plano

En este artículo se muestra cómo reducir la cantidad de memoria que usa la aplicación cuando pasa al estado de segundo plano para evitar su suspensión o, posiblemente, su finalización.

Nuevos eventos de segundo plano

Windows 10, versión 1607, incorpora dos nuevos eventos de ciclo de vida de aplicación: EnteredBackground y LeavingBackground. Estos eventos permiten que la aplicación sepa cuándo entra y sale del estado de segundo plano.

Cuando la aplicación pasa al estado de segundo plano, pueden cambiar las restricciones de memoria que aplica el sistema. Usa estos eventos para comprobar el consumo de memoria actual y liberar recursos para permanecer por debajo del límite, de modo que no se suspenda o, tal vez, se finalice la aplicación mientras está en segundo plano.

Eventos para controlar el uso de memoria de la aplicación

El evento MemoryManager.AppMemoryUsageLimitChanging se genera justo antes de que cambie el límite del total de memoria que puede usar la aplicación. Por ejemplo, si la aplicación pasa a segundo plano y en la consola Xbox el límite de memoria cambia de 1024 MB a 128 MB.
Este es el evento más importante que se debe controlar para evitar que la plataforma suspenda o finalice la aplicación.

El evento MemoryManager.AppMemoryUsageIncreased se genera cuando el consumo de memoria de la aplicación ha aumentado a un valor superior en la enumeración AppMemoryUsageLevel. Por ejemplo, de Low a Medium. Si bien controlar este evento es opcional, se recomienda hacerlo porque la aplicación sigue siendo responsable de mantenerse por debajo del límite.

El evento MemoryManager.AppMemoryUsageDecreased se genera cuando el consumo de memoria de la aplicación ha disminuido a un valor inferior en la enumeración AppMemoryUsageLevel. Por ejemplo, de High a Low. Controlar este evento es opcional, pero indica que la aplicación puede asignar memoria adicional si es necesario.

Controlar la transición entre el primer plano y el segundo plano

Cuando la aplicación pasa de primer plano a segundo plano, se genera el evento EnteredBackground. Cuando la aplicación vuelve al primer plano, se genera el evento LeavingBackground. Puedes registrar controladores para estos eventos cuando creas la aplicación. En la plantilla de proyecto predeterminada, esto se realiza en el constructor de clase App en App.xaml.cs.

Dado que la ejecución en segundo plano reducirá los recursos de memoria que la aplicación tiene permitido conservar, también debes registrar los eventos AppMemoryUsageIncreased y AppMemoryUsageLimitChanging, que puedes usar para comprobar el uso actual de memoria de la aplicación y el límite actual. En los siguientes ejemplos se muestran los controladores de estos eventos. Para obtener más información sobre el ciclo de vida de las aplicaciones para UWP, consulta Ciclo de vida de la aplicación.

public App()
{
    this.InitializeComponent();

    this.Suspending += OnSuspending;

    // Subscribe to key lifecyle events to know when the app
    // transitions to and from foreground and background.
    // Leaving the background is an important transition
    // because the app may need to restore UI.
    this.EnteredBackground += AppEnteredBackground;
    this.LeavingBackground += AppLeavingBackground;

    // During the transition from foreground to background the
    // memory limit allowed for the application changes. The application
    // has a short time to respond by bringing its memory usage
    // under the new limit.
    Windows.System.MemoryManager.AppMemoryUsageLimitChanging += MemoryManager_AppMemoryUsageLimitChanging;

    // After an application is backgrounded it is expected to stay
    // under a memory target to maintain priority to keep running.
    // Subscribe to the event that informs the app of this change.
    Windows.System.MemoryManager.AppMemoryUsageIncreased += MemoryManager_AppMemoryUsageIncreased;
}

Cuando se genera el evento EnteredBackground, establece la variable de seguimiento para indicar que actualmente estás ejecutando en segundo plano. Esto será útil cuando escribas el código para reducir el uso de memoria.

/// <summary>
/// The application entered the background.
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
private void AppEnteredBackground(object sender, EnteredBackgroundEventArgs e)
{
    _isInBackgroundMode = true;

    // An application may wish to release views and view data
    // here since the UI is no longer visible.
    //
    // As a performance optimization, here we note instead that
    // the app has entered background mode with _isInBackgroundMode and
    // defer unloading views until AppMemoryUsageLimitChanging or
    // AppMemoryUsageIncreased is raised with an indication that
    // the application is under memory pressure.
}

Cuando la aplicación cambia al estado de segundo plano, el sistema reduce el límite de memoria de la aplicación con el fin de garantizar que la aplicación en primer plano actual tenga los recursos suficientes para proporcionar una experiencia del usuario que responda adecuadamente.

El controlador del evento AppMemoryUsageLimitChanging permite a la aplicación saber que la memoria que tiene asignada se ha reducido y proporciona el nuevo límite en los argumentos del evento pasados al controlador. Compara la propiedad MemoryManager.AppMemoryUsage, que proporciona el uso actual de la aplicación, con propiedad la NewLimit de los argumentos del evento, que especifica el nuevo límite. Si el uso de memoria supera el límite, debes reducir dicho uso de memoria.

En este ejemplo, esto se realiza en el método auxiliar ReduceMemoryUsage, que se define más adelante en este artículo.

/// <summary>
/// Raised when the memory limit for the app is changing, such as when the app
/// enters the background.
/// </summary>
/// <remarks>
/// If the app is using more than the new limit, it must reduce memory within 2 seconds
/// on some platforms in order to avoid being suspended or terminated.
///
/// While some platforms will allow the application
/// to continue running over the limit, reducing usage in the time
/// allotted will enable the best experience across the broadest range of devices.
/// </remarks>
/// <param name="sender"></param>
/// <param name="e"></param>
private void MemoryManager_AppMemoryUsageLimitChanging(object sender, AppMemoryUsageLimitChangingEventArgs e)
{
    // If app memory usage is over the limit, reduce usage within 2 seconds
    // so that the system does not suspend the app
    if (MemoryManager.AppMemoryUsage >= e.NewLimit)
    {
        ReduceMemoryUsage(e.NewLimit);
    }
}

Nota

Las configuraciones de algunos dispositivo permitirán que una aplicación siga ejecutándose por encima del nuevo límite de memoria hasta que el sistema sufra presión de recursos, pero las de otros dispositivos no lo permitirán. En concreto, en Xbox, las aplicaciones se suspenderán o finalizarán si no reducen el uso de memoria por debajo de los nuevos límites en un plazo de 2 (dos) segundos. Esto significa que puedes ofrecer la mejor experiencia en la gama más amplia de dispositivos mediante el uso de este evento para reducir el uso de recursos por debajo del límite en el plazo de 2 (dos) segundos desde que se genere el evento.

Es posible que, aunque el uso de memoria de la aplicación esté actualmente por debajo del límite de memoria para aplicaciones en segundo plano, cuando cambie por primera vez a segundo plano, pueda aumentar el consumo de memoria con el tiempo y empezar a acercarse al límite. El controlador del evento AppMemoryUsageIncreased ofrece la posibilidad de comprobar el uso actual cuando aumenta y, si es necesario, liberar memoria.

Comprueba si el valor de AppMemoryUsageLevel es High u OverLimit, y si es así, reduce el uso de memoria. En este ejemplo, esto se controla con el método auxiliar ReduceMemoryUsage. También puede suscribirte al evento AppMemoryUsageDecreased, comprobar si la aplicación está por debajo del límite y, si es así, saber que puedes asignar recursos adicionales.

/// <summary>
/// Handle system notifications that the app has increased its
/// memory usage level compared to its current target.
/// </summary>
/// <remarks>
/// The app may have increased its usage or the app may have moved
/// to the background and the system lowered the target for the app
/// In either case, if the application wants to maintain its priority
/// to avoid being suspended before other apps, it may need to reduce
/// its memory usage.
///
/// This is not a replacement for handling AppMemoryUsageLimitChanging
/// which is critical to ensure the app immediately gets below the new
/// limit. However, once the app is allowed to continue running and
/// policy is applied, some apps may wish to continue monitoring
/// usage to ensure they remain below the limit.
/// </remarks>
/// <param name="sender"></param>
/// <param name="e"></param>
private void MemoryManager_AppMemoryUsageIncreased(object sender, object e)
{
    // Obtain the current usage level
    var level = MemoryManager.AppMemoryUsageLevel;

    // Check the usage level to determine whether reducing memory is necessary.
    // Memory usage may have been fine when initially entering the background but
    // the app may have increased its memory usage since then and will need to trim back.
    if (level == AppMemoryUsageLevel.OverLimit || level == AppMemoryUsageLevel.High)
    {
        ReduceMemoryUsage(MemoryManager.AppMemoryUsageLimit);
    }
}

ReduceMemoryUsage es un método auxiliar que puedes implementar para liberar memoria cuando la aplicación supera el límite de uso mientras se ejecuta en segundo plano. La manera de liberar memoria depende de los detalles específicos de la aplicación, pero una forma recomendada es desechar la interfaz de usuario y los demás recursos asociados con la vista de la aplicación. Para ello, comprueba que estés ejecutando en el estado de segundo plano y luego establece la propiedad Content de la ventana de la aplicación en null, anula el registro de los controladores de eventos de la interfaz de usuario y quita todas las referencias que pudieras tener a la página. Si no logras anular el registro de los controladores de eventos de la interfaz de usuario ni borrar todas las referencias que pudieras tener a la página, no podrás liberar los recursos de la página. A continuación, llama a GC.Collect para recuperar la memoria liberada inmediatamente. Normalmente no fuerza la recolección de elementos no utilizados porque el sistema se encargará de ello. En este caso específico, estamos reduciendo la cantidad de memoria cargada en esta aplicación, ya que entra en segundo plano para reducir la probabilidad de que el sistema determine que debe finalizar la aplicación para reclamar memoria.

/// <summary>
/// Reduces application memory usage.
/// </summary>
/// <remarks>
/// When the app enters the background, receives a memory limit changing
/// event, or receives a memory usage increased event, it can
/// can optionally unload cached data or even its view content in
/// order to reduce memory usage and the chance of being suspended.
///
/// This must be called from multiple event handlers because an application may already
/// be in a high memory usage state when entering the background, or it
/// may be in a low memory usage state with no need to unload resources yet
/// and only enter a higher state later.
/// </remarks>
public void ReduceMemoryUsage(ulong limit)
{
    // If the app has caches or other memory it can free, it should do so now.
    // << App can release memory here >>

    // Additionally, if the application is currently
    // in background mode and still has a view with content
    // then the view can be released to save memory and
    // can be recreated again later when leaving the background.
    if (isInBackgroundMode && Window.Current.Content != null)
    {
        // Some apps may wish to use this helper to explicitly disconnect
        // child references.
        // VisualTreeHelper.DisconnectChildrenRecursive(Window.Current.Content);

        // Clear the view content. Note that views should rely on
        // events like Page.Unloaded to further release resources.
        // Release event handlers in views since references can
        // prevent objects from being collected.
        Window.Current.Content = null;
    }

    // Run the GC to collect released resources.
    GC.Collect();
}

Cuando se recopila el contenido de la ventana, cada fotograma comienza su proceso de desconexión. Si hay objetos Page en el árbol de objetos visuales en el contenido de la ventana, iniciarán la generación del evento Unloaded. Los objetos Pages no se puede borrar completamente de la memoria, a menos que se eliminen todas las referencias a ellos. En la devolución de llamada de Unloaded, realiza los siguientes pasos para garantizar que la memoria se libere rápidamente:

  • Borra y establece cualquier estructura de datos de gran tamaño de Page en null.
  • Anula el registro de todos los controladores de eventos que tienen métodos de devolución de llamada dentro de Page. Asegúrate de registrar esas devoluciones de llamada durante el controlador del evento Loaded correspondiente a Page. El evento Loaded se genera si la interfaz de usuario se ha reconstituido y el objeto Page se ha agregado al árbol de objetos visuales.
  • Llama a GC.Collect al final de la devolución de llamada de Unloaded para efectuar rápidamente la recolección de elementos no utilizados de cualquiera de las estructuras de datos de gran tamaño que estableciste recién en null. De nuevo, normalmente no fuerza la recolección de elementos no utilizados porque el sistema se encargará de ello. En este caso específico, estamos reduciendo la cantidad de memoria cargada en esta aplicación, ya que entra en segundo plano para reducir la probabilidad de que el sistema determine que debe finalizar la aplicación para reclamar memoria.
private void MainPage_Unloaded(object sender, RoutedEventArgs e)
{
   // << free large data sructures and set them to null, here >>

   // Disconnect event handlers for this page so that the garbage
   // collector can free memory associated with the page
   Window.Current.Activated -= Current_Activated;
   GC.Collect();
}

En el controlador del evento LeavingBackground, establece la variable de seguimiento (isInBackgroundMode) para indicar que la aplicación ya no se ejecuta en segundo plano. A continuación, comprueba si el valor de la propiedad Content de la ventana actual es null, que será así si has desechado las vistas de la aplicación para liberar memoria durante la ejecución en segundo plano. Si el contenido de la ventana es null, reconstruye la vista de la aplicación. En este ejemplo, el contenido de la ventana se crea en el método auxiliar CreateRootFrame.

/// <summary>
/// The application is leaving the background.
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
private void AppLeavingBackground(object sender, LeavingBackgroundEventArgs e)
{
    // Mark the transition out of the background state
    _isInBackgroundMode = false;

    // Restore view content if it was previously unloaded
    if (Window.Current.Content == null)
    {
        CreateRootFrame(ApplicationExecutionState.Running, string.Empty);
    }
}

El método auxiliar CreateRootFrame vuelve a crear el contenido de la vista de la aplicación. El código de este método es casi idéntico al código del controlador OnLaunched proporcionado en la plantilla de proyecto predeterminada. La única diferencia es que el controlador Launching determina el estado de ejecución anterior a partir de la propiedad PreviousExecutionState de la clase LaunchActivatedEventArgs y el método CreateRootFrame simplemente obtiene el estado de ejecución anterior que se pasó como argumento. Para minimizar el código duplicado, puedes refactorizar el código del controlador de eventos Launching predeterminado para llamar a CreateRootFrame.

void CreateRootFrame(ApplicationExecutionState previousExecutionState, string arguments)
{
    Frame rootFrame = Window.Current.Content as Frame;

    // Do not repeat app initialization when the Window already has content,
    // just ensure that the window is active
    if (rootFrame == null)
    {
        // Create a Frame to act as the navigation context and navigate to the first page
        rootFrame = new Frame();

        // Set the default language
        rootFrame.Language = Windows.Globalization.ApplicationLanguages.Languages[0];

        rootFrame.NavigationFailed += OnNavigationFailed;

        if (previousExecutionState == ApplicationExecutionState.Terminated)
        {
            //TODO: Load state from previously suspended application
        }

        // Place the frame in the current Window
        Window.Current.Content = rootFrame;
    }

    if (rootFrame.Content == null)
    {
        // When the navigation stack isn't restored navigate to the first page,
        // configuring the new page by passing required information as a navigation
        // parameter
        rootFrame.Navigate(typeof(MainPage), arguments);
    }
}

Directrices

Pasar de primer plano a segundo plano

Cuando una aplicación pasa de primer plano a segundo plano, el sistema funciona en nombre de la aplicación para liberar los recursos que no son necesarios en segundo plano. Por ejemplo, los marcos de interfaz de usuario vacían las texturas almacenadas en caché y el subsistema de vídeo libera la memoria asignada en nombre de la aplicación. Aun así, una aplicación debe supervisar cuidadosamente su uso de memoria para evitar que el sistema la suspenda o la finalice.

Cuando una aplicación pasa de primer plano a segundo plano, primero obtiene un evento EnteredBackground y después un evento AppMemoryUsageLimitChanging.

  • Usa el evento EnteredBackground para liberar los recursos de interfaz de usuario que sabes que la aplicación no necesita mientras se ejecuta en segundo plano. Por ejemplo, podrías liberar la imagen de portada de una canción.
  • Usa el evento MemoryManager.AppMemoryUsageLimitChanging para asegurarte de que la aplicación esté usando menos memoria que el nuevo límite de segundo plano. Si no es así, asegúrate de liberar recursos. Si no lo haces, puede que se suspenda o finalice la aplicación según la directiva específica del dispositivo.
  • Invoca manualmente el recolector de elementos no utilizados si la aplicación supera el nuevo límite de memoria cuando se genera el evento AppMemoryUsageLimitChanging.
  • Usa el evento AppMemoryUsageIncreased para seguir supervisando el uso de memoria de la aplicación mientras se ejecuta en segundo plano si esperas que cambie. Si el valor de AppMemoryUsageLevel es High u OverLimit, asegúrate de liberar recursos.
  • Considera liberar recursos de interfaz de usuario en el controlador del evento AppMemoryUsageLimitChanging en lugar de hacerlo en el controlador de EnteredBackground como una optimización del rendimiento. Usa un valor booleano que se establece los controladores de los eventos EnteredBackground/LeavingBackground para saber si la aplicación está en segundo plano o en primer plano. A continuación, en el controlador del evento AppMemoryUsageLimitChanging, si el valor de AppMemoryUsage supera el límite y la aplicación está en segundo plano (según el valor booleano), puedes liberar recursos de interfaz de usuario.
  • No realices operaciones prolongadas en el evento EnteredBackground porque puedes provocar que al usuario la transición entre aplicaciones le parezca lenta.

Pasar de segundo plano a primer plano

Cuando una aplicación pasa de segundo plano a primer plano, primero obtiene un evento AppMemoryUsageLimitChanging y después un evento LeavingBackground.

  • No uses el evento LeavingBackground para volver a crear los recursos de interfaz de usuario que la aplicación desechó cuando pasó a segundo plano.