當應用程式移至背景時釋出記憶體

本文向您展示如何減少應用程式在進入背景狀態時使用的記憶體量,以便它不會被暫停並可能終止。

新的背景事件

Windows 10 版本 1607 引進兩個新的應用程式生命週期事件:EnteredBackgroundLeavingBackground。 這些事件讓您的應用程式知道它何時進入和離開背景。

當您的應用程式進入背景時,系統強制執行的記憶體限制可能會變更。 使用這些事件檢查您當前的記憶體消耗和可用資源,以保持在限制以下,以便您的應用程式在背景時不會被掛起並可能終止。

用來控制應用程式記憶體使用量的事件

MemoryManager.AppMemoryUsageLimitChanging 是在應用程式可以使用的總記憶體限制變更之前引發的。 例如,當應用程式移至背景,並在 Xbox 上,記憶體限制會從 1024MB 變更為 128MB。
這是讓平台無法暫停或終止應用程式時處理的最重要事件。

當應用程式的記憶體耗用量增加為 AppMemoryUsageLevel 列舉中的較高值時,就會引發 MemoryManager.AppMemoryUsageIncreased。 例如,從。 處理此事件是選擇性的,但建議使用,因為應用程式仍須負責保持在限制之下。

當應用程式的記憶體消耗降低到 AppMemoryUsageLevel 枚舉中的較低值時,會引發 MemoryManager.AppMemoryUsageDecreased。 例如,從。 處理此事件是選擇性的,但表示應用程式可能會視需要配置額外的記憶體。

處理前景與背景之間的轉換

當您的應用程式從前景移至背景時,會引發 EnteredBackground 事件。 當您的應用程式回到前景時,會引發 LeavingBackground 事件。 建立應用程式時,您可以註冊這些事件的處理程式。 在預設項目範本中,這會在 App.xaml.cs的 App 類別建構函式中完成。

因為在背景中執行會減少應用程式允許保留的記憶體資源,因此您也應該註冊 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 屬性,以指定新的限制。 如果您的記憶體使用量超過限制,您需要減少記憶體使用量。

在此範例中,這會在 Helper 方法 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 是否為 HighOverLimit,如果是,請減少記憶體使用量。 在此範例中,這是由 Helper 方法 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 是一種協助程式方法,您可以在應用程式超過背景執行時的使用量限制時,實作以釋放記憶體。 釋放記憶體的方式取決於您的應用程式細節,但釋出記憶體的建議方式之一是處置您的 UI 和與應用程式檢視相關聯的其他資源。 若要這樣做,請確定您是在背景狀態中執行,然後將應用程式視窗的 Content 屬性設定為 null ,並取消註冊您的 UI 事件處理程式,並移除您可能對頁面的任何其他參考。 無法取消註冊 UI 事件處理程式,並清除您對頁面可能必須的任何其他參考,將會防止頁面資源被釋放。 然後呼叫 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 都會開始其中斷連線程式。 如果視窗內容下的視覺物件樹中有 Pages,這些頁面將開始觸發其 Unloaded 事件。 除非移除所有對頁面的參考,否則無法完全清除記憶體中的頁面。 在 Unloaded 回呼中,執行以下操作以確保快速釋放記憶體:

  • 清除頁面中的任何大型資料結構,並將設定為 null
  • 取消註冊頁面內具有回呼方法的所有事件處理程式。 請務必在 Page 的 Loaded 事件處理程式期間註冊這些回呼。 當 UI 已重新建立,而且頁面已新增至可視化物件樹狀結構時,就會引發 Loaded 事件。
  • 在 Unloaded 回調結束時調用 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,請重建您的應用程式檢視。 在此範例中,視窗內容是在 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);
    }
}

CreateRootFrame 協助程式方法會重新建立應用程式的檢視內容。 此方法中的程式代碼與預設項目範本中提供的 OnLaunched 處理程式程式代碼幾乎完全相同。 唯一的差異在於 Launching 處理程序根據 LaunchActivatedEventArgsPreviousExecutionState 屬性決定先前的執行狀態,而 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);
    }
}

指導方針

從前景移至背景

當應用程式從前景移至背景時,系統會代表應用程式運作,以釋放背景中不需要的資源。 例如,UI 架構會排清快取的紋理,而影片子系統會釋放代表應用程式配置的記憶體。 不過,應用程式仍然需要仔細監視其記憶體使用量,以避免系統暫停或終止。

當應用程式從前景移至背景時,它會先取得 EnteredBackground 事件,然後取得 AppMemoryUsageLimitChanging 事件。

  • 請務必使用 EnteredBackground 事件來釋放您知道應用程式在背景執行時不需要的 UI 資源。 例如,您可以釋放歌曲的封面藝術影像。
  • 請務必使用 AppMemoryUsageLimitChanging 事件,以確保您的應用程式使用的記憶體小於新的背景限制。 如果不是的話,請務必釋放資源。 如果您不這樣做,您的應用程式可能會根據設備特定政策被暫停或終止。
  • 如果在引發 AppMemoryUsageLimitChanging 事件時您的應用程式超出了新的記憶體限制,請務必手動呼叫垃圾收集器。
  • 如果您希望應用程式在背景執行時記憶體使用情況發生變化,請務必使用 AppMemoryUsageIncreased 事件繼續監視應用程式的記憶體使用量。 如果 AppMemoryUsageLevelHighOverLimit,請確保釋放資源。
  • 考慮AppMemoryUsageLimitChanging 事件處理程序中而不是在 EnteredBackground 處理程序中釋放 UI 資源,作為效能最佳化。 使用 EnteredBackground/LeavingBackground 事件處理程式中設定的布林值來追蹤應用程式是否位於背景或前景。 然後在 AppMemoryUsageLimitChanging 事件處理程序中,如果 AppMemoryUsage 超出限制並且應用程式處於背景 (基於布林值),您可以釋放 UI 資源。
  • 不要EnteredBackground 事件中執行長時間執行的操作,因為這可能會導致應用程式之間的轉換對使用者來說顯得緩慢。

從背景移動到前景

當應用程式從背景移動到前景時,應用程式將首先取得 AppMemoryUsageLimitChanging 事件,然後取得 LeavingBackground 事件。

  • 請務必 使用 LeavingBackground 事件,在移至背景時重新建立您的應用程式捨棄的 UI 資源。
  • 背景媒體播放範例 - 展示當您的應用程式進入背景狀態時如何釋放記憶體。
  • 診斷工具 - 使用診斷工具觀察垃圾收集事件並驗證您的應用程式是否按照您期望的方式釋放記憶體。