다음을 통해 공유


앱이 백그라운드로 이동할 때 메모리 회수

이 문서에서는 앱이 일시 중단되거나 종료되지 않도록 앱이 백그라운드 상태로 이동할 때 앱이 사용하는 메모리의 용량을 줄이는 방법을 보여줍니다.

새 백그라운드 이벤트

Windows 10 버전 1607에는 새로운 애플리케이션 수명 주기 이벤트 두 가지, 즉 EnteredBackgroundLeavingBackground가 도입되었습니다. 이 이벤트들은 앱이 백그라운드로 가거나 백그라운드에서 나올 시기를 앱에 알립니다.

앱이 백그라운드로 이동하면 시스템에서 적용하는 메모리 제약 조건이 변경될 수 있습니다. 앱이 백그라운드에 있는 동안 일시 중단되거나 종료되지 않도록, 이 이벤트들을 사용하여 현재의 메모리 사용량과 사용 가능한 리소스가 한도 미만으로 유지되는지 검사하세요.

앱의 메모리 사용량을 조절하기 위한 이벤트

앱에서 사용할 수 있는 메모리의 총 한도가 변경되기 직전에 MemoryManager.AppMemoryUsageLimitChanging이 발생합니다. 그 예로 앱이 백그라운드로 이동하면 Xbox의 메모리 한도가 1,024MB에서 128MB로 변경됩니다.
이 이벤트는 플랫폼이 앱을 일시 중단시키거나 종료시키지 못하게 하기 위해 처리해야 하는 가장 중요한 이벤트입니다.

AppMemoryUsageLevel 열거형에서 앱의 메모리 사용량이 더 높은 값으로 증가하면 MemoryManager.AppMemoryUsageIncreased가 발생합니다. 예를 들면 낮음에서 중간으로 증가할 때에 그러합니다. 이 이벤트의 처리는 선택 사항이지만, 한도 미만으로 유지해야 할 책임이 여전히 애플리케이션에 있으므로, 가급적 하는 것이 좋습니다.

AppMemoryUsageLevel 열거형에서 앱의 메모리 사용량이 더 낮은 값으로 감소하면 MemoryManager.AppMemoryUsageDecreased가 발생합니다. 예를 들면 높음에서 낮음으로 감소할 때에 그러합니다. 이 이벤트의 처리는 선택 사항이지만, 필요한 경우 애플리케이션이 추가 메모리를 할당할 수 있음을 나타냅니다.

포그라운드와 백그라운드 간 전환 처리하기

앱이 포그라운드에서 백그라운드로 이동하면 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높음 또는 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는 앱이 백그라운드에서 실행되는 동안 사용량 한도를 초과할 경우 메모리를 해제하도록 구현할 수 있는 도우미 메서드입니다. 메모리를 해제하는 방법은 앱의 세부 사항에 따라 달라집니다. 다만 메모리 용량을 확보하는 한 가지 권장 방법은 바로 UI 및 앱 뷰에 연결된 기타 리소스를 제거하는 것입니다. 이렇게 하려면 백그라운드 상태에서 실행 중인지 확인한 다음 앱 창의 콘텐츠 속성을 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();
}

창 콘텐츠가 수집되면 각 프레임이 연결 해제 프로세스를 시작합니다. 창 콘텐츠 하위의 시각적 개체 트리에 페이지가 있으면 해당 페이지에서 로드되지 않은 이벤트가 실행되기 시작합니다. 페이지에 대한 참조가 전부 제거되지 않으면 메모리에서 페이지를 완전히 지울 수 없습니다. 로드되지 않은 콜백에서 메모리가 신속하게 확보되도록 다음 작업을 수행하세요.

  • 페이지에서 대용량 데이터 구조를 지우고 null(으)로 설정하세요.
  • 페이지 내에 콜백 메서드가 있는 모든 이벤트 처리기의 등록을 취소하세요. 페이지에 대한 이벤트 처리기가 로드되는 동안 이들 콜백을 등록해야 합니다. 로드된 이벤트는 UI가 재구성되고 페이지가 시각적 개체 트리에 추가된 경우에 발생합니다.
  • 방금 null(으)로 설정한 대용량 데이터 구조를 하나라도 신속하게 가비지 수집하려면 로드되지 않은 콜백의 종료 시에 GC.Collect을(를) 호출하세요. 일반적으로 사용자를 위해 시스템이 가비지 컬렉션을 처리하므로 사용자가 이를 강제로 처리하지 않습니다. 이런 특정한 경우 시스템이 메모리를 회수하기 위해서는 앱이 종료되어야 한다고 판단할 가능성을 줄이기 위해 애플리케이션이 백그라운드로 이동할 때 애플리케이션에 청구되는 메모리 양을 줄입니다.
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 처리기 코드와 거의 동일합니다. 한 가지 차이점이 있다면 Launching 처리기는 LaunchActivatedEventArgsPreviousExecutionState 속성에서 이전 실행 상태를 확인하고, 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);
    }
}

지침

포그라운드에서 백그라운드로 이동하기

앱이 포그라운드에서 백그라운드로 이동하면 시스템이 앱을 대신하여 백그라운드에 필요 없는 리소스를 확보합니다. 그 예로 UI 프레임워크는 캐싱된 텍스처를 플러싱하고, 비디오 하위 시스템은 앱을 대신하여 할당된 메모리를 확보합니다. 다만 앱은 이때에도 시스템에서 일시 중단되거나 종료되지 않도록 메모리 사용량을 주의 깊게 모니터링해야 합니다.

앱이 포그라운드에서 백그라운드로 이동하면 먼저 EnteredBackground 이벤트가 발생하고, 이후에 AppMemoryUsageLimitChanging 이벤트가 발생합니다.

  • 백그라운드에서 실행되는 동안 앱에 필요 없다고 확인된 UI 리소스를 확보하려면 반드시 EnteredBackground 이벤트를 사용하세요. 예를 들면 노래의 커버 아트 이미지를 확보할 수 있습니다.
  • 앱이 새 백그라운드 한도보다 적은 메모리를 사용 중인지 확인하려면 반드시 AppMemoryUsageLimitChanging 이벤트를 사용하세요. 그렇지 않다면 리소스를 확보해야 합니다. 리소스를 확보하지 않으면 디바이스별 정책에 따라 앱이 일시 중단되거나 종료될 수 있습니다.
  • AppMemoryUsageLimitChanging 이벤트가 발생할 때 앱이 새 메모리 한도를 초과하면 반드시 가비지 수집기를 수동으로 호출하세요.
  • 앱의 메모리 사용량이 변경될 것으로 예상되는 경우 백그라운드에서 실행되는 동안 메모리 사용량을 계속 모니터링하려면 반드시 AppMemoryUsageIncreased 이벤트를 사용하세요. AppMemoryUsageLevel높음 또는 OverLimit이면 리소스를 확보해야 합니다.
  • 성능 최적화 수단으로 EnteredBackground 처리기 대신 AppMemoryUsageLimitChanging 이벤트 처리기에서 UI 리소스를 확보하는 것이 좋습니다. 앱이 백그라운드에 있는지, 아니면 포그라운드에 있는지 추적하려면 EnteredBackground/LeavingBackground 이벤트 처리기에서 설정된 부울 값을 사용하세요. 이후 AppMemoryUsageLimitChanging 이벤트 처리기에서 AppMemoryUsage가 한도를 초과했으며 앱이 백그라운드에 있다면(부울 값 기준) UI 리소스를 확보할 수 있습니다.
  • EnteredBackground 이벤트에서 장기 작업을 실행하지 마세요. 애플리케이션 간 전환이 사용자에게 천천히 표시될 수 있습니다.

백그라운드에서 포그라운드로 이동하기

앱이 백그라운드에서 포그라운드로 이동하면 먼저 AppMemoryUsageLimitChanging 이벤트가 발생하고, 이후에 LeavingBackground 이벤트가 발생합니다.

  • 백그라운드로 이동할 때 앱에서 제거된 UI 리소스를 다시 생성하려면 반드시 LeavingBackground 이벤트를 사용하세요.
  • 백그라운드 미디어 재생 샘플 - 앱이 백그라운드 상태로 이동한 경우 메모리를 확보하는 방법을 보여줍니다.
  • 진단 도구 - 진단 도구를 사용하면 가비지 수집 이벤트를 관찰하고, 앱이 예상대로 메모리를 해제하고 있는지 검증할 수 있습니다.