Freigeben über


Verbessern der App-Leistung

Eine schlechte App-Leistung kann sich auf viele Weisen bemerkbar machen. Dies kann dazu führen, dass eine App nicht mehr reagiert, das Scrollen zu langsam ist und die Akkulaufzeit des Geräts verkürzt wird. Leistungsoptimierung umfasst jedoch mehr als das bloße Implementieren eines effizienten Codes. Es muss ebenfalls berücksichtigt werden, wie der Benutzer die App-Leistung wahrnimmt. Wenn laufende Vorgänge Benutzer beispielsweise nicht an anderen Aktivitäten hindern, kann dies die Benutzerfreundlichkeit verbessern.

Es gibt viele Techniken, um die Leistung und der wahrgenommenen Leistung von .NET Multi-Platform App UI (.NET MAUI)-Apps zu steigern. Insgesamt können diese Techniken die von einer CPU zu leistende Arbeit und den von einer Anwendung verbrauchten Speicherplatz erheblich reduzieren.

Verwenden Sie einen Profiler

Beim Entwickeln einer App sollte erst nach dem Profiling versucht werden, den Code zu optimieren. Beim Profiling wird bestimmt, in welchen Bereichen Codeoptimierungen am wirkungsvollsten Leistungsprobleme beheben. Der Profiler analysiert die Speichernutzung der App und zeichnet die Ausführungszeit von Methoden in der App auf. Die Daten helfen beim Navigieren durch die Ausführungspfade der App und die Ausführungskosten des Codes, so kann ermittelt werden, wie die Leistung am besten optimiert werden kann.

.NET MAUI-Apps können mit dotnet-trace unter Android, iOS, Mac und Windows sowie mit PerfView unter Windows profiliert werden. Weitere Informationen finden Sie unter .NET MAUI-Apps profilieren.

Die folgenden Best Practices werden für das Profiling einer App empfohlen:

  • Vermeiden Sie es, eine App in einem Simulator zu profilen, da der Simulator die App-Leistung beeinträchtigen kann.
  • Das Profiling sollte im Idealfall auf einer Vielzahl von Geräten ausgeführt werden, da Leistungsmessungen auf einem Gerät nicht unbedingt die Leistungsmerkmale auf anderen Geräten widerspiegeln. Ist dies nicht möglich, sollte das Profiling zumindest auf einem Gerät ausgeführt werden, das die niedrigste, erwartete Spezifikation aufweist.
  • Schließen Sie alle anderen Apps, um sicherzustellen, dass die gesamte Auswirkung der zu profilierenden App und nicht die der anderen Apps gemessen wird.

Verwenden kompilierter Bindungen

Kompilierte Bindungen verbessern die Datenbindungsleistung in .NET MAUI-Apps, indem Bindungsausdrücke zur Kompilierungszeit aufgelöst werden, anstatt zur Laufzeit mit Reflektion. Das Kompilieren eines Bindungsausdrucks generiert kompilierten Code, der in der Regel eine Bindung 8 bis 20 Mal schneller auflöst als bei Verwendung einer klassischen Bindung. Weitere Informationen finden Sie unter Compiled bindings.

Reduzieren unnötiger Bindungen

Verwenden Sie keine Bindungen für Inhalte, die problemlos statisch festgelegt werden können. Das Binden von Daten, die nicht gebunden werden müssen, bringt keine Vorteile mit sich, da Bindungen nicht kostengünstig sind. Das Festlegen von Button.Text = "Accept" ist beispielsweise mit weniger Mehraufwand verbunden als das Binden von Button.Text an die Eigenschaft string mit dem Wert „Akzeptieren“.

Auswählen des richtigen Layouts

Ein Layout, in dem mehrere untergeordnete Elemente angezeigt werden können, das jedoch nur über ein untergeordnetes Element verfügt, ist Vergeudung. Z. B. zeigt das folgende Beispiel ein VerticalStackLayout mit einem einzelnen untergeordneten Element:

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

Dies ist eine Verschwendung und das Element VerticalStackLayout sollte entfernt werden, wie im folgenden Beispiel gezeigt:

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

Darüber hinaus sollten Sie nicht versuchen, die Darstellung eines bestimmten Layouts durch Kombinieren anderer Layouts zu reproduzieren. Dies führt dazu, dass unnötige Layoutberechnungen durchgeführt werden. Versuchen Sie beispielsweise nicht, ein Grid Layout durch eine Kombination aus HorizontalStackLayout Elementen zu reproduzieren. Das folgende Beispiel veranschaulicht dieses Fehlverhalten:

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

Dies ist Vergeudung, da unnötige Layoutberechnungen durchgeführt werden. Stattdessen kann das gewünschte Layout besser über ein Grid erreicht werden, wie im folgenden Beispiel gezeigt:

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

Optimieren von Bildressourcen

Bilder gehören zu den speicherintensivsten Ressourcen, die Apps verwenden und werden häufig in hoher Auflösung erfasst. Dadurch entstehen zwar lebendige, detailreiche Bilder, aber Anwendungen, die solche Bilder anzeigen, benötigen in der Regel mehr CPU-Leistung zur Dekodierung des Bildes und mehr Speicher zum Speichern des dekodierten Bildes. Es ist unwirtschaftlich, ein hochauflösendes Bild im Speicher zu decodieren, wenn es anschließend für die Anzeige auf eine kleinere Größe herunterskaliert wird. Verringern Sie stattdessen die CPU-Auslastung und den Speicherbedarf, indem Sie Versionen der gespeicherten Bilder erstellen, die den voraussichtlichen Anzeigegrößen nahe kommen. Beispiel: Ein Bild, das in einer Listenansicht angezeigt wird, benötigt wahrscheinlich eine niedrigere Auflösung als ein Bild für den Vollbildmodus.

Die Bilder sollten außerdem nur erstellt werden, wenn erforderlich. Sie sollten freigegeben werden, sobald die App sie nicht mehr benötigt. Wenn eine Anwendung beispielsweise ein Bild anzeigt, indem sie die Daten aus einem Stream liest, muss sichergestellt werden, dass der Stream nur dann erstellt wird, wenn er benötigt wird, und dass der Stream freigegeben wird, wenn er nicht mehr benötigt wird. Dies kann erreicht werden, indem der Datenstrom bei Erstellung der Seite erstellt wird, oder bei Auslösung des Page.Appearing-Ereignisses, und der Datenstrom anschließend bei Auslösung des Page.Disappearing-Ereignisses verworfen wird.

Stellen Sie beim Herunterladen eines Bilds für die Anzeige mit der ImageSource.FromUri(Uri) Methode sicher, dass das heruntergeladene Bild für einen geeigneten Zeitraum zwischengespeichert wird. Weitere Informationen finden Sie unter Image caching.

Verkleinern des visuellen Baums

Durch eine Reduzierung der Anzahl von Elementen auf einer Seite wird der Seitenrenderer schneller. Es gibt zwei Haupttechniken, um dies zu erreichen. Bei der ersten Technik werden Elemente ausgeblendet, die nicht sichtbar sind. Die Eigenschaft IsVisible der einzelnen Elemente bestimmt, ob die Elemente Teil der visuellen Struktur sein sollten. Daher sollten Sie das Element entfernen oder die zugehörige Eigenschaft IsVisible auf false festlegen, wenn ein Element nicht sichtbar ist, da es ausgeblendet wird.

Bei der zweiten Technik werden unnötige Elemente entfernt. Im folgenden Beispiel wird ein Seitenlayout dargestellt, das mehrere Label Elemente enthält:

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

Dasselbe Seitenlayout kann wie im folgenden Beispiel gezeigt mit einer reduzierten Elementanzahl verwaltet werden:

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

Verringern der Größe des Ressourcenverzeichnisses der Anwendung

Alle Ressourcen, die in der App verwendet werden, sollten in ihrem Ressourcenverzeichnis gespeichert werden, um eine Duplizierung zu verhindern. Dadurch kann der Umfang des XAML reduziert werden, der in der gesamten App geparst werden muss. Das folgende Beispiel zeigt die HeadingLabelStyle Ressource, die in der gesamten App verwendet wird und daher im Ressourcenverzeichnis definiert ist:

<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, das für eine Seite spezifisch ist, sollte jedoch nicht in das Ressourcenverzeichnis der App aufgenommen werden, da die Ressourcen dann beim Start der App geparst werden, anstatt wenn sie von einer Seite benötigt werden. Wenn eine Ressource von einer anderen Seite als der Startseite verwendet wird, sollte sie in das Ressourcenverzeichnis für diese Seite aufgenommen werden, was dabei hilft, den XAML zu reduzieren, der beim App-Start geparst wird. Das folgende Beispiel zeigt die HeadingLabelStyle Ressource, die sich nur auf einer einzelnen Seite befindet und daher im Ressourcenverzeichnis der Seite definiert ist:

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

Weitere Informationen zu App-Ressourcen finden Sie unter Apps mit XAML gestalten.

Reduzieren der App-Größe

Wenn .NET MAUI Ihre Anwendung erstellt, kann ein Bindeglied namens ILLink verwendet werden, um die Gesamtgröße der Anwendung zu reduzieren. ILLink reduziert die Größe, indem der vom Compiler erstellte Zwischencode analysiert wird. Es entfernt nicht verwendete Methoden, Eigenschaften, Felder, Ereignisse, Strukturen und Klassen, um eine App zu erstellen, die nur Code- und Baugruppenabhängigkeiten enthält, die zum Ausführen erforderlich sind.

Weitere Informationen zum Konfigurieren des Verknüpfungsverhaltens finden Sie unter Verknüpfen einer Android-App, Verknüpfen einer iOS-App und Verknüpfen einer Mac Catalyst-App.

Verringern des App Aktivierungszeitraums

Alle Apps haben einen Aktivierungszeitraum. Damit ist die Zeit zwischen dem Anwendungsstart und dem Zeitpunkt gemeint, an dem die App zur Verwendung bereit ist. Während dieses Aktivierungszeitraums erhalten Benutzer einen ersten Eindruck der App. Daher ist es wichtig, die Aktivierungsdauer so kurz wie möglich zu halten, um einen positiven ersten Eindruck zu hinterlassen.

Vor der anfänglichen UI zeigt eine App einen Begrüßungsbildschirm an, um dem Benutzer zu signalisieren, dass sie gestartet wird. Wenn die App die anfängliche UI nicht schnell anzeigen kann, sollten Benutzer während des Aktivierungszeitraums auf dem Begrüßungsbildschirm über den Ladefortschritt informiert werden. So wird angezeigt, dass die App nicht abgestürzt ist. Dies könnte über eine Statusanzeige oder ein ähnliches Steuerelement erfolgen.

Während des Aktivierungszeitraums führen Apps Aktivierungslogik aus, die häufig das Laden und Verarbeiten von Ressourcen umfasst. Der Aktivierungszeitraum kann reduziert werden, indem sichergestellt wird, dass die erforderliche Ressourcen innerhalb der App verpackt und nicht remote abgerufen werden. Unter bestimmten Umständen müssen z.B. lokal gespeicherte Platzhalterdaten während des Aktivierungszeitraums geladen werden. Sobald die anfängliche UI angezeigt wird und der Benutzer mit der App interagieren kann, können die Platzhalterdaten nach und nach aus einer Remotequelle ersetzt werden. Darüber hinaus sollte die Aktivierungslogik der App nur die Arbeit ausführen, die erforderlich ist, damit der Benutzer die App nutzen kann. Dies kann hilfreich sein, wenn dadurch das Laden zusätzlicher Assemblys verzögert wird, da Assemblys bei der ersten Verwendung geladen werden.

Sorgfältiges Auswählen des Containers für die Abhängigkeitsinjektion

Container zur Injektion von Abhängigkeiten führen zu zusätzlichen Leistungseinschränkungen bei mobilen Apps. Das Registrieren und Auflösen von Typen mit einem Container wirkt sich negativ auf die Leistung aus, da der Container Reflektion zum Erstellen der einzelnen Typen verwendet, insbesondere dann, wenn Abhängigkeiten für jede Seitennavigation in der App rekonstruiert werden müssen. Wenn zahlreiche oder tiefe Abhängigkeiten vorhanden sind, können die Kosten für die Erstellung erheblich zunehmen. Darüber hinaus kann die Typregistrierung, die in der Regel während des App-Starts erfolgt, je nach verwendetem Container eine spürbare Auswirkung auf die Startzeit haben. Weitere Informationen zur Abhängigkeitsinjektion in .NET MAUI-Apps finden Sie unter Abhängigkeitsinjektion.

Als Alternative kann die Abhängigkeitsinjektion durch manuelle Implementierung mithilfe von Factorys leistungsstärker werden.

Erstellen von Shell-Apps

.NET MAUI Shell-Apps bieten eine übersichtliche Navigation auf der Grundlage von Flyouts und Registerkarten. Wenn die Benutzererfahrung Ihrer App mit Shell implementiert werden kann, ist es von Vorteil, dies zu tun. Shell-Apps tragen dazu bei, ein schlechtes Startverhalten zu vermeiden, da die Seiten bei Bedarf als Reaktion auf die Navigation erstellt werden und nicht erst beim Start der App, wie es bei Apps der Fall ist, die ein TabbedPage verwenden. Weitere Informationen finden Sie in der Shell-Übersicht.

Optimieren der ListView-Leistung

Bei Verwendung einer ListView müssen einige Benutzeroberflächen optimiert werden:

  • Initialisierung: Das Zeitintervall beginnt mit der Erstellung des Steuerelements und endet, wenn die Elemente auf dem Bildschirm angezeigt werden.
  • Bildlauf: Die Möglichkeit, einen Bildlauf durch die Liste durchzuführen, und die Sicherstellung, dass die Benutzeroberfläche nicht hinter Fingerbewegungen zurückbleibt.
  • Interaktion für das Hinzufügen, Löschen und Auswählen von Elementen.

Das ListView-Steuerelement erfordert eine Anwendung zur Bereitstellung von Daten und Zellvorlagen. Wie dies erreicht wird, hat großen Einfluss auf die Leistung des Steuerelements. Weitere Informationen finden Sie unter Cache-Daten.

Verwenden von asynchroner Programmierung

Durch die Verwendung asynchroner Programmierung kann die Reaktionsfähigkeit Ihrer Anwendung insgesamt verbessert und Leistungsengpässe können häufig vermieden werden. Das aufgabenbasierte asynchrone Muster ist in .NET das empfohlene Entwurfsmuster für asynchrone Vorgänge. Eine falsche Verwendung des TAPs kann jedoch zu nicht funktionierenden Anwendungen führen.

Grundlagen

Die folgenden allgemeinen Richtlinien sollten bei der Verwendung von TAP beachtet werden.

  • Machen Sie sich mit dem Aufgabenlebenszyklus vertraut, der von der TaskStatus-Enumeration dargestellt wird. Weitere Informationen finden Sie unter The meaning of TaskStatus (Die Bedeutung von TaskStatus) und Aufgabenstatus.
  • Verwenden Sie die Task.WhenAll-Methode, um asynchron auf den Abschluss mehrerer asynchroner Vorgänge zu warten, anstatt für mehrere asynchrone Vorgänge await einzeln zu verwenden. Weitere Informationen finden Sie unter Task.WhenAll.
  • Verwenden Sie die Task.WhenAny-Methode, um asynchron auf den Abschluss mehrerer asynchroner Vorgänge zu warten. Weitere Informationen finden Sie unter Task.WhenAny.
  • Verwenden Sie die Task.Delay-Methode, um ein Task-Objekt zu erstellen, das nach dem angegebenen Zeitpunkt abgeschlossen wird. Dies ist beispielsweise beim Abrufen von Daten und Verzögern von Benutzereingaben für einen vordefinierten Zeitraum nützlich. Weitere Informationen finden Sie unter Task.Delay.
  • Führen Sie rechenintensive, synchrone CPU-Vorgänge auf dem Threadpool mit der Task.Run-Methode aus. Diese Methode ist eine Verknüpfung für die TaskFactory.StartNew-Methode mit den optimalen Argumenten. Weitere Informationen finden Sie unter Task.Run.
  • Vermeiden Sie es, asynchrone Konstruktoren zu erstellen. Verwenden Sie stattdessen Lebenszyklusereignisse oder separate Initialisierungslogik, um await ordnungsgemäß für beliebige Initialisierungen auszuführen. Weitere Informationen finden Sie unter Asynchrone Konstruktoren auf blog.stephencleary.com.
  • Verwenden Sie das Lazy-Task-Muster, um zu vermeiden, dass Sie während des Starts der Anwendung auf den Abschluss asynchroner Vorgänge warten müssen. Weitere Informationen finden Sie unter AsyncLazy.
  • Erstellen Sie einen Aufgabenwrapper für vorhandene asynchrone Vorgänge, die keine aufgabenbasierten asynchronen Muster verwenden, indem Sie TaskCompletionSource<T>-Objekte erstellen. Diese Objekte erhalten die Vorteile der Task-Programmierbarkeit und ermöglichen es Ihnen, die Lebensdauer und den Abschluss der zugeordneten Task-Elemente zu steuern. Weitere Informationen finden Sie unter Das Wesen von TaskCompletionSource.
  • Geben Sie ein Task-Objekt zurück, anstatt ein erwartetes Task-Objekt zurückzugeben, wenn das Ergebnis eines asynchronen Vorgangs nicht verarbeitet werden muss. Aufgrund weniger Kontextwechsel ist dies leistungsfähiger.
  • Verwenden Sie die TPL-Datenflussbibliothek (Task Parallel Library) in Szenarios wie der Verarbeitung von Daten, sobald sie verfügbar sind, oder wenn Sie über mehrere Vorgänge verfügen, die asynchron miteinander kommunizieren müssen. Weitere Informationen finden Sie unter Datenfluss (Task Parallel Library).

UI

Die folgenden Richtlinien sollten bei der Verwendung von TAP mit UI-Steuerelementen beachtet werden:

  • Rufen Sie eine asynchrone Version einer API auf, wenn diese verfügbar ist. So wird der UI-Thread nicht blockiert und die Benutzerfreundlichkeit der App optimiert.

  • Aktualisieren Sie Benutzeroberflächenelemente mit Daten aus asynchronen Vorgängen auf dem Benutzeroberflächenthread, um die Auslösung von Ausnahmen zu vermeiden. Updates der ListView.ItemsSource-Eigenschaft werden jedoch automatisch an den Benutzeroberflächenthread gemarshallt. Informationen dazu, wie bestimmt wird, ob Code im UI-Thread ausgeführt wird, finden Sie unter Thread auf dem UI-Thread erstellen.

    Wichtig

    Alle Steuerelementeigenschaften, die per Datenbindung aktualisiert werden, werden automatisch an den Benutzeroberflächenthread gemarshallt.

Fehlerbehandlung

Die folgenden Richtlinien zum Umgang mit Fehlern sollten bei der Verwendung von TAP beachtet werden:

  • Erfahren Sie mehr über die asynchrone Ausnahmebehandlung. Nicht behandelte Ausnahmen, die von asynchron ausgeführtem Code ausgelöst werden, werden an den aufrufenden Thread zurückgegeben. Bestimmte Szenarios sind hiervon ausgenommen. Weitere Informationen finden Sie unter Ausnahmebehandlung (Task Parallel Library).
  • Vermeiden Sie es, async void-Methoden zu erstellen. Erstellen Sie stattdessen async Task-Methoden. Diese Methoden erleichtern die Fehlerbehandlung, Erstellbarkeit und Testbarkeit. Eine Ausnahme von dieser Richtlinie bilden asynchrone Ereignishandler, die void zurückgeben müssen. Weitere Informationen finden Sie unter Vermeiden von asynchronen Voids.
  • Kombinieren Sie blockierenden und asynchronen Code nicht, indem Sie die Methoden Task.Wait, Task.Result oder GetAwaiter().GetResult aufrufen, da dies zu Deadlocks führen kann. Wenn Sie jedoch gegen diese Führungslinie verstoßen müssen, besteht der empfohlene Ansatz darin, die GetAwaiter().GetResult-Methode aufzurufen, da Aufgabenausnahmen dabei beibehalten werden. Weitere Informationen finden Sie unter Durchgehend asynchron und Behandlung von Aufgabenausnahmen in .NET 4.5.
  • Verwenden Sie nach Möglichkeit die ConfigureAwait-Methode, um kontextfreien Code zu erstellen. Kontextfreier Code führt zu einer besseren Leistung bei mobilen Apps und ist eine nützliche Technik zur Vermeidung von Deadlocks beim Arbeiten mit einer teilweise asynchronen Codebasis. Weitere Informationen finden Sie unter Konfigurieren des Kontexts.
  • Verwenden Sie Fortsetzungsaufgaben für Funktionalitäten wie das Verarbeiten von Ausnahmen, die vom vorherigen asynchronen Vorgang ausgelöst wurden, und brechen Sie eine Fortsetzung ab, bevor sie beginnt oder während sie ausgeführt wird. Weitere Informationen finden Sie unter Verketten von Aufgaben mithilfe von kontinuierlichen Aufgaben.
  • Verwenden Sie eine asynchrone ICommand-Implementierung, wenn asynchrone Vorgänge über ICommand aufgerufen werden. Dadurch wird sichergestellt, dass alle Ausnahmen in der asynchronen Befehlslogik behandelt werden können. Weitere Informationen finden Sie unter Asynchrone Programmierung: Vorlagen für asynchrone MVVM-Anwendungen: Befehle.

Verzögern der Kosten der Objekterstellung

Mit der verzögerten Initialisierung kann die Erstellung eines Objekts bis zu seiner ersten Verwendung hinausgezögert werden. Diese Methode wird vorwiegend zur Steigerung der Leistung, der Vermeidung von Berechnungen und der Verringerung von Speicheranforderungen verwendet.

Erwägen Sie die Verwendung der trägen Initialisierung für Objekte, deren Erstellung in den folgenden Szenarien teuer ist:

  • Wenn die App das Objekt möglicherweise nicht verwendet
  • Wenn andere kostenintensive Vorgänge abgeschlossen werden müssen, bevor das Objekt erstellt wird

Mit der Lazy<T> Klasse kann wie im folgenden Beispiel gezeigt ein verzögert initialisierter Typ definiert werden:

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)
{
    ...
}

Die verzögerte Initialisierung tritt beim ersten Zugriff auf die Lazy<T>.Value-Eigenschaft auf. Beim ersten Zugriff wird der umschlossene Typ erstellt, zurückgegeben und für einen späteren Zugriff gespeichert.

Weitere Informationen zur verzögerten Initialisierung finden Sie unter Verzögerte Initialisierung.

IDisposable Ressourcen freigeben

Die IDisposable-Schnittstelle stellt einen Mechanismus zum Freigeben von Ressourcen und eine Dispose-Methode bereit, die zum expliziten Freigeben von Ressourcen implementiert werden sollte. IDisposable ist kein Destruktor und sollte nur unter den folgenden Umständen implementiert werden:

  • Wenn die Klasse nicht verwaltete Ressourcen besitzt. Typische, nicht verwaltete Ressourcen, die eine Freigabe erfordern, sind Dateien, Datenströme und Netzwerkverbindungen.
  • Wenn die Klasse nicht verwaltete IDisposable-Ressourcen besitzt.

Typconsumer können anschließend die IDisposable.Dispose-Implementierung aufrufen, um Ressourcen freizugeben, wenn die Instanz nicht mehr benötigt wird. Dies erreichen Sie auf zweierlei Art und Weise:

  • Durch Umschließen des IDisposable-Objekt in einer using-Anweisung
  • Durch Umschließen des Aufrufs von IDisposable.Dispose in einem try/finally-Block

Umschließen des IDisposable-Objekts in einer using-Anweisung

Im folgenden Beispiel wird veranschaulicht, wie ein IDisposable Objekt in einer using Anweisung umschlossen wird:

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

Die StreamReader-Klasse implementiert IDisposable, und die using-Anweisung stellt eine praktische Syntax bereit, die die Methode StreamReader.Dispose für das StreamReader-Objekt aufruft, bevor es den Bereich verlässt. Innerhalb des using-Blocks ist das StreamReader-Objekt schreibgeschützt und kann nicht neu zugewiesen werden. Die using-Anweisung stellt auch sicher, dass die Dispose-Methode selbst dann aufgerufen wird, wenn der Compiler die Zwischensprache (Intermediate Language, IL) für einen try/finally-Block implementiert und dabei eine Ausnahme auftritt.

Den Aufruf von IDisposable.Dispose in einen try/finally-Block verpacken

Im folgenden Beispiel wird veranschaulicht, wie der Aufruf von IDisposable.Dispose in einem try/finally-Block umschlossen wird:

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

Die StreamReader-Klasse implementiert IDisposable, und der finally-Block ruft die Methode StreamReader.Dispose auf, um die Ressource freizugeben. Weitere Informationen finden Sie unter System.IDisposable.

Abbestellen von Ereignissen

Um Speicherverluste zu verhindern, sollten Ereignisabonnements abbestellt werden, bevor das abonnierende Objekt verworfen wird. Bis zur Abbestellung verweist der Delegat des Ereignisses im Veröffentlichungsobjekt auf einen Delegaten, der den Ereignishandler des Abonnenten einkapselt. Solange das Veröffentlichungsobjekt diesen Verweis enthält, wird die Garbage Collection den vom abonnierenden Objekt genutzten Speicher nicht freigeben.

Im folgenden Beispiel wird dargestellt, wie ein Ereignisabonnement abbestellt wird:

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

Die Subscriber-Klasse bestellt das Abonnement des Ereignisses in der Dispose-Methode ab.

Verweiszyklen können auch im Zusammenhang mit Ereignishandlern und Lambdasyntax auftreten, so wie Lambdaausdrücke auf Objekte verweisen und sie im Speicher resident halten können. Daher kann ein Verweis auf die anonyme Methode in einem Feld gespeichert und verwendet werden, um sich von dem Ereignis abzumelden, wie im folgenden Beispiel gezeigt:

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

Das _handler Feld enthält den Verweis auf die anonyme Methode und dient zum An- und Abmelden von Ereignisabonnements.

Vermeiden sie starke Zirkelbezüge auf iOS und Mac Catalyst

In einigen Situationen ist es möglich, starke Verweiszyklen zu erstellen, die Objekte davor schützen, dass ihr Speicher vom Garbage Collector wieder zurückgefordert wird. Nehmen wir zum Beispiel den Fall, dass eine NSObject-abgeleitete Unterklasse, wie eine Klasse, die von UIView erbt, zu einem NSObject-abgeleiteten Container hinzugefügt wird und von Objective-C stark referenziert wird, wie im folgenden Beispiel gezeigt:

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

Wenn dieser Code die Container-Instanz erstellt, enthält das C#-Objekt einen starken Verweis auf ein Objective-C-Objekt. Die MyView-Instanz enthält auch einen ähnlichen, starken Verweis auf ein Objective-C-Objekt.

Zudem erhöht der Aufruf von container.AddSubview den Verweiszähler für die nicht verwaltete MyView-Instanz. In diesem Fall erstellt die .NET für iOS-Laufzeitumgebung eine GCHandle-Instanz, um das MyView-Objekt im verwalteten Code zu erhalten, da es keine Garantie dafür gibt, dass verwaltete Objekte einen Verweis auf dieses Objekt behalten. Im Hinblick auf den verwalteten Code würde das MyView-Objekt nach dem AddSubview(UIView)-Aufruf zurückgefordert werden, wäre da nicht GCHandle.

Das nicht verwaltete MyView-Objekt verfügt über einen GCHandle-Zeiger auf das verwaltete Objekt, bekannt als starker Link. Das verwaltete Objekt enthält einen Verweis auf die Container-Instanz. Die Container-Instanz enthält wiederum einen verwalteten Verweis auf das MyView-Objekt.

In Fällen, in denen ein enthaltenes Objekt einen Link zu seinem Container speichert, stehen mehrere Optionen für den Umgang mit den Zirkelverweisen zur Verfügung:

  • Vermeiden Sie, dass der Zirkelverweis einen schwachen Verweis auf den Container beibehält.
  • Rufen Sie Dispose für die Objekte auf.
  • Unterbrechen Sie den Zyklus manuell, indem Sie den Link zum Container auf nullsetzen.
  • Entfernen Sie die enthaltenen Objekte manuell aus dem Container.

Verwenden von schwachen Verweise

Eine Möglichkeit, einen Zyklus zu verhindern, besteht in der Verwendung eines Weak-Verweises vom untergeordneten zum übergeordneten Element. Der obige Code könnte z. B. wie im folgenden Beispiel dargestellt sein:

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

Hier halt das darin enthaltene Objekt das übergeordnete Element nicht aktiv. Allerdings hält das übergeordnete Element das untergeordnete Element während des erfolgten container.AddSubView-Aufrufs aktiv.

Dies geschieht auch in iOS-APIs, die das Delegaten- oder Datenquellenmuster verwenden, wobei eine Peer-Klasse die Implementierung enthält. Wenn Sie z. B. die Delegate Eigenschaft oder DataSource in der UITableView Klasse festlegen.

Bei Klassen, die ausschließlich zum Zweck der Implementierung eines Protokolls erstellt werden, z.B. die IUITableViewDataSource-Klasse, können Sie, anstatt eine Unterklasse zu erstellen, nur die Schnittstelle in der Klasse implementieren, die Methode überschreiben und this die DataSource-Eigenschaft zuweisen.

Verwerfen von Objekten mit starken Verweisen

Wenn ein starker Verweis vorhanden ist, und es schwierig ist, die Abhängigkeit zu entfernen, führen Sie eine Dispose-Methode zum Deaktivieren des übergeordneten Zeigers aus.

Bei Containern überschreiben Sie die Methode Dispose, um die enthaltenen Objekte zu entfernen, wie im folgenden Beispiel gezeigt:

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

Für ein untergeordnetes Objekt, das einen starken Verweis zum übergeordneten Objekt beibehält, deaktivieren Sie den Verweis auf das übergeordnete Element in der Dispose-Implementierung:

class MyChild : UIView
{
    MyContainer _container;

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

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