앱 성능 향상
앱 성능 저하는 여러 가지 면에서 나타납니다. 앱이 응답하지 않는 것처럼 보이고, 스크롤 속도가 느려질 수 있으며, 디바이스 배터리 수명을 줄일 수 있습니다. 그러나 성능 최적화에는 효율적인 코드를 구현하는 것 이상의 작업이 포함됩니다. 앱 성능에 대한 사용자의 경험도 고려해야 합니다. 예를 들어 사용자가 다른 작업을 수행하지 못하도록 차단하지 않고 작업을 실행하도록 하면 사용자 환경을 개선하는 데 도움이 될 수 있습니다.
.NET 다중 플랫폼 앱 UI(.NET MAUI) 앱의 성능 및 인식된 성능을 높이기 위한 많은 기술이 있습니다. 전체적으로 이러한 기술은 CPU에서 수행하는 작업의 양과 앱에서 사용하는 메모리 양을 크게 줄일 수 있습니다.
프로파일러 사용
앱을 개발할 때는 프로파일링된 후에만 코드를 최적화하는 것이 중요합니다. 프로파일링은 코드 최적화가 성능 문제를 줄이는 데 가장 큰 영향을 미치는 위치를 결정하는 기술입니다. 프로파일러가 앱의 메모리 사용량을 추적하고 앱에서 메서드의 실행 시간을 기록합니다. 이 데이터는 최적화를 위한 최상의 기회를 검색할 수 있도록 앱의 실행 경로와 코드의 실행 비용을 탐색하는 데 도움이 됩니다.
.NET MAUI 앱은 Android, iOS, Mac, Windows에서는 dotnet-trace
을, Windows에서는 PerfView를 사용하여 프로파일링할 수 있습니다. 자세한 내용은 .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
설정합니다. 요소의 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
열거형으로 표시되는 작업 수명 주기를 이해합니다. 자세한 내용은 작업 상태 및 작업 상태를 참조하십시오. - 여러 비동기 작업이 완료될 때까지 비동기적으로 기다리기 위해 개별 비동기 작업을
await
하지 않고Task.WhenAll
메서드를 사용합니다. 자세한 내용은 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(작업 병렬 라이브러리) 데이터 흐름 라이브러리를 사용합니다. 자세한 내용은 데이터 흐름(작업 병렬 라이브러리)참조하세요.
사용자 인터페이스
TAP을 UI 컨트롤과 함께 사용할 때는 다음 지침을 따라야 합니다.
API를 사용할 수 있는 경우 비동기 버전의 API를 호출합니다. 이렇게 하면 UI 스레드가 차단 해제되어 앱에 대한 사용자의 환경을 개선하는 데 도움이 됩니다.
예외가 발생하지 않도록 UI 스레드에서 비동기 작업의 데이터로 UI 요소를 업데이트합니다. 그러나
ListView.ItemsSource
속성에 대한 업데이트는 자동으로 UI 스레드로 마샬링됩니다. 코드가 UI 스레드에서 실행되는지 확인하는 방법에 대한 자세한 내용은 UI 스레드스레드 만들기를 참조하세요.중요하다
데이터 바인딩을 통해 업데이트되는 모든 컨트롤 속성은 자동으로 UI 스레드로 마샬링됩니다.
오류 처리
TAP을 사용하는 경우 다음 오류 처리 지침을 따라야 합니다.
- 비동기 예외 처리에 대해 알아봅니다. 비동기 실행 중인 코드에서 throw된 미처리된 예외는 특정 시나리오를 제외하고 호출 스레드로 다시 전파됩니다. 자세한 내용은 예외 처리(작업 병렬 라이브러리)참조하세요.
-
async void
메서드를 만들지 말고 대신async Task
메서드를 만듭니다. 이를 통해 오류 처리, 작성성 및 테스트 용이성을 더 쉽게 수행할 수 있습니다. 이 지침의 예외는void
반환해야 하는 비동기 이벤트 처리기입니다. 자세한 내용은 비동기 Void 피하기를 참조하세요. - 교착 상태가 발생할 수 있으므로
Task.Wait
,Task.Result
또는GetAwaiter().GetResult
메서드를 호출하여 차단 및 비동기 코드를 혼합하지 마세요. 그러나 이 지침을 위반해야 하는 경우 작업 예외가 유지되므로GetAwaiter().GetResult
메서드를 호출하는 것이 좋습니다. 자세한 내용은 비동기 프로그래밍 끝까지 및 .NET 4.5 작업 예외 처리를 참조하세요. - 가능하면
ConfigureAwait
메서드를 사용하여 컨텍스트 없는 코드를 만듭니다. 컨텍스트 없는 코드는 모바일 앱의 성능이 향상되며 부분적으로 비동기 코드베이스로 작업할 때 교착 상태를 방지하는 데 유용한 기술입니다. 자세한 내용은 구성 컨텍스트을 참조하세요. - 기능으로 이전 비동기 작업에서 발생한 예외를 처리하고, 연속 작업이 시작되기 전이나 실행 중에 취소할 수 있는 연속 작업을 사용합니다. 자세한 내용은 연속 작업사용하여
연결 작업을 참조하세요. - 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
메서드가 호출되도록 합니다.
IDisposable.Dispose 호출을 try/finally 블록으로 래핑하세요.
다음 예제에서는 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로부터 상속 받은 클래스와 같은 NSObject파생 서브클래스가 NSObject
파생 컨테이너에 추가되고, Objective-C에서 강하게 참조되는 경우를 다음 예제에서 고려해 보십시오.
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
인스턴스에 대한 참조 수가 증가합니다. 이 경우 iOS 런타임용 .NET은 관리되는 개체가 참조를 유지한다는 보장이 없으므로 관리 코드에서 MyView
개체를 활성 상태로 유지하는 GCHandle
인스턴스를 만듭니다. 관리 코드 관점에서 MyView
개체는 GCHandle
가 아니었다면 AddSubview(UIView) 호출 후에 회수되었을 것입니다.
관리되지 않는 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에서도 발생합니다. 예를 들어 UITableView 클래스에서 Delegate 속성 또는 DataSource 설정할 때입니다.
프로토콜을 구현하기 위해 순수하게 만들어진 클래스의 경우(예: IUITableViewDataSource) 서브클래스를 만드는 대신 수행할 수 있는 작업은 클래스에서 인터페이스를 구현하고 메서드를 재정의하고 this
DataSource
속성을 할당할 수 있습니다.
강력한 참조가 있는 개체를 삭제하세요.
강력한 참조가 있고 종속성을 제거하기 어려운 경우 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;
}
}
.NET MAUI