アプリがバックグラウンドに移動したときのメモリの解放

この記事では、アプリが一時停止にされたり、場合によっては終了にされたりすることがないように、バックグラウンド状態に移行したアプリで使用するメモリの量を削減する方法を説明します。

新しいバックグラウンド イベント

Windows 10 バージョン 1607 では、2 つ新しいアプリケーション ライフ サイクル イベント EnteredBackground および LeavingBackground が導入されています。 これらのイベントによって、バックグラウンドへの移行とバックグラウンドからの移行をアプリに通知できます。

アプリがバックグラウンドに移行すると、システムにより強制されるメモリ制限が変化する場合があります。 これらのイベントを使用することで、バックグラウンドに移行しているアプリが中断されたり、場合によっては終了されたりしないように、現在のメモリ消費量を確認してリソースを解放し、制限値を下回っている状態を保ちます。

アプリのメモリ使用量を制御するイベント

MemoryManager.AppMemoryUsageLimitChanging は、アプリで使用できる合計メモリの制限が変更される直前に発生します。 たとえば、アプリがバックグラウンドに移行し、それが Xbox 上である場合、メモリ制限が 1024 MB から 128 MB に変更されます。
プラットフォームによってアプリが中断または終了されることを回避するには、このイベントを処理することが最も重要になります。

MemoryManager.AppMemoryUsageIncreased は、アプリのメモリ消費量が AppMemoryUsageLevel 列挙値の高い値まで増加したときに発生します。 たとえば、Low から Medium まで増加した場合です。 このイベントの処理は省略可能ですが、制限未満に留まることについてはまだアプリに責任があるため、処理することをお勧めします。

MemoryManager.AppMemoryUsageDecreased は、アプリのメモリ消費量が AppMemoryUsageLevel 列挙値の低い値まで減少したときに発生します。 たとえば、High から Low まで減少した場合です。 このイベントの処理は省略可能ですが、処理することによって必要に応じてアプリで追加のメモリを割り当てられる可能性があることが示されます。

フォアグラウンドとバックグラウンドの間の移行の処理

アプリがフォアグラウンドからバックグラウンドに移動すると、EnteredBackground イベントが発生します。 アプリがフォアグラウンドに戻るときには、LeavingBackground イベントが発生します。 アプリの作成時にこれらのイベントのハンドラーを登録できます。 既定のプロジェクト テンプレートでは、これは、App.xaml.cs の App クラス コンストラクターで行われます。

バックグラウンドで実行すると、アプリが保持することを許可されているメモリ リソースが減少するため、AppMemoryUsageIncreasedAppMemoryUsageLimitChanging イベントについても登録する必要があります。これらは、アプリの現在のメモリ使用量と、現在の制限を確認するために使用できます。 これらのイベントのハンドラーを、次の例に示します。 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 ハンドラーによって、メモリ使用量が増加したときに現在の使用量を確認し、必要に応じて、メモリを解放する機会を得ることができます。

AppMemoryUsageLevelHigh または 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 は、バックグラウンドで実行されているアプリがメモリ使用量の制限を超えている場合にメモリを解放するために実装できるヘルパー メソッドです。 メモリを解放する方法はアプリの仕様によって異なりますが、推奨されるメモリ解放の方法の 1 つは、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();
}

ウィンドウのコンテンツが収集されると、各フレームでは、その切断プロセスが開始されます。 ビジュアル オブジェクト ツリーで、ウィンドウのコンテンツの下に Page がある場合、これらはその Unloaded イベントを発生を開始します。 Page は、Page へのすべての参照を削除しない限り、メモリから完全には消去できません。 Unloaded コールバックでは、メモリが直ちに解放されるように次の処理を行います。

  • Page 内の大規模なデータ構造体を消去して null に設定します。
  • Page 内でコールバック メソッドを持つすべてのイベント ハンドラーの登録を解除します。 Page の Loaded イベント ハンドラーで、これらのコールバックを確実に登録します。 Loaded イベントは、UI が再構築され、Page がビジュアル オブジェクト ツリーに追加されたときに発生します。
  • 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) を設定する必要があります。 次に、現在のウィンドウの Contentnull であるかどうかを確認します。バックグラウンドでの実行中にメモリを消去するためにアプリ ビューを破棄した場合は、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 ハンドラーのコードとほぼ同じです。 1 つ異なる点は、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 イベントを使用して、バックグラウンドで実行中のアプリのメモリ使用量を継続的に監視します。 AppMemoryUsageLevelHigh または OverLimit の場合は、リソースが解放されるようにします。
  • パフォーマンス最適化として、EnteredBackground ハンドラーではなく、AppMemoryUsageLimitChanging イベント ハンドラーで UI リソースを解放することを考慮します。 EnteredBackground/LeavingBackground イベント ハンドラーでブール値セットを使用し、アプリがバックグラウンドで動作しているかフォアグラウンドで動作しているかを追跡します。 次に、AppMemoryUsageLimitChanging イベント ハンドラーで、AppMemoryUsage が制限を超えていて、アプリがバックグラウンドで動作している場合は (ブール値に基づく)、UI リソースを解放します。
  • EnteredBackground イベントでは時間がかかる操作を実行しないでください。アプリケーション間の移行がユーザーに対して遅く表示される可能性があるためです。

バックグラウンドからフォアグラウンドへの移動

アプリがバックグラウンドからフォアグラウンドに移動すると、アプリはまず AppMemoryUsageLimitChanging イベントを受け取り、次に LeavingBackground イベントを受け取ります。

  • 必ずLeavingBackground イベントを使用して、バックグラウンドに移動したときに破棄した UI リソースを再作成します。
  • バックグラウンド メディア再生のサンプル - アプリがバックグラウンド状態に移行するときにメモリを解放する方法を説明します。
  • 診断ツール - 診断ツールを使用して、ガベージ コレクション イベントを監視し、予想される方法によりアプリでメモリが解放されているかどうかを検証します。