Zwiększanie wydajności aplikacji
Niska wydajność aplikacji przedstawia się na wiele sposobów. Może to spowodować, że aplikacja nie odpowiada, może spowodować powolne przewijanie i zmniejszyć żywotność baterii urządzenia. Jednak optymalizacja wydajności wymaga więcej niż tylko zaimplementowania wydajnego kodu. Należy również rozważyć doświadczenie użytkownika na temat wydajności aplikacji. Na przykład zapewnienie, że operacje są wykonywane bez blokowania użytkownikowi wykonywania innych działań, mogą pomóc w ulepszaniu środowiska użytkownika.
Istnieje wiele technik zwiększania wydajności i odczuwalnej wydajności aplikacji wieloplatformowych .NET MAUI. Łącznie te techniki mogą znacznie zmniejszyć ilość pracy wykonywanej przez procesor CPU oraz ilość pamięci zużywanej przez aplikację.
Korzystanie z profilera
Podczas tworzenia aplikacji ważne jest, aby podjąć próbę optymalizacji kodu tylko po jego profilowaniu. Profilowanie to technika określania, gdzie optymalizacje kodu będą miały największy wpływ na zmniejszenie problemów z wydajnością. Profiler śledzi użycie pamięci aplikacji i rejestruje czas działania metod w aplikacji. Te dane ułatwiają nawigowanie po ścieżkach wykonywania aplikacji oraz koszt wykonywania kodu, dzięki czemu można odnaleźć najlepsze możliwości optymalizacji.
Aplikacje MAUI platformy .NET można profilować przy użyciu dotnet-trace
w systemach Android, iOS i Mac oraz w programie PerfView w systemie Windows. Aby uzyskać więcej informacji, zobacz Profilowanie aplikacji .NET MAUI.
Podczas profilowania aplikacji zalecane są następujące najlepsze rozwiązania:
- Unikaj profilowania aplikacji w symulatorze, ponieważ symulator może zakłócać wydajność aplikacji.
- W idealnym przypadku profilowanie powinno być wykonywane na różnych urządzeniach, ponieważ pomiary wydajności na jednym urządzeniu nie zawsze będą pokazywać charakterystykę wydajności innych urządzeń. Jednak co najmniej profilowanie powinno być wykonywane na urządzeniu, które ma najniższą oczekiwaną specyfikację.
- Zamknij wszystkie inne aplikacje, aby upewnić się, że cały wpływ profilowanej aplikacji jest mierzony, a nie innych aplikacji.
Używanie skompilowanych powiązań
Skompilowane powiązania zwiększają wydajność powiązań danych w aplikacjach .NET MAUI przez rozpoznawanie wyrażeń powiązań w czasie kompilacji, a nie w czasie wykonywania z odbiciem. Kompilowanie wyrażenia powiązania generuje skompilowany kod, który zazwyczaj rozwiązuje powiązanie 8–20 razy szybciej niż użycie powiązania klasycznego. Aby uzyskać więcej informacji, zobacz Skompilowane powiązania.
Zmniejszanie niepotrzebnych powiązań
Nie używaj powiązań dla zawartości, które można łatwo ustawić statycznie. Nie ma żadnej korzyści w wiązaniu danych, których wiązanie nie jest konieczne, ponieważ wiązania nie są opłacalne. Na przykład ustawienie Button.Text = "Accept"
ma mniejsze obciążenie niż powiązanie Button.Text z właściwością viewmodel string
o wartości "Accept".
Wybieranie prawidłowego układu
Układ, który jest zdolny do wyświetlania wielu elementów podrzędnych, ale ma tylko jedno dziecko, jest marnotrawny. Na przykład w poniższym przykładzie przedstawiono VerticalStackLayout z pojedynczym dzieckiem.
<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>
Jest to marne i należy usunąć element VerticalStackLayout, jak pokazano w poniższym przykładzie:
<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>
Ponadto nie należy podejmować próby odtworzenia wyglądu określonego układu przy użyciu kombinacji innych układów, ponieważ powoduje to niepotrzebne obliczenia układu. Na przykład nie próbuj odtworzyć układu Grid przy użyciu kombinacji HorizontalStackLayout elementów. W poniższym przykładzie przedstawiono przykład tego złego rozwiązania:
<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>
Jest to marnotrawne, ponieważ wykonywane są niepotrzebne obliczenia układu. Zamiast tego żądany układ można lepiej osiągnąć przy użyciu Grid, jak pokazano w poniższym przykładzie:
<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>
Optymalizowanie zasobów obrazu
Obrazy to niektóre z najdroższych zasobów używanych przez aplikacje i są często przechwytywane w wysokiej rozdzielczości. Chociaż powoduje to tworzenie dynamicznych obrazów pełnych szczegółów, aplikacje, które wyświetlają takie obrazy, zwykle wymagają większego użycia procesora CPU do dekodowania obrazu i większej ilości pamięci do przechowywania zdekodowanego obrazu. Dekodowanie obrazu o wysokiej rozdzielczości w pamięci jest marnotrawne, gdy zostanie on przeskalowany w dół do mniejszego rozmiaru na potrzeby wyświetlania. Zamiast tego zmniejsz zużycie procesora CPU i pamięci, tworząc wersje przechowywanych obrazów, które są zbliżone do przewidywanych rozmiarów wyświetlania. Na przykład obraz wyświetlany w widoku listy powinien być najprawdopodobniej niższą rozdzielczością niż obraz wyświetlany na pełnym ekranie.
Ponadto obrazy powinny być tworzone tylko wtedy, gdy jest to wymagane i powinny być zwalniane natychmiast, gdy aplikacja nie będzie już ich wymagała. Jeśli na przykład aplikacja wyświetla obraz, odczytując dane ze strumienia, upewnij się, że strumień jest tworzony tylko wtedy, gdy jest wymagany, i upewnij się, że strumień jest zwalniany, gdy nie jest już wymagany. Można to osiągnąć przez utworzenie strumienia podczas tworzenia strony lub gdy wyzwolone zostanie zdarzenie Page.Appearing, a następnie usunięcie strumienia po wyzwoleniu zdarzenia Page.Disappearing.
Podczas pobierania obrazu do wyświetlania za pomocą metody ImageSource.FromUri(Uri) upewnij się, że pobrany obraz jest buforowany przez odpowiedni czas. Aby uzyskać więcej informacji, zapoznaj się z buforowaniem obrazów.
Zmniejszanie liczby elementów na stronie
Zmniejszenie liczby elementów na stronie spowoduje szybsze renderowanie strony. Istnieją dwie główne techniki osiągnięcia tego celu. Pierwszym z nich jest ukrycie elementów, które nie są widoczne. Właściwość IsVisible każdego elementu określa, czy element powinien być widoczny na ekranie. Jeśli element nie jest widoczny, ponieważ jest ukryty za innymi elementami, usuń element lub ustaw jego właściwość IsVisible
na wartość false
. Ustawienie właściwości IsVisible
elementu na false
zachowuje element w drzewie wizualnym, ale wyklucza ją z obliczeń renderowania i układu.
Drugą techniką jest usunięcie niepotrzebnych elementów. Na przykład poniżej przedstawiono układ strony zawierający wiele Label elementów:
<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>
Ten sam układ strony można zachować przy użyciu mniejszej liczby elementów, jak pokazano w poniższym przykładzie:
<VerticalStackLayout Padding="20,35,20,20"
Spacing="25">
<Label Text="Hello" />
<Label Text="Welcome to the App!" />
<Label Text="Downloading Data..." />
</VerticalStackLayout>
Zmniejsz rozmiar słownika zasobów aplikacji
Wszystkie zasoby używane w całej aplikacji powinny być przechowywane w słowniku zasobów aplikacji, aby uniknąć duplikowania. Pomoże to zmniejszyć ilość kodu XAML, który musi zostać przeanalizowany w całej aplikacji. W poniższym przykładzie przedstawiono zasób HeadingLabelStyle
, który jest używany dla całej aplikacji, a więc jest zdefiniowany w słowniku zasobów aplikacji:
<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>
Jednak kod XAML specyficzny dla strony nie powinien być uwzględniony w słowniku zasobów aplikacji, ponieważ zasoby będą następnie analizowane podczas uruchamiania aplikacji, a nie wtedy, gdy jest to wymagane przez stronę. Jeśli zasób jest używany przez stronę, która nie jest stroną uruchamiania, powinna zostać umieszczona w słowniku zasobów dla tej strony, co pomaga zmniejszyć liczbę kodu XAML analizowanego podczas uruchamiania aplikacji. W poniższym przykładzie przedstawiono zasób HeadingLabelStyle
, który znajduje się tylko na jednej stronie, a więc jest zdefiniowany w słowniku zasobów strony:
<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>
Aby uzyskać więcej informacji na temat zasobów aplikacji, zobacz Style apps using XAML.
Zmniejszanie rozmiaru aplikacji
Gdy program .NET MAUI kompiluje aplikację, konsolidator o nazwie ILLink może służyć do zmniejszenia ogólnego rozmiaru aplikacji. IlLink zmniejsza rozmiar, analizując kod pośredni utworzony przez kompilator. Usuwa nieużywane metody, właściwości, pola, zdarzenia, struktury i klasy, aby utworzyć aplikację zawierającą tylko zależności kodu i zestawu niezbędne do uruchomienia aplikacji.
Aby uzyskać więcej informacji na temat konfigurowania zachowania linkera, zobacz Linkowanie aplikacji na Androida, Linkowanie aplikacji na iOSi Linkowanie aplikacji Mac Catalyst.
Skrócenie okresu aktywacji aplikacji
Wszystkie aplikacje mają okres aktywacji, czyli czas między rozpoczęciem działania aplikacji a przygotowaniem aplikacji do użycia. Ten okres aktywacji zapewnia użytkownikom pierwsze wrażenie aplikacji, dlatego ważne jest, aby skrócić okres aktywacji i postrzeganie jej przez użytkownika, aby uzyskać korzystne pierwsze wrażenie aplikacji.
Przed wyświetleniem początkowego interfejsu użytkownika aplikacji powinien zostać wyświetlony ekran powitalny wskazujący użytkownikowi, że aplikacja jest uruchamiana. Jeśli aplikacja nie może szybko wyświetlić swojego początkowego interfejsu użytkownika, ekran powitalny powinien służyć do informowania użytkownika o postępie w okresie aktywacji, aby zapewnić pewność, że aplikacja nie zawiesiła się. To zapewnienie może być paskiem postępu lub podobną kontrolą.
W okresie aktywacji aplikacje wykonują logikę aktywacji, która często obejmuje ładowanie i przetwarzanie zasobów. Okres aktywacji można zmniejszyć, upewniając się, że wymagane zasoby są pakowane w aplikacji, zamiast pobierać zdalnie. Na przykład w niektórych okolicznościach może być to odpowiednie w okresie aktywacji, aby załadować lokalnie przechowywane dane zastępcze. Następnie po wyświetleniu początkowego interfejsu użytkownika i możliwości interakcji użytkownika z aplikacją dane zastępcze mogą być stopniowo zastępowane ze źródła zdalnego. Ponadto logika aktywacji aplikacji powinna wykonywać tylko pracę wymaganą do rozpoczęcia korzystania z aplikacji przez użytkownika. Może to pomóc, jeśli opóźni ładowanie dodatkowych bibliotek, ponieważ biblioteki są ładowane po raz pierwszy, gdy są używane.
Starannie wybierz kontener wstrzykiwania zależności
Kontenery wstrzykiwania zależności wprowadzają dodatkowe ograniczenia wydajności do aplikacji mobilnych. Rejestrowanie i rozpoznawanie typów w kontenerze ma koszt wydajności ze względu na użycie odbicia kontenera do tworzenia każdego typu, zwłaszcza jeśli zależności są odtwarzane dla każdej nawigacji stron w aplikacji. Jeśli istnieje wiele lub głębokie zależności, koszt tworzenia może znacznie wzrosnąć. Ponadto rejestracja typu, która zwykle występuje podczas uruchamiania aplikacji, może mieć zauważalny wpływ na czas uruchamiania, zależny od używanego kontenera. Aby uzyskać więcej informacji na temat wstrzykiwania zależności w aplikacjach .NET MAUI, zobacz wpis wstrzykiwanie zależności.
Alternatywnie iniekcja zależności może być bardziej wydajna, implementując ją ręcznie przy użyciu fabryk.
Tworzenie aplikacji Shell
Aplikacje .NET MAUI Shell zapewniają przemyślane doświadczenie nawigacyjne oparte na wysuwanych menu i kartach. Jeśli doświadczenie użytkownika aplikacji można zaimplementować przy użyciu Shella, warto to zrobić. Aplikacje powłoki pomagają uniknąć słabego doświadczenia uruchamiania, ponieważ strony są tworzone na żądanie w odpowiedzi na działanie nawigacyjne, a nie podczas uruchamiania aplikacji, co występuje w przypadku aplikacji korzystających z TabbedPage. Aby uzyskać więcej informacji, zobacz omówienie Shell.
Optymalizowanie wydajności elementu ListView
W przypadku korzystania z ListViewistnieje wiele środowisk użytkownika, które należy zoptymalizować:
- Inicjalizacja — okres zaczynający się od utworzenia elementu sterującego i kończący się, gdy elementy są wyświetlane na ekranie.
- Przewijanie — możliwość przewijania listy i upewnienia się, że interfejs użytkownika nie pozostaje w tyle za gestami dotykowymi.
- Interakcje dodawanie, usuwanie i wybieranie elementów.
Kontrolka ListView wymaga, aby aplikacja dostarczała dane i szablony komórek. Sposób osiągnięcia tego celu będzie miał duży wpływ na wydajność kontrolki. Aby uzyskać więcej informacji, zobacz Cache data.
Używanie programowania asynchronicznego
Ogólną responsywność aplikacji można zwiększyć i można często unikać wąskich gardeł wydajności przy użyciu programowania asynchronicznego. Na platformie .NET Wzorzec Asynchroniczny oparty na Zadaniach (TAP) jest zalecanym wzorcem projektowania dla operacji asynchronicznych. Jednak nieprawidłowe użycie interfejsu TAP może spowodować, że aplikacje będą działać nieefektywnie.
Podstawy
Podczas korzystania z interfejsu TAP należy postępować zgodnie z następującymi ogólnymi wytycznymi:
- Zapoznaj się z cyklem życia zadania, który jest reprezentowany przez wyliczenie
TaskStatus
. Aby uzyskać więcej informacji, zobacz znaczenie TaskStatus i stan zadania. - Użyj metody
Task.WhenAll
, aby asynchronicznie czekać na zakończenie wielu operacji asynchronicznych, zamiast wykonywać je kolejno za pomocąawait
. Aby uzyskać więcej informacji, zobacz Task.WhenAll. - Użyj metody
Task.WhenAny
, aby asynchronicznie poczekać na zakończenie jednej z wielu operacji asynchronicznych. Aby uzyskać więcej informacji, zobacz Task.WhenAny. - Użyj metody
Task.Delay
, aby utworzyć obiektTask
, który zostanie zakończony po upływie określonego czasu. Jest to przydatne w przypadku scenariuszy, takich jak sondowanie danych i opóźnianie obsługi danych wejściowych użytkownika dla wstępnie określonego czasu. Aby uzyskać więcej informacji, zobacz Task.Delay. - Wykonaj intensywne operacje synchroniczne procesora w puli wątków przy użyciu metody
Task.Run
. Ta metoda jest skrótem dla metodyTaskFactory.StartNew
z najbardziej optymalnym zestawem argumentów. Aby uzyskać więcej informacji, zobacz Task.Run. - Unikaj próby utworzenia konstruktorów asynchronicznych. Zamiast tego należy używać zdarzeń cyklu życia aplikacji lub oddzielnej logiki inicjalizacji, aby poprawnie
await
jakiejkolwiek inicjalizacji. Więcej informacji znajdziesz na blogu.stephencleary.com w artykule Konstruktory asynchroniczne. - Użyj wzorca leniwego zadania, aby uniknąć oczekiwania na wykonanie operacji asynchronicznych podczas uruchamiania aplikacji. Aby uzyskać więcej informacji, zobacz AsyncLazy.
- Utwórz opakowanie zadań dla istniejących operacji asynchronicznych, które nie używają wzorca TAP, tworząc obiekty
TaskCompletionSource<T>
. Te właśnie obiekty czerpią korzyści z programowalnościTask
i umożliwiają kontrolowanie czasu życia oraz zakończenia skojarzonegoTask
. Aby uzyskać więcej informacji, zobacz Charakterystyka TaskCompletionSource. - Zwróć obiekt
Task
, zamiast zwracać oczekiwany obiektTask
, gdy nie ma potrzeby przetwarzania wyniku operacji asynchronicznej. Jest to bardziej wydajne z powodu mniejszego przełączania kontekstu. - Użyj biblioteki przepływu danych Task Parallel Library (TPL) w scenariuszach, takich jak przetwarzanie danych w miarę ich dostępności, lub gdy masz do czynienia z wieloma operacjami, które muszą się asynchronicznie komunikować. Aby uzyskać więcej informacji, zobacz Przepływ danych (biblioteka równoległa zadań).
Interfejs użytkownika
Podczas korzystania z TAP z elementami sterującymi interfejsu użytkownika należy postępować zgodnie z następującymi wskazówkami:
Wywołaj asynchroniczną wersję interfejsu API, jeśli jest dostępna. To sprawi, że wątek interfejsu użytkownika pozostanie odblokowany, co pomoże ulepszyć doświadczenie użytkownika z aplikacją.
Aby uniknąć wyrzucania wyjątków, zaktualizuj elementy interfejsu użytkownika, korzystając z danych z operacji asynchronicznych w wątku interfejsu użytkownika. Jednak aktualizacje właściwości
ListView.ItemsSource
będą automatycznie przekazywane do wątku interfejsu użytkownika. Aby uzyskać informacje na temat określania, czy kod jest uruchomiony w wątku interfejsu użytkownika, zobacz Tworzenie wątku w wątku interfejsu użytkownika.Ważny
Wszystkie właściwości kontrolki aktualizowane za pośrednictwem powiązania danych będą automatycznie przekazywane do wątku UI.
Obsługa błędów
Podczas korzystania z interfejsu TAP należy postępować zgodnie z następującymi wytycznymi dotyczącymi obsługi błędów:
- Dowiedz się więcej o asynchronicznej obsłudze wyjątków. Nieobsługiwane wyjątki zgłaszane przez kod uruchomiony asynchronicznie są propagowane z powrotem do wątku wywołującego, z wyjątkiem niektórych scenariuszy. Aby uzyskać więcej informacji, zobacz Obsługa wyjątków (biblioteka równoległa zadań).
- Unikaj tworzenia metod
async void
i zamiast tego twórz metodyasync Task
. Umożliwiają one łatwiejsze obsługę błędów, komponowanie i możliwość testowania. Wyjątkiem od tych wytycznych są asynchroniczne programy obsługi zdarzeń, które muszą zwrócićvoid
. Aby uzyskać więcej informacji, zobacz Unikaj Async Void. - Nie mieszaj kodu blokującego i asynchronicznego, wywołując metody
Task.Wait
,Task.Result
lubGetAwaiter().GetResult
, ponieważ mogą one spowodować zakleszczenie. Jednak jeśli te wytyczne muszą zostać naruszone, preferowaną metodą jest wywołanie metodyGetAwaiter().GetResult
, ponieważ zachowuje wyjątki zadań. Aby uzyskać więcej informacji, zobacz Async All the Way oraz Task Exception Handling in .NET 4.5(Obsługa wyjątków zadań na platformie .NET 4.5). - Użyj metody
ConfigureAwait
, jeśli to możliwe, aby utworzyć kod bez kontekstu. Kod bez kontekstu ma lepszą wydajność dla aplikacji mobilnych i jest przydatną techniką unikania zakleszczenia podczas pracy z częściowo asynchroniczną bazą kodu. Aby uzyskać więcej informacji, zobacz Configure Context. - Użyj zadań kontynuacji do funkcjonalności, takich jak obsługa wyjątków zgłaszanych przez poprzednią operację asynchroniczną oraz anulowanie kontynuacji przed rozpoczęciem bądź w trakcie jej działania. Aby uzyskać więcej informacji, zobacz Łączenie zadań przy użyciu zadań ciągłych.
- Użyj asynchronicznej implementacji ICommand, gdy operacje asynchroniczne są wywoływane z ICommand. Dzięki temu można obsłużyć wszelkie wyjątki w asynchronicznej logice poleceń. Aby uzyskać więcej informacji, zobacz Programowanie asynchroniczne: Wzorce dla asynchronicznych aplikacji MVVM: Polecenia.
Opóźnij koszt tworzenia obiektów
Inicjowanie z opóźnieniem może służyć do odroczenia tworzenia obiektu do momentu jego pierwszego użycia. Ta technika jest używana głównie w celu zwiększenia wydajności, uniknięcia obliczeń i zmniejszenia wymagań dotyczących pamięci.
Rozważ użycie inicjowania z opóźnieniem dla obiektów, które są kosztowne do utworzenia w następujących scenariuszach:
- Aplikacja może nie używać obiektu .
- Przed utworzeniem obiektu należy wykonać inne kosztowne operacje.
Klasa Lazy<T>
służy do definiowania typu zainicjowanego z opóźnieniem, jak pokazano w poniższym przykładzie:
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)
{
...
}
Inicjowanie z opóźnieniem występuje przy pierwszym uzyskiwaniu dostępu do właściwości Lazy<T>.Value
. Opakowany typ jest tworzony i zwracany przy pierwszym dostępie oraz przechowywany dla dowolnego przyszłego dostępu.
Aby uzyskać więcej informacji na temat leniwego inicjowania, zobacz Inicjowanie z opóźnieniem.
Zwolnienie zasobów IDisposable
Interfejs IDisposable
udostępnia mechanizm wydawania zasobów. Udostępnia Dispose
metodę, która powinna zostać zaimplementowana w celu jawnego wydania zasobów.
IDisposable
nie jest destruktorem, i powinien być implementowany tylko w następujących okolicznościach:
- Gdy klasa jest właścicielem niezarządzanych zasobów. Typowe niezarządzane zasoby, które wymagają zwolnienia, obejmują pliki, strumienie i połączenia sieciowe.
- Gdy klasa posiada zarządzane zasoby
IDisposable
.
Użytkownicy danego typu mogą następnie wywoływać implementację IDisposable.Dispose
, aby zwolnić zasoby, gdy instancja nie jest już wymagana. Istnieją dwa podejścia do osiągnięcia tego celu:
- Zawijając obiekt
IDisposable
w wyrażenieusing
. - Zawijając wywołanie
IDisposable.Dispose
w bloktry
/finally
.
Zawijanie obiektu IDisposable w instrukcji using
W poniższym przykładzie pokazano, jak opakowować obiekt IDisposable
w instrukcji using
:
public void ReadText(string filename)
{
string text;
using (StreamReader reader = new StreamReader(filename))
{
text = reader.ReadToEnd();
}
...
}
Klasa StreamReader
implementuje IDisposable
, a instrukcja using
udostępnia wygodną składnię, która wywołuje metodę StreamReader.Dispose
na obiekcie StreamReader
przed opuszczeniem zakresu. W bloku using
obiekt StreamReader
jest tylko do odczytu i nie można go ponownie przypisać. Instrukcja using
gwarantuje również, że metoda Dispose
jest wywoływana, nawet jeśli wystąpi wyjątek, ponieważ kompilator implementuje język pośredni (IL) dla bloku try
/finally
.
Zawinąć wywołanie IDisposable.Dispose w bloku try/finally
W poniższym przykładzie pokazano, jak opakowować wywołanie IDisposable.Dispose
w bloku try
/finally
:
public void ReadText(string filename)
{
string text;
StreamReader reader = null;
try
{
reader = new StreamReader(filename);
text = reader.ReadToEnd();
}
finally
{
if (reader != null)
reader.Dispose();
}
...
}
Klasa StreamReader
implementuje IDisposable
, a blok finally
wywołuje metodę StreamReader.Dispose
w celu zwolnienia zasobu. Aby uzyskać więcej informacji, zobacz IDisposable Interface.
Anulowanie subskrypcji zdarzeń
Aby zapobiec wyciekom pamięci, zdarzenia powinny zostać odsubskrybowane przed zniszczeniem obiektu subskrybenta. Dopóki zdarzenie nie zostanie wyrejestrowane, delegat zdarzenia w obiekcie publikującym posiada odwołanie do delegata, który inkapsuluje procedurę obsługi zdarzeń subskrybenta. Tak długo, jak obiekt publikujący przechowuje to odwołanie, zbieranie śmieci nie usunie pamięci obiektu subskrybenta.
W poniższym przykładzie pokazano, jak anulować subskrypcję zdarzenia:
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;
}
}
Klasa Subscriber
anuluje subskrypcję zdarzenia w metodzie Dispose
.
Cykle odwołań mogą również wystąpić w przypadku używania procedur obsługi zdarzeń i składni lambda, ponieważ wyrażenia lambda mogą odwoływać się do obiektów i utrzymywać je aktywnymi. W związku z tym odwołanie do metody anonimowej może być przechowywane w polu i używane do anulowania subskrypcji zdarzenia, jak pokazano w poniższym przykładzie:
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;
}
}
Pole _handler
utrzymuje odwołanie do anonimowej funkcji i jest używane do subskrypcji zdarzeń oraz rezygnacji z subskrypcji.
Unikaj silnych cyklicznych referencji w systemach iOS i Mac Catalyst
W niektórych sytuacjach istnieje możliwość utworzenia silnych cykli odwołań, które mogą uniemożliwić obiektom odzyskaniu pamięci przez moduł garbage collector. Rozważmy na przykład przypadek, w którym podklasa pochodna NSObject, takiej, która dziedziczy z UIView, jest dodana do kontenera pochodnego NSObject
i jest silnie referencjonowana z Objective-C, jak pokazano w przykładzie poniżej.
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));
Gdy ten kod utworzy wystąpienie Container
, obiekt języka C# będzie miał silne odwołanie do obiektu Objective-C. Podobnie wystąpienie MyView
będzie również mieć silne odwołanie do obiektu Objective-C.
Ponadto wywołanie container.AddSubview
zwiększy liczbę referencji dla wystąpienia niezarządzanego MyView
. W takim przypadku środowisko uruchomieniowe platformy .NET dla systemu iOS tworzy wystąpienie GCHandle
w celu zachowania aktywności obiektu MyView
w kodzie zarządzanym, ponieważ nie ma gwarancji, że wszystkie obiekty zarządzane będą do niego odwoływać się. Z perspektywy kodu zarządzanego, obiekt MyView
zostałby odzyskany po wywołaniu AddSubview(UIView), gdyby nie GCHandle
.
Niezarządzany obiekt MyView
będzie miał GCHandle
wskazujący obiekt zarządzany, znany jako silny link. Obiekt zarządzany będzie zawierać odwołanie do wystąpienia Container
. W rezultacie instancja Container
będzie miała zarządzane odniesienie do obiektu MyView
.
W sytuacjach, gdy obiekt zawarty w kontenerze przechowuje łącze do tego kontenera, istnieje kilka sposobów radzenia sobie z odwołaniem cyklicznym:
- Unikaj odwołania cyklicznego, zachowując słabe odniesienie do kontenera.
- Wywołaj
Dispose
na obiektach. - Ręczne przerwanie cyklu przez ustawienie linku do kontenera na
null
. - Ręcznie usuń zawarty obiekt z kontenera.
Używanie słabych odwołań
Jednym ze sposobów zapobiegania cyklowi jest użycie słabej referencji od dziecka do rodzica. Na przykład powyższy kod może być pokazany w poniższym przykładzie:
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));
W tym miejscu zawarty obiekt nie będzie utrzymywać elementu nadrzędnego przy życiu. Jednak rodzic utrzymuje dziecko przy życiu poprzez wywołanie container.AddSubView
.
Dzieje się tak również w API systemu iOS, które używają wzorca delegata lub źródła danych, gdzie klasa równorzędna implementuje funkcjonalność. Na przykład podczas ustawiania właściwości Delegate lub DataSource w klasie UITableView.
W przypadku klas, które są tworzone wyłącznie ze względu na implementację protokołu, na przykład IUITableViewDataSource, co można zrobić, to zamiast tworzyć podklasę, można po prostu zaimplementować interfejs w klasie i zastąpić metodę, a następnie przypisać właściwość DataSource
do this
.
Usuwanie obiektów z silnymi odwołaniami
Jeśli istnieje silne odwołanie i trudno jest usunąć tę zależność, skonfiguruj metodę Dispose
tak, aby wyczyściła wskaźnik nadrzędnego.
W przypadku kontenerów przesłoń metodę Dispose
, aby usunąć zawarte obiekty, jak pokazano w poniższym przykładzie:
class MyContainer : UIView
{
public override void Dispose()
{
// Brute force, remove everything
foreach (var view in Subviews)
{
view.RemoveFromSuperview();
}
base.Dispose();
}
}
W przypadku obiektu podrzędnego, który utrzymuje silne odwołanie do elementu nadrzędnego, wyczyść odwołanie do elementu nadrzędnego w implementacji Dispose
:
class MyChild : UIView
{
MyContainer _container;
public MyChild(MyContainer container)
{
_container = container;
}
public override void Dispose()
{
_container = null;
}
}