Liberar memória quando seu app é movido para o segundo plano

Este artigo mostra como reduzir a quantidade de memória que o aplicativo usa quando é movido para o estado em segundo plano, de maneira que não seja suspenso e possivelmente encerrado.

Novos eventos em segundo plano

O Windows 10, versão 1607, apresenta dois novos eventos de ciclo de vida do aplicativo, EnteredBackground e LeavingBackground. Esses eventos informam o aplicativo quando ele está entrando no e saindo do segundo plano.

Quando o aplicativo é movido para segundo plano, as restrições de memória impostas pelo sistema podem mudar. Use esses eventos para verificar o consumo de memória atual e liberar recursos para ficar abaixo do limite, de maneira que o aplicativo não seja suspenso e possivelmente encerrado enquanto está no segundo plano.

Eventos para controlar o uso da memória do aplicativo

MemoryManager.AppMemoryUsageLimitChanging é acionado pouco antes do limite de memória total que o aplicativo pode usar ser alterado. Por exemplo, quando o aplicativo é movido para o segundo plano e no Xbox o limite de memória muda de 1.024 MB para 128 MB.
Este é o evento mais importante a ser manipulado para evitar que a plataforma suspenda ou encerre o aplicativo.

MemoryManager.AppMemoryUsageIncreased é acionado quando o consumo de memória do aplicativo é aumentado para um valor superior na enumeração AppMemoryUsageLevel. Por exemplo, de Baixo para Médio. A manipulação desse evento é opcional, mas recomendada porque o aplicativo continua sendo responsável por se manter abaixo do limite.

MemoryManager.AppMemoryUsageDecreased é acionado quando o consumo de memória do aplicativo é diminuído para um valor inferior na enumeração AppMemoryUsageLevel. Por exemplo, de Alto para Baixo. A manipulação desse evento é opcional, mas indica que o aplicativo poderá ser capaz de alocar memória adicional, se necessário.

Manipular a transição entre primeiro e segundo plano

Quando o aplicativo é movido do primeiro para o segundo plano, o evento EnteredBackground é acionado. Quando o aplicativo retorna ao primeiro plano, o evento LeavingBackground é acionado. Você pode registrar manipuladores para esses eventos quando o aplicativo é criado. No modelo de projeto padrão, isso é feito no construtor de classe App em App.xaml.cs.

Como a execução em segundo plano reduz os recursos de memória que o aplicativo tem permissão para reter, também é necessário realizar o registro dos eventos AppMemoryUsageIncreased e AppMemoryUsageLimitChanging, que podem ser usados para verificar o uso da memória do aplicativo e o limite atuais. Os manipuladores para esses eventos são mostrados nos exemplos a seguir. Para saber mais sobre o ciclo de vida de aplicativos para aplicativos UWP, consulte Ciclo de vida do aplicativo.

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 o evento EnteredBackground for acionado, defina a variável de rastreamento para indicar que você está em execução em segundo plano. Isso será útil quando você escreverá o código para reduzir o uso da memória.

/// <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 o aplicativo faz a transição para o segundo plano, o sistema reduz o limite de memória para o aplicativo para garantir que o aplicativo em primeiro plano no momento tenha recursos suficientes para proporcionar uma experiência de usuário responsiva.

O manipulador de eventos AppMemoryUsageLimitChanging permite que o aplicativo saiba que sua memória alocada foi reduzida e fornece o novo limite nos argumentos de eventos passados ao manipulador. Compare a propriedade MemoryManager.AppMemoryUsage, que fornece o uso atual do aplicativo, com a propriedade NewLimit dos argumentos de eventos, que especifica o novo limite. Se o uso da memória exceder o limite, você precisará reduzi-lo.

Neste exemplo, isso é feito no método auxiliar ReduceMemoryUsage, que é definido posteriormente neste artigo.

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

Observação

Algumas configurações de dispositivo permitirão que um aplicativo continue em execução com o novo limite de memória até que o sistema sofra pressão por recursos, mas algumas delas não permitirão. No Xbox especificamente, os aplicativos serão suspensos ou encerrados caso não reduzam o uso de memória para os novos limites dentro de 2 segundos. Isso significa que você pode oferecer a melhor experiência na mais ampla variedade de dispositivos usando esse evento para reduzir o uso de recursos para abaixo do limite, dentro dos dois segundos após o acionamento do evento.

É possível que, embora o uso da memória do aplicativo esteja abaixo do limite de memória para aplicativos em segundo plano quando ocorre a primeira transição para o segundo plano, isso pode aumentar o consumo de memória ao longo do tempo e começar a se aproximar do limite. O manipulador AppMemoryUsageIncreased oferece uma oportunidade para verificar o uso quando ele aumenta e, se necessário, liberar espaço na memória.

Verifique se o AppMemoryUsageLevel está High ou OverLimite, em caso afirmativo, reduza o uso de memória. Neste exemplo, isso é tratado pelo método auxiliar ReduceMemoryUsage. Você também pode se inscrever no evento AppMemoryUsageDecreased. Verifique se o aplicativo está abaixo do limite e, em caso afirmativo, você sabe que pode alocar recursos adicionais.

/// <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 é um método auxiliar que pode ser implementado para liberar espaço na memória quando o aplicativo estiver além do limite de uso em execução em segundo plano. A maneira de liberar espaço na memória depende das especificações do seu aplicativo, mas uma forma recomendada para liberar memória é descartar a interface do usuário e outros recursos associados ao modo de exibição do aplicativo. Para isso, certifique-se de que esteja executando no estado em segundo plano e defina a propriedade Content da janela do aplicativo como null e cancele o registro dos manipuladores de eventos da interface do usuário, além de remover todas as outras referências que você possa ter à página. Deixar de cancelar o registro dos manipuladores de eventos da interface do usuário e limpar todas as outras referências que você possa ter à página evitará que os recursos sejam liberados. Em seguida, chame GC.Collect para recuperar a memória liberada imediatamente. Normalmente, não imponha coleta de lixo porque o sistema cuidará disso para você. Neste caso específico, estamos reduzindo a quantidade de memória, carregada para este aplicativo conforme ele entra em segundo plano para reduzir a probabilidade de que o sistema determinará que ele deve encerrar o aplicativo para recuperar memória.

/// <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 o conteúdo da janela for coletado, cada quadro começará seu processo de desconexão. Se houver páginas na árvore de objetos visual no conteúdo da janela, elas começarão a acionar o eventos Unloaded. As páginas não podem ser completamente limpas da memória, a menos que todas as referências a elas sejam removidas. No retorno de chamada Unloaded, faça o seguinte para garantir que a memória seja liberada rapidamente:

  • Limpe e defina qualquer estrutura de dados grande em sua página como null.
  • Cancele o registro de todos os manipuladores de eventos que tenham métodos de retorno de chamada dentro da página. Assegure-se de registrar esses retornos de chamada durante a utilização do manipulador de eventos Loaded da página. O evento Loaded é acionado quando a interface do usuário for reconstituída e a página for adicionada à árvore visual do objeto.
  • Chame GC.Collect no final do retorno da chamada Unloaded para coletar rapidamente o lixo de qualquer uma das estruturas de dados grandes que você definiu como null. Mais uma vez, normalmente você não impõe coleta de lixo porque o sistema cuidará disso para você. Neste caso específico, estamos reduzindo a quantidade de memória, carregada para este aplicativo conforme ele entra em segundo plano para reduzir a probabilidade de que o sistema determinará que ele deve encerrar o aplicativo para recuperar memória.
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();
}

No manipulador de eventos LeavingBackground, defina a variável de rastreamento (isInBackgroundMode) para indicar que o aplicativo não está mais em execução em segundo plano. Em seguida, verifique se Content da janela atual é null, o que acontecerá se você tiver descartado os modos de exibição do aplicativo para liberar memória enquanto estava em execução em segundo plano. Se o conteúdo da janela for null, recompile o modo de exibição do aplicativo. Neste exemplo, o conteúdo da janela é criado no 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);
    }
}

O método auxiliar CreateRootFrame recria o conteúdo do modo de exibição para o aplicativo. O código desse método é quase idêntico ao do código do manipulador OnLaunched fornecido no modelo de projeto padrão. A única diferença é que o manipulador Launching determina o estado de execução anterior a partir da propriedade PreviousExecutionState do LaunchActivatedEventArgs e o método CreateRootFrame simplesmente obtém o estado de execução anterior passado como um argumento. Para minimizar o código duplicado, você pode refatorar o código padrão do manipulador de eventos Launching para chamar 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);
    }
}

Diretrizes

Mudança do primeiro para o segundo plano

Quando um aplicativo é movido do primeiro para o segundo plano, o sistema funciona em nome do aplicativo para liberar recursos que não sejam necessários no segundo plano. Por exemplo, as estruturas da interface do usuário liberam texturas em cache, e o subsistema de vídeo libera memória alocada em nome do aplicativo. No entanto, um aplicativo ainda precisará monitorar cuidadosamente o uso de memória para evitar ser suspenso ou encerrado pelo sistema.

Quando for movido do primeiro para o segundo plano, um aplicativo receberá primeiramente um evento EnteredBackground e, em seguida, um evento AppMemoryUsageLimitChanging.

  • Use o evento EnteredBackground para liberar recursos de interface do usuário de que o aplicativo reconhecidamente não precisa durante a execução no segundo plano. Por exemplo, você pode liberar a imagem de arte da capa de uma música.
  • Use o evento AppMemoryUsageLimitChanging para garantir que o aplicativo use menos memória do que o novo limite em segundo plano. Certifique-se de liberar recursos, caso não tenha feito isso. Se você não fizer isso, o aplicativo poderá ser suspenso ou encerrado de acordo com a política de dispositivo específica.
  • Invoque manualmente o coletor de lixo se o aplicativo estiver acima do novo limite da memória quando o evento AppMemoryUsageLimitChanging for acionado.
  • Use o evento AppMemoryUsageIncreased para continuar monitorando o uso de memória do aplicativo em execução no segundo plano se achar que isso deve mudar. Se o AppMemoryUsageLevel for High ou OverLimit, não se esqueça de liberar recursos.
  • Considere liberar recursos da interface do usuário no manipulador de eventos AppMemoryUsageLimitChanging em vez do manipulador EnteredBackground como uma otimização de desempenho. Use um valor booliano definido nos manipuladores de eventos EnteredBackground/LeavingBackground para acompanhar se o aplicativo está no segundo ou no primeiro plano. Em seguida, no manipulador de eventos AppMemoryUsageLimitChanging, se AppMemoryUsage estiver acima do limite e o aplicativo estiver no segundo plano (com base no valor booliano), você poderá liberar recursos de interface do usuário.
  • Não execute operações de longa duração no evento EnteredBackground porque você pode fazer com que a transição entre os aplicativos pareça lenta para o usuário.

Mudança do segundo para o primeiro plano

Quando for movido do segundo para o primeiro plano, o aplicativo receberá primeiramente um evento AppMemoryUsageLimitChanging e, em seguida, um evento LeavingBackground.

  • Use o evento LeavingBackground para recriar recursos de interface do usuário que o aplicativo descartou ao mover para o segundo plano.