Liberare memoria quando l'app passa in background

Questo articolo illustra come ridurre la quantità di memoria usata dall'app quando passa allo stato in background in modo che non venga sospesa ed eventualmente terminata.

Nuovi eventi in background

Windows 10, versione 1607, introduce due nuovi eventi del ciclo di vita dell'applicazione, EnteredBackground e LeavingBackground. Questi eventi consentono all'app di sapere quando entra e lascia il background.

Quando l'app passa in background, i vincoli di memoria applicati dal sistema possono cambiare. Usare questi eventi per controllare il consumo di memoria corrente e le risorse gratuite per rimanere al di sotto del limite in modo che l'app non venga sospesa ed eventualmente terminata mentre è in background.

Eventi per controllare l'utilizzo della memoria dell'app

MemoryManager.AppMemoryUsageLimitChanging viene generato poco prima che venga modificato il limite di memoria totale che l'app può usare. Ad esempio, quando l'app si sposta in background e su Xbox il limite di memoria cambia da 1024 MB a 128 MB.
Questo è l'evento più importante da gestire per impedire alla piattaforma di sospendere o terminare l'app.

MemoryManager.AppMemoryUsageIncreased viene generato quando l'utilizzo della memoria dell'app è aumentato a un valore superiore nell'enumerazione AppMemoryUsageLevel. Ad esempio, da Basso a Medio. La gestione di questo evento è facoltativa ma consigliata perché l'applicazione è ancora responsabile di rimanere al di sotto del limite.

MemoryManager.AppMemoryUsageDecreased viene generato quando l'utilizzo della memoria dell'app è ridotto a un valore inferiore nell'enumerazione AppMemoryUsageLevel. Ad esempio, da Alto a Basso. La gestione di questo evento è facoltativa, ma indica che l'applicazione può allocare memoria aggiuntiva, se necessario.

Gestire la transizione tra primo piano e background

Quando l'app passa dal primo piano allo sfondo, viene generato l'evento EnteredBackground. Quando l'app torna in primo piano, viene generato l'evento LeavingBackground. È possibile registrare i gestori per questi eventi quando viene creata l'app. Nel modello di progetto predefinito, questa operazione viene eseguita nel costruttore della classe App in App.xaml.cs.

Poiché l'esecuzione in background riduce le risorse di memoria che l'app può conservare, ci si deve anche registrare per gli eventi AppMemoryUsageIncreased e AppMemoryUsageLimitChanging che si possono usare per controllare l'utilizzo corrente della memoria dell'app e il limite corrente. I gestori per questi eventi sono illustrati negli esempi seguenti. Per altre informazioni sui metodi del ciclo di vita delle app UWP, vedere ciclo di vita dell'App.

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

Quando viene generato l'evento EnteredBackground, impostare la variabile di rilevamento per indicare che è attualmente in esecuzione in background. Ciò sarà utile quando si scrive il codice per ridurre l'utilizzo della 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.
}

Quando l'app passa in background, il sistema riduce il limite di memoria per l'app per garantire che l'app in primo piano corrente disponga di risorse sufficienti per offrire un'esperienza utente reattiva

Il gestore eventi AppMemoryUsageLimitChanging consente all'app di sapere che la memoria allocata è stata ridotta e fornisce il nuovo limite negli argomenti dell'evento passati al gestore. Confrontare la MemoryManager.AppMemoryUsage che fornisce l'utilizzo corrente dell'app, alla proprietà NewLimit degli argomenti dell'evento, che specifica il nuovo limite. Se l'utilizzo della memoria supera il limite, è necessario ridurre l'utilizzo della memoria.

In questo esempio questa operazione viene eseguita nel metodo helper ReduceMemoryUsage, definito più avanti in questo articolo.

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

Alcune configurazioni del dispositivo consentiranno a un'applicazione di continuare l'esecuzione oltre il nuovo limite di memoria fino a quando il sistema non subisce un utilizzo elevato delle risorse e altri no. In Xbox, in particolare, le app verranno sospese o terminate se non riducono la memoria al di sotto dei nuovi limiti entro 2 secondi. Ciò significa che è possibile offrire la migliore esperienza nell'ampia gamma di dispositivi usando questo evento per ridurre l'utilizzo delle risorse al di sotto del limite entro 2 secondi dall'evento generato.

È possibile che, anche se l'utilizzo della memoria dell'app è attualmente inferiore al limite di memoria per le app in background quando passa per la prima volta in background, può aumentare il consumo di memoria nel tempo e iniziare ad avvicinarsi al limite. Il gestore AppMemoryUsageIncreased offre la possibilità di controllare l'utilizzo corrente quando aumenta e, se necessario, liberare memoria.

Verificare se AppMemoryUsageLevel è High o OverLimit e, in tal caso, ridurre l'utilizzo della memoria. In questo esempio questo viene gestito dal metodo helper ReduceMemoryUsage. È anche possibile sottoscrivere l'evento AppMemoryUsageDecreased, verificare se l'app è inferiore al limite e, in tal caso, si sa che è possibile allocare risorse aggiuntive.

/// <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 è un metodo helper che si può implementare per rilasciare memoria quando l'app supera il limite di utilizzo durante l'esecuzione in background. La modalità di rilascio della memoria dipende dalle specifiche dell'app, ma un modo consigliato per liberare memoria consiste nell'eliminare l'interfaccia utente e le altre risorse associate alla visualizzazione dell'app. A tale scopo, assicurarsi di essere in esecuzione nello stato in background, quindi impostare la proprietà Content della finestra dell'app su null e annullare la registrazione dei gestori eventi dell'interfaccia utente e rimuovere eventuali altri riferimenti che potrebbe essere necessario per la pagina. Se non si annulla la registrazione dei gestori eventi dell'interfaccia utente e si cancellano altri riferimenti, potrebbe essere necessario annullare la registrazione della pagina, le risorse della pagina non verranno rilasciate. Quindi chiamare GC.Collect per recuperare immediatamente la memoria liberata. In genere non si forza l'operazione di Garbage Collection perché il sistema ne prenderà cura automaticamente. In questo caso specifico, si riduce la quantità di memoria addebitata a questa applicazione man mano che entra in background per ridurre la probabilità che il sistema determini che deve terminare l'app per recuperare 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();
}

Quando viene raccolto il contenuto della finestra, ogni frame avvia il processo di disconnessione. Se nella struttura ad albero oggetti visivi sono presenti pagine sotto il contenuto della finestra, inizieranno a generarne l'evento Unloaded. Le pagine non possono essere cancellate completamente dalla memoria, a meno che non vengano rimossi tutti i riferimenti. Nel callback scaricato eseguire le operazioni seguenti per assicurarsi che la memoria venga liberata rapidamente:

  • Cancellare e impostare tutte le strutture di dati di grandi dimensioni nella pagina su null.
  • Annullare la registrazione di tutti i gestori di eventi che dispongono di metodi di callback all'interno di Page. Assicurarsi di registrare tali callback durante il gestore eventi Loaded per page. L'evento Loaded viene generato quando l'interfaccia utente è stata ricostituita e la pagina è stata aggiunta all'albero degli oggetti visivi.
  • Chiamare GC.Collect alla fine del callback scaricato per eseguire rapidamente il Garbage Collection di tutte le strutture di dati di grandi dimensioni appena impostate su null. Di nuovo, in genere non si forza l'operazione di Garbage Collection perché il sistema ne prenderà cura automaticamente. In questo caso specifico, si riduce la quantità di memoria addebitata a questa applicazione man mano che entra in background per ridurre la probabilità che il sistema determini che deve terminare l'app per recuperare 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();
}

Nel gestore di eventi LeavingBackground, si deve impostare la variabile di rilevamento (isInBackgroundMode) per indicare che l'app non è più in esecuzione in background. Controllare quindi se il Contenuto della finestra corrente è null, che sarà se sono state eliminate le visualizzazioni dell'app per cancellare la memoria durante l'esecuzione in background. Se il contenuto della finestra è null, ricompilare la visualizzazione dell'app. In questo esempio il contenuto della finestra viene creato nel metodo helper 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);
    }
}

Il metodo helper CreateRootFrame ricrea il contenuto della visualizzazione per l'app. Il codice in questo metodo è quasi identico al codice del gestore OnLaunched fornito nel modello di progetto predefinito. La differenza è che il gestore Launching determina lo stato di esecuzione precedente dalla proprietà PreviousExecutionState di LaunchActivatedEventArgs e il metodo CreateRootFrame ottiene semplicemente lo stato di esecuzione precedente passato come argomento. Per ridurre al minimo il codice duplicato, è possibile effettuare il refactoring del codice predefinito del gestore eventi Launching per chiamare 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);
    }
}

Linee guida

Passaggio dal primo piano al background

Quando un'app passa dal primo piano al background, il sistema funziona per conto dell'app per liberare risorse non necessarie in background. Ad esempio, i framework dell'interfaccia utente scaricano trame memorizzate nella cache e il sottosistema video libera memoria allocata per conto dell'app. Tuttavia, un'app dovrà comunque monitorare attentamente l'utilizzo della memoria per evitare di essere sospesa o terminata dal sistema.

Quando un'app passa dal primo piano al background, ottiene prima un evento EnteredBackground e quindi un evento AppMemoryUsageLimitChanging.

  • Usare l'evento EnteredBackground per liberare risorse dell'interfaccia utente che sai che l'app non è necessaria durante l'esecuzione in background. Ad esempio, è possibile liberare l'immagine della copertina per una canzone.
  • Usare l'evento AppMemoryUsageLimitChanging per assicurarti che l'app usi meno memoria del nuovo limite in background. Assicurarsi di liberare le risorse in caso contrario. In caso contrario, l'app potrebbe essere sospesa o terminata in base a criteri specifici del dispositivo.
  • Richiamare manualmente il Garbage Collector se l'app supera il nuovo limite di memoria quando viene generato l'evento AppMemoryUsageLimitChanging.
  • Usare l'evento AppMemoryUsageIncreased per continuare a monitorare l'utilizzo della memoria dell'app durante l'esecuzione in background se si prevede che cambi. Se AppMemoryUsageLevel è Alto o OverLimit, assicurarsi di liberare risorse.
  • Valutare la possibilità di liberare risorse dell'interfaccia utente nel gestore eventi AppMemoryUsageLimitChanging anziché nel gestore EnteredBackground come ottimizzazione delle prestazioni. Usa un valore booleano impostato nei gestori eventi EnteredBackground/LeavingBackground per tenere traccia se l'app è in background o in primo piano. Quindi, nel gestore dell'evento AppMemoryUsageLimitChanging, se AppMemoryUsage supera il limite e l'app è in background (in base al valore booleano) è possibile liberare le risorse dell'interfaccia utente.
  • Non eseguire operazioni a esecuzione prolungata nell'evento EnteredBackground perché è possibile che la transizione tra le applicazioni venga visualizzata lentamente all'utente.

Passaggio dal background al primo piano

Quando un'app passa dal background al primo piano, l'app ottiene prima un evento AppMemoryUsageLimitChanging e quindi un evento LeavingBackground.

  • Usare l'evento LeavingBackground per ricreare le risorse dell'interfaccia utente rimosse dall'app durante lo spostamento in background.