Освобождение памяти при переходе приложения в фоновый режим

В этой статье рассказывается о том, как уменьшить потребление приложением, когда оно переходит в фоновое состояние, чтобы приложение не было приостановлено или, возможно, завершено.

Новые фоновые события

В Windows 10 версии 1607 впервые представлены два новых события жизненного цикла приложений: EnteredBackground и LeavingBackground. Эти события позволяют приложению определить, когда оно входит в фоновый режим и выходит из него.

Когда приложение переходит в фоновый режим, принудительные системные ограничения памяти могут измениться. Используйте эти события, чтобы проверить текущее потребление памяти и свободные ресурсы и не превысить лимит. В случае превышения лимита работа вашего приложения будет приостановлена и, возможно, завершена, пока оно находится в фоновом режиме.

События для контроля использования памяти приложения

MemoryManager.AppMemoryUsageLimitChanging создается сразу перед тем, как поменяется лимит общей памяти, доступной приложению. Например, если приложение переходит в фоновый режим и выполняется на Xbox, лимит памяти меняется с 1024 МБ до 128 МБ.
Очень важно правильно обработать это событие, чтобы платформа не приостановила и не завершила работу приложения.

MemoryManager.AppMemoryUsageIncreased создается, если потребление памяти приложением выросло до более высокого значения в перечислении AppMemoryUsageLevel. Например, с Low на Medium. Обрабатывать это событие не обязательно, но рекомендуется, поскольку приложение по-прежнему не должно превышать лимит.

MemoryManager.AppMemoryUsageDecreased создается, если потребление памяти приложением снизилось до более низкого значения в перечислении AppMemoryUsageLevel. Например, с High до Low. Обрабатывать это событие необязательно, однако оно указывает на то, что приложение при необходимости может иметь возможность выделить дополнительную память.

Обработка перехода между передним планом и фоновым режимом

Когда приложение переходит с переднего плана в фоновый режим, создается событие EnteredBackground. Когда приложение возвращается на передний план, вызывается событие LeavingBackground. Во время создания приложения можно регистрировать обработчики для этих событий. Для этого в шаблоне проекта по умолчанию это выполняется в конструкторе классов приложений в файле App.xaml.cs.

Так как работа в фоновом режиме уменьшает объем памяти, доступный приложению, вам также следует зарегистрировать события AppMemoryUsageIncreasedи AppMemoryUsageLimitChanging, которые можно использовать, чтобы проверять текущий объем используемой памяти и текущее ограничение. Обработчики этих событий показаны в следующих примерах. Дополнительные сведения о жизненном цикле приложений UWP см. в разделе Жизненный цикл приложения.

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

Когда вызывается событие EnteredBackground, задайте переменной отслеживания значение, чтобы указать, что сейчас приложение работает в фоновом режиме. Это полезно, когда вы создаете код для уменьшения использования памяти.

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

Когда приложение переходит в фоновый режим, система снижает ограничение доступной ему памяти, чтобы у приложения на переднем плане было достаточно ресурсов.

Обработчик события AppMemoryUsageLimitChanging позволяет приложению узнать, что объем доступной памяти уменьшен, а также предоставляет новое ограничение в аргументах события, переданных в обработчик. Сравните свойство MemoryManager.AppMemoryUsage, которое предоставляет сведения о текущем используемом объеме памяти приложения, и свойство NewLimit аргументов события, которое определяет новое ограничение. Если вы превышаете ограничение, необходимо сократить потребление памяти.

В данном примере это делается во вспомогательном методе ReduceMemoryUsage, определенного ниже в этой статье.

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

Примечание

В некоторых конфигурациях устройства приложение может работать при превышении нового ограничения памяти, пока система не столкнется с дефицитом ресурсов, а в других конфигурациях это невозможно. Так, на консолях Xbox приложения будут приостановлены или закрыты, если они не уменьшат объем используемой памяти в течение 2 секунд. Это значит, что для оптимальной работы на самом широком спектре устройств используйте это событие, чтобы снизить объем используемых ресурсов в течение двух секунд после возникновения события.

Бывает так, что использование ресурсов памяти приложением в настоящее время, после первого перехода в фоновый режим, находится в допустимых пределах памяти для фоновых приложений, впоследствии расходование памяти увеличивается и начинает приближаться к лимиту. Обработчик события AppMemoryUsageIncreased позволяет проверить ваше текущий объем используемых ресурсов и, при необходимости, освободить память.

Если значение AppMemoryUsageLevel равно High или OverLimit, уменьшите объем используемой памяти. В этом примере процесс реализуется вспомогательным методом ReduceMemoryUsage. Вы также можете подписаться события AppMemoryUsageDecreased, чтобы убедиться, что ваше приложение не превышает ограничение, и при необходимости выделить дополнительные ресурсы.

/// <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 — это вспомогательный метод , который вы можете реализовать, чтобы освободить память, когда приложение превышает ограничение для приложений, работающих в фоновом режиме. Способ освобождения памяти зависит от конкретного приложения, но в общем случае рекомендуется освобождать ресурсы пользовательского интерфейса и другие ресурсы, связанные с визуализацией. Для этого убедитесь, что работаете в фоновом режиме, затем задайте для свойства Содержимое окна вашего приложения значение null и отмените регистрацию обработчиков событий ИП, а затем удалите любые существующие ссылки на страницу. Если вы не отмените регистрацию своих обработчиков событий пользовательского интерфейса и не удалите все ссылки на страницу, ресурсы страницы освобождены не будут. Затем вызовите метод GC.Collect, чтобы немедленно воспользоваться освободившейся памятью. Обычно сбор мусора не запускается принудительно, потому что система берет эту заботу на себя. В этом конкретном случае мы сокращаем объем памяти, выделенной этому приложению, при его переходе в фоновый режим, чтобы снизить вероятность того, что система завершит работу приложения для высвобождения памяти.

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

Если содержимое окна собирается, каждый Frame начинает процесс отключения. Если в визуальном дереве объектов в разделе содержимого окна есть страницы, будут вызываться события Unloaded. Страницы невозможно полностью удалить из памяти, если не удалить все ссылки на них. В обратном вызове Unloaded выполните следующие действия, чтобы быстро освободить память.

  • Очистите все крупные структуры данных на странице и присвойте им значение null.
  • Отмените регистрацию всех обработчиков событий, в которых есть методы обратного вызова на странице. Зарегистрируйте эти обратные вызовы в обработчике события Loaded для страницы. Событие Loaded наступает, когда пользовательский интерфейс восстанавливается, а страница добавляется в визуальное дерево объектов.
  • Вызовите метод GC.Collect в конце обратного вызова Unloaded, чтобы быстро собрать мусор всех крупных структур данных, которым вы присвоили значение null. Опять же, обычно сбор мусора не запускается принудительно, потому что система берет эту заботу на себя. В этом конкретном случае мы сокращаем объем памяти, выделенной этому приложению, при его переходе в фоновый режим, чтобы снизить вероятность того, что система завершит работу приложения для высвобождения памяти.
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();
}

В обработчике событий LeavingBackground следует задать переменную отслеживания (isInBackgroundMode), чтобы указать, что приложение больше не работает в фоновом режиме. Затем проверьте, не присвоено ли свойству Content текущего окна значение null, что происходит, если вы удалили представления приложения, чтобы очистить память в фоновом режиме. Если содержимого окна имеет значение null, перестройте представление приложения. В этом примере содержимое окна создано во вспомогательном методе 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);
    }
}

Вспомогательный метод CreateRootFrame воссоздает содержимое представления приложения. Код в этом методе почти идентичен коду обработчика OnLaunched, представленного в шаблоне проекта. Единственное отличие состоит в том, что обработчик Launching определяет предыдущее состояние выполнения на основе свойства PreviousExecutionState объекта LaunchActivatedEventArgs, а метод CreateRootFrame просто получает предыдущее состояние, переданное в качестве аргумента. Чтобы не дублировать код, можно выполнить рефакторинг кода обработчика событий Launching по умолчанию, чтобы вызывать 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);
    }
}

Рекомендации

Переход с переднего плана в фоновый режим

При перемещении приложения с переднего плана в фоновый режим система действует от имени приложения, чтобы освободить ресурсы, которые не требуются в фоновом режиме. Например, платформы ИП удаляют кэшированные текстуры, а подсистема видео освобождает память, выделенную от имени приложения. Однако приложению по-прежнему необходимо тщательно отслеживать использование памяти, чтобы его работа не была приостановлена или прекращена системой.

Когда приложения перемещается с переднего плана в фоновый режим, оно сначала получает событие EnteredBackground, а затем — AppMemoryUsageLimitChanging.

  • Используйте событие EnteredBackground,чтобы освободить ресурсы ИП, которые, насколько вам известно, не нужны приложению в фоновом режиме. Например, можно освободить изображение обложки для композиции.
  • Используйте событие AppMemoryUsageLimitChanging, чтобы убедиться, что приложение потребляет меньше памяти, чем указано в лимите для фонового режима. В противном случае не забудьте освободить ресурсы. Если этого не сделать, работа приложения может быть приостановлена или прекращена в соответствии с политикой для конкретного устройства.
  • Вызывайте сборщик мусора вручную, если приложение превышает лимит использования памяти, когда создается событие AppMemoryUsageLimitChanging.
  • Используйте событие AppMemoryUsageIncreased, чтобы продолжить мониторинг использования памяти приложения, работая в фоновом режиме, если вы допускаете, что этот показатель может измениться. Если AppMemoryUsageLevel имеет значение High или OverLimit, не забудьте освободить ресурсы.
  • Оцените целесообразность освобождения ресурсов ИП в обработчике событий AppMemoryUsageLimitChanging вместо EnteredBackground в качестве средства оптимизации производительности. Используйте логическое значение, заданное в обработчиках событий EnteredBackground/LeavingBackground, чтобы отследить, работает ли приложение в фоновом режиме или на переднем плане. Затем в обработчике событий AppMemoryUsageLimitChanging можно освободить ресурсы ИП, если AppMemoryUsage превышает лимит и приложение работает в фоновом режиме (на основе логического значения).
  • Не выполняйте длительные операции в событии EnteredBackground, поскольку в этом случае переключение между приложениями может казаться пользователю медленнее.

Переход с фонового режима на передний план

Когда приложения перемещаются с фонового режима на передний план, приложение сначала получит событие AppMemoryUsageLimitChanging, а затем — LeavingBackground.

  • Используйте событие LeavingBackground, чтобы восстановить ресурсы ИП, освобожденные приложением при переходе в фоновый режим.