Udostępnij za pośrednictwem


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ć środowisko użytkownika dotyczące 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 postrzeganej wydajności w aplikacjach interfejsu użytkownika aplikacji wieloplatformowych platformy .NET (.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 systemów dotnet-trace Android, iOS i Mac i Windows oraz programu PerfView w systemie Windows. Aby uzyskać więcej informacji, zobacz Profilowanie aplikacji MAUI platformy .NET.

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 żadnych zalet związanych z danymi powiązania, które nie muszą być powiązane, ponieważ powią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 "Akceptuj".

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 pokazano VerticalStackLayout element z pojedynczym elementem podrzędnym:

<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 VerticalStackLayout element powinien zostać usunięty, 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ć Grid układu 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 elementu 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ąć, tworząc strumień po utworzeniu strony lub gdy Page.Appearing zdarzenie jest uruchamiane, a następnie dysponując strumień po Page.Disappearing uruchomieniu zdarzenia.

Podczas pobierania obrazu do wyświetlania za ImageSource.FromUri(Uri) pomocą metody upewnij się, że pobrany obraz jest buforowany przez odpowiedni czas. Aby uzyskać więcej informacji, zobacz Buforowanie obrazów.

Zmniejszanie rozmiaru drzewa wizualnego

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ć częścią drzewa wizualnego, czy nie. W związku z tym, jeśli element nie jest widoczny, ponieważ jest ukryty za innymi elementami, usuń element lub ustaw jego IsVisible właściwość na false.

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 HeadingLabelStyle przedstawiono zasób, 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 HeadingLabelStyle pokazano zasób, 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 (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 konsolidatora, zobacz Łączenie aplikacji systemu Android, Łączenie aplikacji systemu iOS i Łączenie aplikacji Katalizator dla komputerów Mac.

Skrócenie okresu aktywacji aplikacji

Wszystkie aplikacje mają okres aktywacji, czyli czas między uruchomieniem aplikacji a czasem gotowości 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 zestawów, ponieważ zestawy są ładowane po raz pierwszy.

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 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 powłoki

Aplikacje .NET MAUI Shell zapewniają środowisko nawigacji z opiniami na podstawie wysuwanych i kart. Jeśli środowisko użytkownika aplikacji można zaimplementować za pomocą powłoki, warto to zrobić. Aplikacje powłoki pomagają uniknąć słabego środowiska uruchamiania, ponieważ strony są tworzone na żądanie w odpowiedzi na nawigację, a nie podczas uruchamiania aplikacji, co występuje w przypadku aplikacji korzystających z elementu TabbedPage. Aby uzyskać więcej informacji, zobacz Omówienie powłoki.

Optymalizowanie wydajności elementu ListView

W przypadku korzystania z programu ListViewistnieje wiele środowisk użytkownika, które należy zoptymalizować:

  • Inicjowanie — interwał czasu rozpoczynający się od utworzenia kontrolki 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.
  • Interakcja z dodawaniem, usuwaniem i wybieraniem 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 Buforowanie danych.

Używanie programowania asynchronicznego

Ogólną szybkość reakcji aplikacji można zwiększyć, a wąskie gardła wydajności są często unikane przy użyciu programowania asynchronicznego. Na platformie .NET wzorzec asynchroniczny oparty na zadaniach (TAP) jest zalecanym wzorcem projektowym dla operacji asynchronicznych. Jednak nieprawidłowe użycie interfejsu TAP może spowodować, że aplikacje nie będą możliwe.

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 TaskStatus przez wyliczenie. Aby uzyskać więcej informacji, zobacz Znaczenie stanu zadania i zadania.
  • Task.WhenAll Użyj metody , aby asynchronicznie czekać na zakończenie wielu operacji asynchronicznych, a nie pojedynczo await serię operacji asynchronicznych. Aby uzyskać więcej informacji, zobacz Task.WhenAll.
  • Task.WhenAny Użyj metody , aby asynchronicznie poczekać na zakończenie jednej z wielu operacji asynchronicznych. Aby uzyskać więcej informacji, zobacz Task.WhenAny.
  • Task.Delay Użyj metody , aby utworzyć Task obiekt, który zakończy się 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 CPU w puli wątków przy użyciu Task.Run metody . Ta metoda jest skrótem dla TaskFactory.StartNew metody z najbardziej optymalnymi argumentami ustawionymi. Aby uzyskać więcej informacji, zobacz Task.Run.
  • Unikaj próby utworzenia konstruktorów asynchronicznych. Zamiast tego należy użyć zdarzeń cyklu życia lub oddzielnej logiki inicjowania, aby poprawnie await zainicjować. Aby uzyskać więcej informacji, zobacz Konstruktory asynchroniczne w blog.stephencleary.com.
  • Użyj wzorca opóźnionego zadania, aby uniknąć oczekiwania na wykonanie operacji asynchronicznych podczas uruchamiania aplikacji. Aby uzyskać więcej informacji, zobacz AsyncLazy.
  • Utwórz otokę zadań dla istniejących operacji asynchronicznych, które nie używają interfejsu TAP, tworząc TaskCompletionSource<T> obiekty. Te obiekty uzyskują korzyści z Task programowania i umożliwiają kontrolowanie okresu istnienia i ukończenia skojarzonego elementu Task. Aby uzyskać więcej informacji, zobacz Charakter zadaniaCompletionSource.
  • Task Zwróć obiekt, zamiast zwracać oczekiwany Task obiekt, gdy nie ma potrzeby przetwarzania wyniku operacji asynchronicznej. Jest to bardziej wydajne z powodu mniejszego przełączania kontekstu.
  • Użyj biblioteki przepływów danych biblioteki równoległej zadań (TPL) w scenariuszach, takich jak przetwarzanie danych, gdy staną się dostępne, lub gdy masz wiele operacji, które muszą komunikować się ze sobą asynchronicznie. Aby uzyskać więcej informacji, zobacz Przepływ danych (biblioteka równoległa zadań).

INTERFEJS UŻYTKOWNIKA

Podczas korzystania z interfejsu TAP z kontrolkami 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. Spowoduje to odblokowanie wątku interfejsu użytkownika, co pomoże ulepszyć środowisko użytkownika w aplikacji.

  • Zaktualizuj elementy interfejsu użytkownika przy użyciu danych z operacji asynchronicznych w wątku interfejsu użytkownika, aby uniknąć zgłaszania wyjątków. Jednak aktualizacje ListView.ItemsSource właściwości będą automatycznie marshalowane 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żne

    Wszystkie właściwości kontrolki aktualizowane za pośrednictwem powiązania danych będą automatycznie marshalowane do wątku interfejsu użytkownika.

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 async void metod i zamiast tego twórz async Task metody. 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ć wartość void. Aby uzyskać więcej informacji, zobacz Unikanie Async Void.
  • Nie mieszaj kodu blokującego i asynchronicznego przez wywołanie Task.Waitmetod , Task.Resultlub GetAwaiter().GetResult , ponieważ mogą one spowodować zakleszczenie. Jeśli jednak te wytyczne muszą zostać naruszone, preferowaną metodą jest wywołanie GetAwaiter().GetResult metody, ponieważ zachowuje wyjątki zadań. Aby uzyskać więcej informacji, zobacz Async All the Way and Task Exception Handling in .NET 4.5 (Asynchronizuj całą drogę i obsługę wyjątków zadań na platformie .NET 4.5).
  • ConfigureAwait Użyj metody , jeśli to możliwe, aby utworzyć kod wolny od 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 Konfigurowanie kontekstu.
  • Użyj zadań kontynuacji dla funkcji, takich jak obsługa wyjątków zgłaszanych przez poprzednią operację asynchroniczną, i anulowanie kontynuacji przed jej uruchomieniem lub uruchomieniem. Aby uzyskać więcej informacji, zobacz Łączenie zadań za pomocą zadań ciągłych.
  • Użyj implementacji asynchronicznej ICommand , gdy operacje asynchroniczne są wywoływane z klasy ICommand. Dzięki temu można obsłużyć wszelkie wyjątki w asynchronicznej logice poleceń. Aby uzyskać więcej informacji, zobacz Async Programming: Patterns for Asynchronous MVVM Applications: Commands (Asynchroniczne programowanie: wzorce dla asynchronicznych aplikacji MVVM: polecenia).

Opóźnienie kosztu 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 następuje przy pierwszym uzyskiwaniu Lazy<T>.Value dostępu do właściwości. 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 Lazy Initialization (Inicjowanie z opóźnieniem).

Wydawanie zasobów IDisposable

Interfejs IDisposable udostępnia mechanizm wydawania zasobów. Udostępnia metodę Dispose , 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 plików, strumieni i połączeń sieciowych.
  • Gdy klasa jest właścicielem zarządzanych IDisposable zasobów.

Użytkownicy typu mogą następnie wywoływać implementację IDisposable.Dispose , aby zwolnić zasoby, gdy wystąpienie nie jest już wymagane. Istnieją dwa podejścia do osiągnięcia tego celu:

  • Zawijając IDisposable obiekt w instrukcji using .
  • Zawijając wywołanie elementu IDisposable.Dispose w try/finally bloku.

Zawijanie obiektu IDisposable w instrukcji using

W poniższym przykładzie pokazano, jak opakowować IDisposable obiekt w instrukcji using :

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

Klasa StreamReader implementuje IDisposablemetodę , a using instrukcja udostępnia wygodną składnię, która wywołuje StreamReader.Dispose metodę w StreamReader obiekcie przed wyjściem z zakresu. using W bloku StreamReader obiekt jest tylko do odczytu i nie można go ponownie przypisać. Instrukcja using zapewnia również, że Dispose metoda jest wywoływana nawet w przypadku wystąpienia wyjątku, ponieważ kompilator implementuje język pośredni (IL) dla try/finally bloku.

Zawijanie wywołania do elementu IDisposable.Dispose w bloku try/finally

W poniższym przykładzie pokazano, jak opakowować wywołanie IDisposable.Dispose w try/finally bloku:

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 IDisposablemetodę , a finally blok wywołuje metodę StreamReader.Dispose , aby zwolnić zasób. Aby uzyskać więcej informacji, zobacz IDisposable Interface (Interfejs IDisposable).

Anulowanie subskrypcji zdarzeń

Aby zapobiec wyciekom pamięci, zdarzenia powinny zostać anulowane przed likwidacją obiektu subskrybenta. Dopóki zdarzenie nie zostanie anulowane, pełnomocnik zdarzenia w obiekcie publikowania ma odwołanie do delegata, który hermetyzuje procedurę obsługi zdarzeń subskrybenta. Tak długo, jak obiekt publikowania przechowuje to odwołanie, odzyskiwanie pamięci nie spowoduje odzyskania 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 swojej Dispose metodzie.

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ć ich przy życiu. 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 metody anonimowej i jest używane do subskrypcji zdarzeń i anulowania subskrypcji.

Unikaj silnych odwołań okrągłych w systemach iOS i Mac Catalyst

W niektórych sytuacjach istnieje możliwość utworzenia silnych cykli odwołań, które mogą uniemożliwić obiektom odzyskanie pamięci przez moduł odśmiecenia pamięci. Rozważmy na przykład przypadek, w którym podklasa NSObjectpochodna, taka jak klasa dziedziczona z UIViewklasy , jest dodawana do kontenera pochodnego NSObjecti jest silnie przywoływana z Objective-Cklasy , jak pokazano w poniższym przykładzie:

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));

Po utworzeniu Container wystąpienia ten kod będzie miał silne odwołanie do Objective-C obiektu w języku C#. MyView Podobnie wystąpienie będzie mieć silne odwołanie do Objective-C obiektu.

Ponadto wywołanie metody , aby container.AddSubview zwiększyć liczbę odwołań w wystąpieniu niezarządzanych MyView . W takim przypadku środowisko uruchomieniowe platformy .NET dla systemu iOS tworzy GCHandle wystąpienie w celu zachowania MyView aktywności obiektu w kodzie zarządzanym, ponieważ nie ma żadnej gwarancji, że żadne obiekty zarządzane zachowają do niego odwołanie. Z perspektywy MyView kodu zarządzanego obiekt zostanie odzyskany po AddSubview(UIView) wywołaniu, gdyby nie obiekt GCHandle.

Niezarządzany MyView obiekt będzie miał GCHandle wskazanie obiektu zarządzanego , znanego jako silny link. Obiekt zarządzany będzie zawierać odwołanie do Container wystąpienia. Z kolei Container wystąpienie będzie mieć zarządzane odwołanie do MyView obiektu.

W sytuacjach, gdy zawarty obiekt przechowuje łącze do kontenera, dostępnych jest kilka opcji obsługi odwołania cyklicznego:

  • Unikaj odwołania cyklicznego, utrzymując słabe odwołanie do kontenera.
  • Wywołaj metodę 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 cyklu jest użycie słabego odwołania od elementu podrzędnego do elementu nadrzędnego. 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 przez wywołanie metody container.AddSubView.

Dzieje się tak również w interfejsach API systemu iOS, które używają wzorca delegata lub źródła danych, gdzie klasa równorzędna zawiera implementację. Na przykład podczas ustawiania Delegate właściwości lub DataSourceUITableView klasy .

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ć DataSource właściwość do thisklasy .

Usuwanie obiektów z silnymi odwołaniami

Jeśli istnieje silne odwołanie i trudno jest usunąć zależność, wyczyść metodę Dispose wskaźnika 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;
    }
}