앱 성능 향상

앱 성능 저하는 여러 가지 면에서 나타납니다. 앱이 응답하지 않는 것처럼 보이고, 스크롤 속도가 느려질 수 있으며, 디바이스 배터리 수명을 줄일 수 있습니다. 그러나 성능을 최적화하려면 효율적인 코드를 구현하는 것 이상이 필요합니다. 앱 성능에 대한 사용자의 환경도 고려해야 합니다. 예를 들어 사용자가 다른 활동을 수행하지 못하도록 차단하지 않고 작업을 실행하면 사용자 환경을 향상시키는 데 도움이 될 수 있습니다.

.NET 다중 플랫폼 앱 UI(.NET MAUI) 앱의 성능 및 인식된 성능을 높이기 위한 많은 기술이 있습니다. 전체적으로 이러한 기술은 CPU에서 수행하는 작업의 양과 앱에서 사용하는 메모리 양을 크게 줄일 수 있습니다.

프로파일러 사용

앱을 개발할 때는 프로파일링된 후에만 코드를 최적화하는 것이 중요합니다. 프로파일링은 코드 최적화가 성능 문제를 줄이는 데 가장 큰 영향을 주는지를 확인하는 기술입니다. 프로파일러가 앱의 메모리 사용량을 추적하고 앱에서 메서드의 실행 시간을 기록합니다. 이 데이터는 최적화를 위한 최상의 기회를 검색할 수 있도록 앱의 실행 경로와 코드의 실행 비용을 탐색하는 데 도움이 됩니다.

.NET MAUI 앱은 Android, iOS, Mac 및 Windows 및 Windows의 PerfView를 사용하여 dotnet-trace 프로파일러할 수 있습니다. 자세한 내용은 .NET MAUI 앱 프로파일링을 참조 하세요.

앱을 프로파일링할 때 다음과 같은 모범 사례를 사용하는 것이 좋습니다.

  • 시뮬레이터가 앱 성능을 왜곡할 수 있으므로 시뮬레이터에서 앱을 프로파일링하지 않습니다.
  • 하나의 디바이스에서 성능 측정을 수행하더라도 다른 디바이스의 성능 특성을 표시하지 않기 때문에 이상적으로 다양한 디바이스에서 프로파일링을 수행할 수 있어야 합니다. 그러나 프로파일링은 최소한 가장 낮은 사양이 예상되는 디바이스에서 수행되어야 합니다.
  • 다른 모든 앱을 닫아 다른 앱이 아닌 프로파일딩되는 앱의 전체 영향을 측정하도록 합니다.

컴파일된 바인딩 사용

컴파일된 바인딩은 리플렉션을 사용하여 런타임이 아닌 컴파일 시간에 바인딩 식을 확인하여 .NET MAUI 앱에서 데이터 바인딩 성능을 향상시킵니다. 바인딩 식을 컴파일하면 일반적으로 클래식 바인딩을 사용하는 것보다 8~20배 더 빠르게 해결하는 컴파일된 코드가 생성됩니다. 자세한 내용은 컴파일된 바인딩을 참조 하세요.

불필요한 바인딩 줄이기

정적으로 쉽게 설정할 수 있는 콘텐츠에 바인딩을 사용하지 마세요. 바인딩할 필요가 없는 데이터를 바인딩할 경우 어떠한 이점도 없습니다. 바인딩은 비용 효율적이지 않기 때문입니다. 예를 들어, Button.Text = "Accept"를 설정하는 것은 "Accept" 값을 사용하여 viewmodel string 속성에 Button.Text를 바인딩하는 것보다 오버헤드가 적습니다.

올바른 레이아웃 선택

여러 자식 요소를 표시할 수 있지만 단일 자식만 있어 불필요한 레이아웃입니다. 예를 들어 다음 예제에서는 단일 자식이 VerticalStackLayout 있는 것을 보여줍니다.

<ContentPage xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
             xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
             x:Class="MyMauiApp.MainPage">
    <VerticalStackLayout>
        <Image Source="waterfront.jpg" />
    </VerticalStackLayout>
</ContentPage>

이는 낭비되며 다음 예제와 VerticalStackLayout 같이 요소를 제거해야 합니다.

<ContentPage xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
             xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
             x:Class="MyMauiApp.MainPage">
    <Image Source="waterfront.jpg" />
</ContentPage>

또한 다른 레이아웃의 조합을 사용하여 특정 레이아웃의 모양을 재현하려고 하지 마세요. 불필요한 레이아웃 계산이 수행됩니다. 예를 들어 요소 조합을 사용하여 HorizontalStackLayout 레이아웃을 Grid 재현하지 마세요. 다음 예제에서는 이 잘못된 사례의 예를 보여줍니다.

<ContentPage xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
             xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
             x:Class="MyMauiApp.MainPage">
    <VerticalStackLayout>
        <HorizontalStackLayout>
            <Label Text="Name:" />
            <Entry Placeholder="Enter your name" />
        </HorizontalStackLayout>
        <HorizontalStackLayout>
            <Label Text="Age:" />
            <Entry Placeholder="Enter your age" />
        </HorizontalStackLayout>
        <HorizontalStackLayout>
            <Label Text="Occupation:" />
            <Entry Placeholder="Enter your occupation" />
        </HorizontalStackLayout>
        <HorizontalStackLayout>
            <Label Text="Address:" />
            <Entry Placeholder="Enter your address" />
        </HorizontalStackLayout>
    </VerticalStackLayout>
</ContentPage>

불필요한 레이아웃 계산이 수행되기 때문에 이는 불필요합니다. 대신 다음 예제와 같이 원하는 레이아웃을 Grid사용하여 더 효율적으로 수행할 수 있습니다.

<ContentPage xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
             xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
             x:Class="MyMauiApp.MainPage">
    <Grid ColumnDefinitions="100,*"
          RowDefinitions="30,30,30,30">
        <Label Text="Name:" />
        <Entry Grid.Column="1"
               Placeholder="Enter your name" />
        <Label Grid.Row="1"
               Text="Age:" />
        <Entry Grid.Row="1"
               Grid.Column="1"
               Placeholder="Enter your age" />
        <Label Grid.Row="2"
               Text="Occupation:" />
        <Entry Grid.Row="2"
               Grid.Column="1"
               Placeholder="Enter your occupation" />
        <Label Grid.Row="3"
               Text="Address:" />
        <Entry Grid.Row="3"
               Grid.Column="1"
               Placeholder="Enter your address" />
    </Grid>
</ContentPage>

이미지 리소스 최적화

이미지는 앱에서 사용하는 가장 비용이 많이 드는 리소스 중 일부이며 종종 고해상도로 캡처됩니다. 이렇게 하면 생동감 넘치는 이미지가 세부 정보로 가득 차 있지만, 이러한 이미지를 표시하는 앱은 일반적으로 이미지를 디코딩하기 위해 더 많은 CPU 사용량과 디코딩된 이미지를 저장하기 위해 더 많은 메모리가 필요합니다. 표시하기 위해 더 작은 크기로 축소하는 경우 메모리에서 고해상도 이미지를 디코딩할 필요가 없습니다. 대신, 예측된 표시 크기에 가까운 저장된 이미지 버전을 만들어 CPU 사용량 및 메모리 공간을 줄입니다. 예를 들어 목록 보기에 표시되는 이미지는 전체 화면에 표시되는 이미지보다 해상도가 더 낮을 가능성이 높습니다.

또한 필요한 경우에만 이미지를 만들어야 하며 앱에 더 이상 필요하지 않은 즉시 릴리스해야 합니다. 예를 들어 앱이 스트림에서 해당 데이터를 읽어 이미지를 표시하는 경우 필요한 경우에만 스트림이 생성되었는지 확인하고 더 이상 필요하지 않을 때 스트림이 해제되었는지 확인합니다. 이는 페이지가 생성될 때나 Page.Appearing 이벤트가 실행될 때 스트림을 만든 후 Page.Disappearing 이벤트가 실행될 때 스트림을 폐기하는 방식으로 구현할 수 있습니다.

메서드를 ImageSource.FromUri(Uri) 사용하여 표시할 이미지를 다운로드할 때 다운로드한 이미지가 적절한 시간 동안 캐시되었는지 확인합니다. 자세한 내용은 이미지 캐싱을 참조 하세요.

시각적 트리 크기 줄이기

페이지의 요소 수를 줄이면 페이지 렌더러의 속도가 더 빨라집니다. 이는 크게 두 가지 방법으로 구현할 수 있습니다. 첫 번째는 표시되지 않는 요소를 숨기는 것입니다. 각 요소의 IsVisible 속성은 해당 요소가 시각적 트리의 일부가 되어야 하는지 여부를 결정합니다. 따라서 요소가 다른 요소 뒤에 숨겨져 있어 표시되지 않을 경우 해당 요소를 제거하거나 IsVisible 속성을 false로 설정합니다.

두 번째 방법은 불필요한 요소를 제거하는 것입니다. 예를 들어 다음은 여러 Label 요소가 포함된 페이지 레이아웃을 보여줍니다.

<VerticalStackLayout>
    <VerticalStackLayout Padding="20,20,0,0">
        <Label Text="Hello" />
    </VerticalStackLayout>
    <VerticalStackLayout Padding="20,20,0,0">
        <Label Text="Welcome to the App!" />
    </VerticalStackLayout>
    <VerticalStackLayout Padding="20,20,0,0">
        <Label Text="Downloading Data..." />
    </VerticalStackLayout>
</VerticalStackLayout>

다음 예제와 같이 동일한 페이지 레이아웃을 기본 감소된 요소 수로 지정할 수 있습니다.

<VerticalStackLayout Padding="20,35,20,20"
                     Spacing="25">
    <Label Text="Hello" />
    <Label Text="Welcome to the App!" />
    <Label Text="Downloading Data..." />
</VerticalStackLayout>

애플리케이션 리소스 사전 크기 줄이기

중복을 방지하려면 앱 전체에서 사용되는 모든 리소스를 앱의 리소스 사전에 저장해야 합니다. 이렇게 하면 앱 전체에서 구문 분석해야 하는 XAML의 양을 줄이는 데 도움이 됩니다. 다음 예제에서는 앱 전체에서 HeadingLabelStyle 사용되는 리소스를 보여 하며 앱의 리소스 사전에 정의됩니다.

<Application xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
             xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
             x:Class="MyMauiApp.App">
     <Application.Resources>
        <Style x:Key="HeadingLabelStyle"
               TargetType="Label">
            <Setter Property="HorizontalOptions"
                    Value="Center" />
            <Setter Property="FontSize"
                    Value="Large" />
            <Setter Property="TextColor"
                    Value="Red" />
        </Style>
     </Application.Resources>
</Application>

그러나 페이지와 관련된 XAML은 앱의 리소스 사전에 포함되어서는 안 됩니다. 리소스는 페이지에서 요구하는 경우 대신 앱 시작 시 구문 분석되므로 안 됩니다. 시작 페이지가 아닌 페이지에서 리소스를 사용하는 경우 해당 페이지의 리소스 사전에 배치해야 하므로 앱이 시작될 때 구문 분석되는 XAML을 줄일 수 있습니다. 다음 예제에서는 단일 페이지에만 있는 리소스를 보여 HeadingLabelStyle 하며 페이지의 리소스 사전에 정의되어 있습니다.

<ContentPage xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
             xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
             x:Class="MyMauiApp.MainPage">
    <ContentPage.Resources>
        <Style x:Key="HeadingLabelStyle"
                TargetType="Label">
            <Setter Property="HorizontalOptions"
                    Value="Center" />
            <Setter Property="FontSize"
                    Value="Large" />
            <Setter Property="TextColor"
                    Value="Red" />
        </Style>
    </ContentPage.Resources>
    ...
</ContentPage>

앱 리소스에 대한 자세한 내용은 XAML을 사용하는 스타일 앱을 참조하세요.

앱 크기 줄이기

.NET MAUI가 앱을 빌드할 때 ILLink라는 링커를 사용하여 앱의 전체 크기를 줄일 수 있습니다. ILLink는 컴파일러에서 생성된 중간 코드를 분석하여 크기를 줄입니다. 사용되지 않는 메서드, 속성, 필드, 이벤트, 구조체 및 클래스를 제거하여 앱을 실행하는 데 필요한 코드 및 어셈블리 종속성만 포함하는 앱을 생성합니다.

링커 동작을 구성하는 방법에 대한 자세한 내용은 Android 앱 연결, iOS 앱 연결 및 Mac Catalyst 앱 연결을 참조하세요.

앱 활성화 기간 줄이기

모든 앱에는 앱이 시작된 시간과 앱이 사용할 준비가 된 시점 사이의 시간인 활성화 기간이 있습니다. 이 활성화 기간은 사용자에게 앱에 대한 첫 인상을 제공하므로 앱에 대한 유리한 첫 인상을 얻기 위해서는 활성화 기간과 사용자 인식을 줄이는 것이 중요합니다.

앱이 초기 UI를 표시하기 전에 사용자에게 앱이 시작 중임을 나타내는 시작 화면을 제공해야 합니다. 앱이 초기 UI를 빠르게 표시할 수 없는 경우 시작 화면을 사용하여 활성화 기간 동안 진행 상황을 사용자에게 알리고 앱이 중단되지 않았다는 확신을 제공해야 합니다. 이 작업은 진행률 표시줄 또는 비슷한 컨트롤에 있을 수 있습니다.

활성화 기간 동안 앱은 리소스의 로드 및 처리를 포함하는 활성화 논리를 실행합니다. 필수 리소스를 원격으로 검색하는 대신 앱 내에서 패키지되도록 하여 정품 인증 기간을 줄일 수 있습니다. 예를 들어 어떤 경우에는 정품 인증 기간 동안 로컬로 저장된 자리 표시자 데이터를 로드하는 것이 적절할 수 있습니다. 그런 다음, 초기 UI가 표시되고 사용자가 해당 앱과 상호 작용할 수 있게 되면 원격 원본에서 자리 표시자 데이터를 점진적으로 바꿀 수 있습니다. 또한 앱의 활성화 논리는 사용자가 앱을 사용하기 시작할 수 있도록 하는 데 필요한 작업만 수행해야 합니다. 어셈블리가 처음으로 사용될 때 로드되면 추가 어셈블리를 로드하는 작업이 지연되는 경우 도움이 될 수 있습니다.

종속성 주입 컨테이너를 신중하게 선택

종속성 주입 컨테이너는 모바일 앱에 추가 성능 제약 조건을 도입합니다. 컨테이너를 사용하여 형식을 등록하고 확인하는 경우, 특히 앱의 각 페이지 탐색에 대한 종속성을 다시 생성하는 경우 컨테이너의 리플렉션 사용으로 인해 성능비용이 발생합니다. 종속성이 많거나 깊은 경우에는 생성 비용이 많이 증가할 수 있습니다. 또한 일반적으로 앱 시작 중에 발생하는 형식 등록은 사용되는 컨테이너에 따라 시작 시간에 눈에 띄는 영향을 미칠 수 있습니다. .NET MAUI 앱의 종속성 주입에 대한 자세한 내용은 종속성 주입을 참조하세요.

또는 팩터리를 사용하여 수동으로 구현하여 종속성 주입을 보다 효율적으로 만들 수 있습니다.

Shell 앱 만들기

.NET MAUI Shell 앱은 플라이아웃 및 탭에 따라 의견 탐색 환경을 제공합니다. 셸을 사용하여 앱 사용자 환경을 구현할 수 있는 경우 이 작업을 수행하는 것이 좋습니다. 셸 앱은 앱 시작 시가 아닌 탐색에 대한 응답으로 요청 시 페이지가 생성되므로 시작 환경이 저하되는 것을 방지하는 데 도움이 됩니다. 이는 앱을 사용하는 TabbedPage앱에서 발생합니다. 자세한 내용은 Shell 개요를 참조하세요.

ListView 성능 최적화

ListView를 사용할 때 최적화해야 하는 여러 가지 사용자 환경이 있습니다.

  • 초기화 - 컨트롤을 만들 때부터 항목이 화면에 표시될 때까지의 시간 간격입니다.
  • 스크롤 - 목록을 스크롤하는 기능이며, UI가 터치 제스처보다 지연되지 않도록 합니다.
  • 상호 작용 - 항목을 추가, 삭제 및 선택합니다.

이 컨트롤을 ListView 사용하려면 앱에서 데이터 및 셀 템플릿을 제공해야 합니다. 이것이 구현되는 방식은 컨트롤의 성능에 큰 영향을 줍니다. 자세한 내용은 캐시 데이터를 참조하세요.

비동기 프로그래밍 사용

비동기 프로그래밍을 사용하여 앱의 전반적인 응답성을 향상하고 성능 병목 상태를 방지할 수 있습니다. .NET에서 TAP(작업 기반 비동기 패턴)은 비동기 작업에 권장되는 디자인 패턴입니다. 그러나 TAP을 잘못 사용하면 앱의 성능이 저하될 수 있습니다.

기본 사항

TAP을 사용할 때는 다음과 같은 일반적인 지침을 따라야 합니다.

  • TaskStatus 열거형으로 표시되는 작업 수명 주기를 이해합니다. 자세한 내용은 TaskStatus의 의미작업 상태를 참조하세요.
  • Task.WhenAll 메서드를 사용하여 일련의 비동기 작업을 개별적으로 await하는 것이 아니라 여러 비동기 작업이 완료될 때까지 비동기적으로 대기합니다. 자세한 내용은 Task.WhenAll을 참조하세요.
  • Task.WhenAny 메서드를 사용하여 여러 비동기 작업 중 하나가 완료될 때까지 비동기적으로 대기합니다. 자세한 내용은 Task.WhenAny를 참조하세요.
  • Task.Delay 메서드를 사영하여 지정된 시간 이후에 완료되는 Task 개체를 생성합니다. 이는 데이터 폴링 및 미리 정해진 시간 동안 사용자 입력 처리 지연 등의 시나리오에 유용합니다. 자세한 내용은 Task.Delay를 참조하세요.
  • Task.Run 메서드를 사용하여 스레드 풀에서 집약적 동기 CPU 작업을 실행합니다. 이 메서드는 가장 최적의 인수가 설정된 TaskFactory.StartNew 메서드의 바로 가기입니다. 자세한 내용은 Task.Run을 참조하세요.
  • 비동기 생성자를 만들지 마십시오. 대신, 수명 주기 이벤트 또는 별도의 초기화 논리를 사용하여 초기화를 올바르게 await합니다. 자세한 내용은 blog.stephencleary.com에서 비동기 생성자를 참조하세요.
  • 지연 작업 패턴을 사용하여 앱 시작 중에 비동기 작업이 완료되는 것을 기다리지 않도록 합니다. 자세한 내용은 AsyncLazy를 참조하세요.
  • TaskCompletionSource<T> 개체를 만들어 TAP을 사용하지 않는 기존 비동기 작업에 대한 작업 래퍼를 만듭니다. 이 개체를 사용하여 Task 프로그래밍의 이점을 얻고 관련된 Task의 수명과 완료를 제어할 수 있도록 합니다. 자세한 내용은 TaskCompletionSource의 특성을 참조하세요.
  • 비동기 작업의 결과를 처리할 필요가 없는 경우 대기 Task 개체를 반환하는 대신, Task 개체를 반환합니다. 이렇게 하면 컨텍스트 전환이 수행되지 않으므로 성능이 향상됩니다.
  • 사용 가능한 경우 데이터 처리와 같은 시나리오에서 또는 비동기적으로 서로 통신해야 하는 작업이 많은 경우에 TPL(작업 병렬 라이브러리) 데이터 흐름 라이브러리를 사용합니다. 자세한 내용은 데이터 흐름(작업 병렬 라이브러리)을 참조하세요.

UI

TAP을 UI 컨트롤과 함께 사용할 때는 다음 지침을 따라야 합니다.

  • 사용할 수 있는 경우, API의 비동기 버전을 호출합니다. 이렇게 하면 UI 스레드가 차단 해제되어 앱에 대한 사용자의 환경을 개선하는 데 도움이 됩니다.

  • UI 요소를 UI 스레드에 대한 비동기 작업의 데이터로 업데이트하여 예외가 throw되지 않도록 합니다. 그러나 ListView.ItemsSource 속성의 업데이트는 자동으로 UI 스레드로 마샬링됩니다. 코드가 UI 스레드에서 실행되고 있는지 확인하는 방법에 대한 자세한 내용은 UI 스레드에서 스레드 만들기를 참조하세요.

    Important

    데이터 바인딩을 통해 업데이트되는 모든 컨트롤 속성은 자동으로 UI 스레드로 마샬링됩니다.

오류 처리

TAP을 사용하는 경우 다음 오류 처리 지침을 따라야 합니다.

  • 비동기 예외 처리에 대해 알아봅니다. 비동기적으로 실행되는 코드에 의해 throw된 처리되지 않은 예외는 특정 시나리오를 제외하고는 호출 스레드로 다시 전파됩니다. 자세한 내용은 예외 처리(작업 병렬 라이브러리)를 참조하세요.
  • async void 메서드를 만들지 말고 대신 async Task 메서드를 만듭니다. 이를 통해 오류 처리, 구성 가능성 및 테스트 가능성을 보다 손쉽게 사용할 수 있습니다. 이 지침의 예외는 void를 반환해야 하는 비동기 이벤트 처리기입니다. 자세한 내용은 비동기 Void 방지를 참조하세요.
  • 교착 상태가 발생할 수 있으므로 Task.Wait, Task.Result 또는 GetAwaiter().GetResult 메서드를 호출하여 차단 및 비동기 코드를 혼합하지 마세요. 그러나 이 지침을 위반해야 하는 경우, 작업 예외를 유지하기 때문에 GetAwaiter().GetResult 메서드를 호출하는 것이 좋습니다. 자세한 내용은 Async All the Way(계속 비동기) 및 Task Exception Handling in .NET 4.5(.NET 4.5의 작업 예외 처리)를 참조하세요.
  • 가능하면 항상 ConfigureAwait 메서드를 사용하여 컨텍스트를 구별하지 않는 코드를 만듭니다. 컨텍스트 없는 코드는 모바일 앱의 성능이 향상되며 부분적으로 비동기 코드베이스로 작업할 때 교착 상태를 방지하는 데 유용한 기술입니다. 자세한 내용은 컨텍스트 구성을 참조하세요.
  • 이전 비동기 작업에서 throw된 예외를 처리하는 기능 및 연속 작업 시작 전에 또는 실행 중에 해당 작업을 취소하는 기능에 연속 작업을 사용합니다. 자세한 내용은 연속 작업을 사용하여 작업 연결을 참조하세요.
  • 비동기 작업이 ICommand에서 호출될 때 비동기 ICommand 구현을 사용합니다. 이렇게 하면 비동기 명령 논리의 모든 예외를 처리할 수 있습니다. 자세한 내용은 비동기 프로그래밍: 비동기 MVVM 애플리케이션 패턴: 명령을 참조하세요.

개체 만들기 비용 지연

초기화 지연은 개체를 처음 사용할 때까지 생성을 지연하는 데 사용될 수 있습니다. 이 방법을 통해 기본적으로 성능을 향상시키고, 계산을 방지하고, 메모리 요구 사항을 줄입니다.

다음 시나리오에서 만드는 데 비용이 많이 드는 개체에 지연 초기화를 사용하는 것이 좋습니다.

  • 앱에서 개체를 사용하지 않을 수 있습니다.
  • 다른 비용이 많이 드는 작업은 개체를 만들기 전에 완료되어야 합니다.

클래스 Lazy<T> 는 다음 예제와 같이 지연 초기화된 형식을 정의하는 데 사용됩니다.

void ProcessData(bool dataRequired = false)
{
    Lazy<double> data = new Lazy<double>(() =>
    {
        return ParallelEnumerable.Range(0, 1000)
                     .Select(d => Compute(d))
                     .Aggregate((x, y) => x + y);
    });

    if (dataRequired)
    {
        if (data.Value > 90)
        {
            ...
        }
    }
}

double Compute(double x)
{
    ...
}

처음으로 Lazy<T>.Value 속성에 액세스할 때 초기화 지연이 발생합니다. 처음 액세스할 때 래핑된 형식이 생성되고 반환되며 나중에 액세스하는 데 사용하기 위해 저장됩니다.

초기화 지연에 대한 자세한 내용은 초기화 지연을 참조하세요.

IDisposable 리소스 릴리스

IDisposable 인터페이스는 리소스를 릴리스하는 메커니즘을 제공합니다. 명시적으로 리소스를 릴리스하기 위해 구현해야 하는 Dispose 메서드를 제공합니다. IDisposable은 소멸자가 아니며 다음과 같은 경우에만 구현되어야 합니다.

  • 클래스가 관리되지 않는 리소스를 소유하는 경우 릴리스해야 하는 일반적인 관리되지 않는 리소스에는 파일, 스트림 및 네트워크 연결이 포함됩니다.
  • 클래스가 관리 IDisposable 리소스를 소유하는 경우

인스턴스가 더 이상 필요하지 않은 경우 형식 소비자는 IDisposable.Dispose 구현을 호출하여 리소스를 해제할 수 있습니다. 이를 위한 두 가지 방법이 있습니다.

  • using 문에서 IDisposable 개체를 래핑합니다.
  • try/finally 블록에서 IDisposable.Dispose에 대한 호출을 래핑합니다.

using 문에서 IDisposable 개체 래핑

다음 예제에서는 문에서 using 개체를 래핑하는 IDisposable 방법을 보여줍니다.

public void ReadText(string filename)
{
    string text;
    using (StreamReader reader = new StreamReader(filename))
    {
        text = reader.ReadToEnd();
    }
    ...
}

StreamReader 클래스는 IDisposable을 구현하고, using 문은 범위를 벗어나기 전에 StreamReader 개체에서 StreamReader.Dispose 메서드를 호출하는 편리한 구문을 제공합니다. using 블록 내에서 StreamReader 개체는 읽기 전용이며 다시 할당할 수 없습니다. 또한 using 문을 사용하면 컴파일러가 try/finally 블록에서 IL(중간 언어)을 구현하므로 예외가 발생하는 경우에도 Dispose 메서드를 호출할 수 있습니다.

try/finally 블록에서 IDisposable.Dispose에 대한 호출 래핑

다음 예제에서는 블록에서 try/finally 호출을 래핑하는 IDisposable.Dispose 방법을 보여줍니다.

public void ReadText(string filename)
{
    string text;
    StreamReader reader = null;
    try
    {
        reader = new StreamReader(filename);
        text = reader.ReadToEnd();
    }
    finally
    {
        if (reader != null)
            reader.Dispose();
    }
    ...
}

StreamReader 클래스는 IDisposable을 구현하고, finally 블록은 StreamReader.Dispose 메서드를 호출하여 리소스를 릴리스합니다. 자세한 내용은 IDisposable 인터페이스를 참조하세요.

이벤트 구독 취소

메모리 누수를 방지하려면 구독자 개체를 삭제하기 전에 이벤트 구독을 취소해야 합니다. 이벤트 구독을 취소할 때까지 게시 개체에서 이벤트의 대리자에는 구독자의 이벤트 처리기를 캡슐화하는 대리자에 대한 참조가 있습니다. 게시 개체에 해당 참조가 있다면 가비지 수집은 구독자 개체 메모리를 회수하지 않습니다.

다음 예제에서는 이벤트에서 구독을 취소하는 방법을 보여 줍니다.

public class Publisher
{
    public event EventHandler MyEvent;

    public void OnMyEventFires()
    {
        if (MyEvent != null)
            MyEvent(this, EventArgs.Empty);
    }
}

public class Subscriber : IDisposable
{
    readonly Publisher _publisher;

    public Subscriber(Publisher publish)
    {
        _publisher = publish;
        _publisher.MyEvent += OnMyEventFires;
    }

    void OnMyEventFires(object sender, EventArgs e)
    {
        Debug.WriteLine("The publisher notified the subscriber of an event");
    }

    public void Dispose()
    {
        _publisher.MyEvent -= OnMyEventFires;
    }
}

Subscriber 클래스가 해당 Dispose 메서드의 이벤트 구독을 해제합니다.

람다 식에서 개체를 참조하고 활성 상태로 유지할 수 있으므로 참조 주기는 이벤트 처리기 및 람다 구문을 사용하는 경우에 발생할 수 있습니다. 따라서 다음 예제와 같이 익명 메서드에 대한 참조를 필드에 저장하고 이벤트에서 구독을 취소하는 데 사용할 수 있습니다.

public class Subscriber : IDisposable
{
    readonly Publisher _publisher;
    EventHandler _handler;

    public Subscriber(Publisher publish)
    {
        _publisher = publish;
        _handler = (sender, e) =>
        {
            Debug.WriteLine("The publisher notified the subscriber of an event");
        };
        _publisher.MyEvent += _handler;
    }

    public void Dispose()
    {
        _publisher.MyEvent -= _handler;
    }
}

_handler 필드는 무명 메서드에 대한 참조를 유지 관리하고 이벤트를 구독하고 구독을 취소하는 데 사용됩니다.

iOS 및 Mac Catalyst에서 강력한 순환 참조 방지

상황에 따라 개체에서 가비지 수집기를 통해 메모리를 회수하지 못하도록 방지할 수 있는 강력한 참조 순환을 만들 수도 있습니다. 예를 들어 다음 예제와 같이 상속되는 클래스와 같은 파생 서브클래스가 UIView-derived 컨테이너에 Objective-C추가NSObject되고 강력하게 참조되는 경우NSObject를 고려합니다.

class Container : UIView
{
    public void Poke()
    {
        // Call this method to poke this object
    }
}

class MyView : UIView
{
    Container _parent;

    public MyView(Container parent)
    {
        _parent = parent;
    }

    void PokeParent()
    {
        _parent.Poke();
    }
}

var container = new Container();
container.AddSubview(new MyView(container));

이 코드에서 Container 인스턴스를 만들면 C# 개체가 Objective-C 개체에 대한 강력한 참조를 갖게 됩니다. 마찬가지로, MyView 인스턴스도 Objective-C 개체에 대한 강력한 참조를 갖게 됩니다.

또한 container.AddSubview를 호출하면 관리되지 않는 MyView 인스턴스의 참조 횟수도 증가합니다. 이 경우 .NET iOS 런타임은 관리되는 개체가 해당 개체에 대한 참조를 유지한다는 보장이 없으므로 관리 코드에서 개체를 활성 상태로 유지하는 MyView 인스턴스를 만듭니다GCHandle. 관리 코드 관점에서 AddSubview(UIView) 호출이 GCHandle에 대한 호출이 아닌 경우 MyView 개체가 회수됩니다.

관리되지 않는 MyView 개체에는 강력한 링크로 알려진 관리 개체를 가리키는 GCHandle이 있습니다. 관리 개체에는 Container 인스턴스에 대한 참조가 포함됩니다. 이에 따라 Container 인스턴스는 MyView 개체에 대한 관리 참조를 갖게 됩니다.

포함된 개체가 해당 컨테이너에 대한 링크를 유지하는 경우에는 순환 참조를 처리할 수 있는 몇 가지 옵션이 있습니다.

  • 컨테이너에 대한 약한 참조를 유지하여 순환 참조를 방지합니다.
  • 개체에 대해 Dispose를 호출합니다.
  • 컨테이너에 대한 링크를 null로 설정하여 순환을 수동으로 끊습니다.
  • 컨테이너에서 포함된 개체를 수동으로 제거합니다.

약한 참조 사용

순환을 방지하기 위해 한 가지 방법은 자식에서 부모까지 약한 참조를 사용하는 것입니다. 예를 들어 위의 코드는 다음 예제와 같이 표시될 수 있습니다.

class Container : UIView
{
    public void Poke()
    {
        // Call this method to poke this object
    }
}

class MyView : UIView
{
    WeakReference<Container> _weakParent;

    public MyView(Container parent)
    {
        _weakParent = new WeakReference<Container>(parent);
    }

    void PokeParent()
    {
        if (weakParent.TryGetTarget (out var parent))
            parent.Poke();
    }
}

var container = new Container();
container.AddSubview(new MyView container));

여기에서 포함된 개체는 부모를 활성 상태로 유지합니다. 그러나 부모는 호출을 통해 자식이 활성 상태로 유지됩니다 container.AddSubView.

이는 피어 클래스에 구현이 포함된 대리자 또는 데이터 원본 패턴을 사용하는 iOS API에서도 발생합니다. 예를 들어 클래스에서 Delegate 속성 또는 DataSource 속성을 설정할 때입니다 UITableView .

하위 클래스를 만드는 대신 수행할 수 있는 작업인 IUITableViewDataSource와 같이 프로토콜을 구현하기 위해 순전히 만들어진 클래스의 경우, 클래스에서 인터페이스를 구현하고, 메서드를 재정의하고, DataSource 속성을 this에 할당할 수 있습니다.

강력한 참조를 사용하여 개체 삭제

강력한 참조가 있고 종속성을 제거하기 어려우면 Dispose 메서드에서 부모 포인터를 제거합니다.

컨테이너의 경우 다음 예제와 같이 포함된 개체를 제거하도록 메서드를 재정 Dispose 의합니다.

class MyContainer : UIView
{
    public override void Dispose()
    {
        // Brute force, remove everything
        foreach (var view in Subviews)
        {
              view.RemoveFromSuperview();
        }
        base.Dispose();
    }
}

부모에 대한 강력한 참조를 유지하는 자식 개체의 경우 Dispose 구현에서 부모에 대한 참조를 제거합니다.

class MyChild : UIView
{
    MyContainer _container;

    public MyChild(MyContainer container)
    {
        _container = container;
    }

    public override void Dispose()
    {
        _container = null;
    }
}