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

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

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

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

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

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

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

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

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

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

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

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

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

Переход с переднего плана на фон

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

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

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

Переход с фона на передний план

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

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