Dela via


Förbättra appprestanda

Dåliga appprestanda presenteras på många sätt. Det kan få en app att verka vara svarslös, orsaka långsam rullning och minska enhetens batteritid. Att optimera prestanda innebär dock mer än att bara implementera effektiv kod. Användarens upplevelse av appprestanda måste också beaktas. Om du till exempel ser till att åtgärder körs utan att hindra användaren från att utföra andra aktiviteter kan det hjälpa till att förbättra användarens upplevelse.

Det finns många tekniker för att öka prestanda och upplevda prestanda för .NET Multi-Platform App UI-appar (.NET MAUI). Tillsammans kan dessa tekniker avsevärt minska mängden arbete som utförs av en processor och mängden minne som förbrukas av en app.

Använda en profilerare

När du utvecklar en app är det viktigt att bara försöka optimera koden när den har profilerats. Profilering är en teknik för att avgöra var kodoptimeringar kommer att ha störst effekt när det gäller att minska prestandaproblem. Profileraren spårar appens minnesanvändning och registrerar körningstiden för metoder i appen. Dessa data hjälper till att navigera genom körningsvägarna för appen och körningskostnaden för koden, så att de bästa optimeringsmöjligheterna kan identifieras.

.NET MAUI-appar kan profileras med dotnet-trace på Android, iOS och Mac och Windows samt med PerfView i Windows. Mer information finns i Profilering av .NET MAUI-appar.

Följande metodtips rekommenderas när du profilerar en app:

  • Undvik att profilera en app i en simulator eftersom simulatorn kan förvränga appens prestanda.
  • Helst bör profilering utföras på en mängd olika enheter, eftersom prestandamätningar på en enhet inte alltid visar prestandaegenskaperna för andra enheter. Profilering bör dock utföras på en enhet som har den lägsta förväntade specifikationen.
  • Stäng alla andra appar för att se till att den fulla effekten av appen som profileras mäts i stället för de andra apparna.

Använda kompilerade bindningar

Kompilerade bindningar förbättrar databindningsprestanda i .NET MAUI-appar genom att lösa bindningsuttryck vid kompileringstiden i stället för vid körning med reflektion. Kompilering av ett bindningsuttryck genererar kompilerad kod som vanligtvis löser en bindning 8–20 gånger snabbare än att använda en klassisk bindning. Mer information finns i Kompilerade bindningar.

Minska onödiga bindningar

Använd inte bindningar för innehåll som enkelt kan anges statiskt. Det finns ingen fördel med att binda data som inte behöver bindas, eftersom bindningar inte är kostnadseffektiva. Till exempel har inställningen Button.Text = "Accept" mindre omkostnader än att binda Button.Text till en viewmodel-egenskap string med värdet "Acceptera".

Välj rätt layout

En layout som kan visa flera underordnade objekt, men som bara har ett enda barn, är slösaktig. I följande exempel visas en VerticalStackLayout med ett enda barn.

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

Detta är slöseri och VerticalStackLayout elementet bör tas bort, som du ser i följande exempel:

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

Försök inte att återskapa utseendet på en specifik layout med hjälp av kombinationer av andra layouter, eftersom detta resulterar i onödiga layoutberäkningar som utförs. Försök till exempel inte återskapa en Grid layout med hjälp av en kombination av HorizontalStackLayout element. I följande exempel visas ett exempel på den här dåliga metoden:

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

Detta är slöseri eftersom onödiga layoutberäkningar utförs. I stället kan den önskade layouten uppnås bättre med hjälp av en Grid, som du ser i följande exempel:

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

Optimera avbildningsresurser

Bilder är några av de dyraste resurserna som appar använder och fångas ofta med höga upplösningar. Även om detta skapar livfulla bilder full av detaljer, kräver appar som visar sådana bilder vanligtvis mer CPU-användning för att avkoda bilden och mer minne för att lagra den avkodade bilden. Det är slöseri att avkoda en högupplöst bild i minnet när den ska skalas ned till en mindre storlek för visning. Minska i stället processoranvändningen och minnesfotavtrycket genom att skapa versioner av lagrade bilder som ligger nära de förväntade visningsstorlekarna. Till exempel bör en bild som visas i en listvy troligen vara en lägre upplösning än en bild som visas i helskärmsläge.

Dessutom bör bilder endast skapas när det behövs och bör släppas så snart appen inte längre kräver dem. Om en app till exempel visar en bild genom att läsa dess data från en dataström, ska du se till att strömmen endast skapas vid behov och att den släpps när den inte längre behövs. Detta kan uppnås genom att skapa strömmen när sidan skapas, eller när den Page.Appearing händelsen utlöses och sedan bortskaffa strömmen när den Page.Disappearing händelsen utlöses.

När du laddar ned en bild för visning med metoden ImageSource.FromUri(Uri) kontrollerar du att den nedladdade bilden cachelagras under en lämplig tid. För mer information, se cachelagring av bild.

Minska antalet element på en sida

Om du minskar antalet element på en sida blir sidåtergivningen snabbare. Det finns två huvudsakliga metoder för att uppnå detta. Det första är att dölja element som inte är synliga. Egenskapen IsVisible för varje element avgör om elementet ska visas på skärmen. Om ett element inte visas på grund av att det är dolt bakom andra element tar du antingen bort elementet eller anger dess IsVisible egenskap till false. Om du ställer in egenskapen IsVisible på ett element till false behåller elementet i det visuella trädet, men exkluderar det från renderings- och layoutberäkningar.

Den andra tekniken är att ta bort onödiga element. Följande visar till exempel en sidlayout som innehåller flera Label element:

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

Samma sidlayout kan bibehållas med ett reducerat antal element, vilket visas i följande exempel:

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

Minska storleken på programresursordlistan

Alla resurser som används i hela appen bör lagras i appens resursordlista för att undvika duplicering. Detta hjälper till att minska mängden XAML som måste parsas i hela appen. I följande exempel visas den HeadingLabelStyle resursen, som används i hela appen och så definieras i appens resursordlista:

<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 som är specifikt för en sida bör dock inte ingå i appens resursordlista, eftersom resurserna sedan parsas vid appstart i stället för när det krävs av en sida. Om en resurs används av en sida som inte är startsidan bör den placeras i resursordlistan för den sidan, vilket bidrar till att minska den XAML som parsas när appen startar. I följande exempel visas den HeadingLabelStyle resursen, som bara finns på en enda sida och så definieras i sidans resursordlista:

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

Mer information om appresurser finns i Style apps using XAML.

Minska appens storlek

När .NET MAUI skapar din app kan en länkare med namnet ILLink användas för att minska appens totala storlek. ILLink minskar storleken genom att analysera den mellanliggande kod som kompilatorn producerar. Den tar bort oanvända metoder, egenskaper, fält, händelser, structs och klasser för att skapa en app som endast innehåller kod- och sammansättningsberoenden som krävs för att köra appen.

Mer information om hur du konfigurerar länkningsbeteendet finns i Länka en Android-app, Länka en iOS-appoch Länka en Mac Catalyst-app.

Minska appaktiveringsperioden

Alla appar har en aktiveringsperiod, vilket är tiden mellan när appen startas och när appen är redo att användas. Den här aktiveringsperioden ger användarna sitt första intryck av appen, så det är viktigt att minska aktiveringsperioden och användarens uppfattning om den, för att de ska få ett gynnsamt första intryck av appen.

Innan en app visar sitt ursprungliga användargränssnitt bör den tillhandahålla en välkomstskärm för att indikera för användaren att appen startas. Om appen inte snabbt kan visa sitt ursprungliga användargränssnitt bör välkomstskärmen användas för att informera användaren om förloppet under aktiveringsperioden för att försäkra att appen inte har hängt sig. Den här försäkran kan vara en förloppsindikator eller liknande kontroll.

Under aktiveringsperioden kör appar aktiveringslogik, vilket ofta omfattar inläsning och bearbetning av resurser. Aktiveringsperioden kan minskas genom att se till att nödvändiga resurser paketeras i appen i stället för att hämtas via fjärranslutning. I vissa fall kan det till exempel vara lämpligt under aktiveringsperioden att läsa in lokalt lagrade platshållardata. När det första användargränssnittet visas och användaren kan interagera med appen kan platshållardata sedan ersättas progressivt från en fjärrkälla. Dessutom bör appens aktiveringslogik endast utföra det arbete som krävs för att låta användaren börja använda appen. Detta kan hjälpa om det fördröjer inläsningen av ytterligare sammansättningar, eftersom sammansättningar läses in första gången de används.

Välj en container för beroendeinmatning noggrant

Containrar för beroendeinmatning inför ytterligare prestandabegränsningar i mobilappar. Registrering och matchning av typer med en container har en prestandakostnad på grund av containerns användning av reflektion för att skapa varje typ, särskilt om beroenden rekonstrueras för varje sidnavigering i appen. Om det finns många eller djupa beroenden kan kostnaden för att skapa öka avsevärt. Dessutom kan typregistrering, som vanligtvis sker under appstart, ha en märkbar inverkan på starttiden, beroende på vilken container som används. Mer information om beroendeinmatning i .NET MAUI-appar finns i Beroendeinmatning.

Som ett alternativ kan beroendeinmatning göras mer högpresterande genom att implementera den manuellt med hjälp av fabriker.

Skapa Shell-appar

.NET MAUI Shell-appar ger en åsiktsbaserad navigeringsupplevelse baserat på utfällbara objekt och flikar. Om appanvändarupplevelsen kan implementeras med Shell är det fördelaktigt att göra det. Shell-appar hjälper till att undvika en dålig startupplevelse eftersom sidor skapas på begäran som svar på navigeringen i stället för vid appstart, vilket sker med appar som använder en TabbedPage. Mer information finns i Shell-översikt.

Optimera ListView-prestanda

När du använder ListViewfinns det ett antal användarupplevelser som bör optimeras:

  • Initiering – tidsintervallet som börjar när kontrollen skapas och slutar när objekt visas på skärmen.
  • Rullning – möjligheten att bläddra igenom listan och se till att användargränssnittet inte släpar efter pekgester.
  • Interaktion för att lägga till, ta bort och välja objekt.

Den ListView kontrollen kräver att en app tillhandahåller data- och cellmallar. Hur detta uppnås kommer att ha stor inverkan på kontrollens prestanda. Mer information finns i Cache-data.

Använda asynkron programmering

Appens övergripande svarstider kan förbättras och flaskhalsar för prestanda undviks ofta med hjälp av asynkron programmering. I .NET är aktivitetsbaserat asynkront mönster (TAP) det rekommenderade designmönstret för asynkrona åtgärder. Om TAP används felaktigt, kan det dock leda till ineffektiva appar.

Grunderna

Följande allmänna riktlinjer bör följas när du använder TAP:

  • Förstå uppgiftslivscykeln, som representeras av TaskStatus uppräkning. Mer information finns i Innebörden av TaskStatus och Aktivitetsstatus.
  • Använd metoden Task.WhenAll för att asynkront vänta på att flera asynkrona åtgärder ska slutföras, i stället för att individuellt await en serie asynkrona åtgärder. Mer information finns i Task.WhenAll.
  • Använd metoden Task.WhenAny för att asynkront vänta tills någon av flera asynkrona åtgärder har slutförts. Mer information finns i Task.WhenAny.
  • Använd metoden Task.Delay för att skapa ett Task objekt som har slutförts efter den angivna tiden. Detta är användbart för scenarier som datainsamling och att fördröja hanteringen av användarindata under en fördefinierad tid. Mer information finns i Task.Delay.
  • Kör intensiva synkrona CPU-åtgärder i trådpoolen med metoden Task.Run. Den här metoden är en genväg för metoden TaskFactory.StartNew, med de mest optimala argumenten inställda. Mer information finns i Task.Run.
  • Undvik att försöka skapa asynkrona konstruktorer. Använd i stället livscykelhändelser eller separat initieringslogik för att korrekt await alla initieringar. Mer information finns i Async Constructors på blog.stephencleary.com.
  • Använd det lata aktivitetsmönstret för att undvika att vänta på att asynkrona åtgärder ska slutföras under appstarten. Mer information finns i AsyncLazy.
  • Skapa en uppgiftshantering för befintliga asynkrona åtgärder som inte använder TAP genom att skapa TaskCompletionSource<T> objekt. Dessa objekt får fördelarna med Task programmerbarhet. De gör att du kan kontrollera livslängden och slutförandet av den associerade Task. Mer information finns i The Nature of TaskCompletionSource.
  • Returnera ett Task objekt i stället för att returnera ett inväntat Task objekt när det inte finns något behov av att bearbeta resultatet av en asynkron åtgärd. Detta är mer högpresterande på grund av att mindre kontextväxling utförs.
  • Använd TPL-dataflödesbiblioteket (Task Parallel Library) i scenarier som att bearbeta data när de blir tillgängliga, eller när du har flera åtgärder som måste kommunicera med varandra asynkront. Mer information finns i Dataflöde (Aktivitetsparallellt bibliotek).

användargränssnitt

Följande riktlinjer bör följas när du använder TAP med användargränssnittskontroller:

  • Anropa en asynkron version av ett API, om det är tillgängligt. Detta kommer att hålla användargränssnittstråden avblockerad, vilket hjälper till att förbättra användarens upplevelse av appen.

  • Uppdatera gränssnittselement med data från asynkrona åtgärder i användargränssnittstråden för att undvika att undantag utlöses. Uppdateringar av egenskapen ListView.ItemsSource överförs dock automatiskt till användargränssnittstråden. Information om hur du avgör om kod körs i användargränssnittstråden finns i Skapa en tråd i användargränssnittstråden.

    Viktig

    Alla kontrollegenskaper som uppdateras via databindning konverteras automatiskt till användargränssnittstråden.

Felhantering

Följande riktlinjer för felhantering bör följas när du använder TAP:

  • Läs mer om asynkron undantagshantering. Ohanterade undantag som genereras av kod som körs asynkront sprids tillbaka till den anropande tråden, förutom i vissa scenarier. För mer information, se Undantagshantering (Task Parallel Library).
  • Undvik att skapa async void metoder och skapa i stället async Task metoder. Dessa möjliggör enklare felhantering, kompositionalitet och testbarhet. Undantaget till den här riktlinjen är asynkrona händelsehanterare, som måste returnera void. För mer information, se Undvik Async Void.
  • Blanda inte blockering och asynkron kod genom att anropa metoderna Task.Wait, Task.Resulteller GetAwaiter().GetResult eftersom de kan leda till ett dödläge. Men om dessa riktlinjer måste brytas är det förordade tillvägagångssättet att anropa metoden GetAwaiter().GetResult eftersom den bevarar uppgiftsavvikelserna. Mer information finns i Async All the Way och Task Exception Handling i .NET 4.5.
  • Använd metoden ConfigureAwait när det är möjligt för att skapa kontextfri kod. Kontextfri kod har bättre prestanda för mobilappar och är en användbar teknik för att undvika dödläge när du arbetar med en delvis asynkron kodbas. Mer information finns i Konfigurera kontext.
  • Använd fortsättningsaktiviteter för funktioner som att hantera undantag som utlöstes av den tidigare asynkrona åtgärden och avbryta en fortsättning innan den startar eller när den körs. För mer information, se Kedja uppgifter genom att använda kontinuerliga uppgifter.
  • Använd en asynkron ICommand implementering när asynkrona åtgärder anropas från ICommand. Detta säkerställer att alla undantag i den asynkrona kommandologiken kan hanteras. Mer information finns i Async Programming: Patterns for Asynchronous MVVM Applications: Commands.

Fördröja kostnaden för att skapa objekt

Lat initiering kan användas för att skjuta upp skapandet av ett objekt tills det först används. Den här tekniken används främst för att förbättra prestanda, undvika beräkning och minska minneskraven.

Överväg att använda lat initiering för objekt som är dyra att skapa i följande scenarier:

  • Appen kanske inte använder objektet.
  • Andra dyra åtgärder måste slutföras innan objektet skapas.

Klassen Lazy<T> används för att definiera en typ med fördröjd initialisering, enligt följande exempel:

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

Lat initiering sker första gången egenskapen Lazy<T>.Value används. Den omslutna typen skapas och returneras vid första åtkomsten och lagras för eventuell framtida åtkomst.

Mer information om lat initiering finns i Lazy Initialization.

Frigöra IDisposable-resurser

Gränssnittet IDisposable tillhandahåller en mekanism för att frigöra resurser. Den tillhandahåller en Dispose metod som ska implementeras för att explicit frigöra resurser. IDisposable är inte en destructor och bör endast genomföras under följande omständigheter:

  • När en klass äger resurser som inte hanteras. Typiska ohanterade resurser som kräver frigöring är filer, strömmar och nätverksanslutningar.
  • När klassen äger de hanterade IDisposable resurserna.

Typkonsumenter kan sedan anropa implementeringen av IDisposable.Dispose för att frigöra resurser när instansen inte längre krävs. Det finns två metoder för att uppnå detta:

  • Genom att omsluta IDisposable-objektet i en using-instruktion.
  • Genom att omsluta anropet till IDisposable.Dispose i ett try/finally-block.

Omslut IDisposable-objektet med en using-instruktion

I följande exempel visas hur du omsluter ett IDisposable objekt i en using-instruktion:

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

Klassen StreamReader implementerar IDisposable, och using-instruktionen innehåller en praktisk syntax som anropar StreamReader.Dispose-metoden på StreamReader-objektet innan den hamnar utanför omfånget. I using-blocket är StreamReader-objektet skrivskyddat och kan inte tilldelas igen. using-instruktionen säkerställer också att Dispose-metoden anropas även om ett undantag inträffar, eftersom kompilatorn implementerar mellanliggande språk (IL) för ett try/finally-block.

Omslut anropet till IDisposable.Dispose med ett try/finally-block

I följande exempel visas hur du omsluter anropet till IDisposable.Dispose i ett try/finally block:

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

Klassen StreamReader implementerar IDisposableoch finally-blocket anropar metoden StreamReader.Dispose för att frigöra resursen. Mer information finns i IDisposable Interface.

Avsluta prenumerationen på händelser

För att förhindra minnesläckor bör händelser avbrytas innan prenumerantobjektet tas bort. Innan prenumerationen på händelsen avslutas, har delegaten för händelsen i det publicerande objektet en referens till den delegat som kapslar in prenumerantens händelsehanterare. Så länge publiceringsobjektet innehåller den här referensen kommer skräpinsamling inte att frigöra prenumerantobjektets minne.

I följande exempel visas hur du avbryter prenumerationen på en händelse:

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

Klassen Subscriber avregistrerar händelsen i sin Dispose-metod.

Referenscykler kan också inträffa när du använder händelsehanterare och lambda-syntax, eftersom lambda-uttryck kan referera till och hålla objekt vid liv. Därför kan en referens till den anonyma metoden lagras i ett fält och användas för att avbryta prenumerationen på händelsen, enligt följande exempel:

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

Fältet _handler underhåller referensen till den anonyma metoden och används för händelseprenumeration och avprenumeration.

Undvik starka cirkelreferenser på iOS och Mac Catalyst

I vissa situationer är det möjligt att skapa starka referenscykler som kan hindra objekt från att få sitt minne återkrävat av skräpinsamlaren. Tänk till exempel på det fall där en NSObject-derived-underklass, till exempel en klass som ärver från UIView, läggs till i en NSObject-derived-container och starkt refereras från Objective-C, som du ser i följande exempel:

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

När den här koden skapar Container-instansen har C#-objektet en stark referens till ett Objective-C objekt. På samma sätt har MyView-instansen också en stark referens till ett Objective-C objekt.

Dessutom ökar anropet till container.AddSubview referensantalet för den ohanterade MyView-instansen. När detta händer skapar .NET för iOS-körning en GCHandle instans för att hålla MyView-objektet i hanterad kod vid liv, eftersom det inte finns någon garanti för att hanterade objekt kommer att behålla en referens till det. Ur ett hanterat kodperspektiv skulle objektet MyView återtas efter AddSubview(UIView)-anrop om det inte var för GCHandle.

Det ohanterade MyView-objektet kommer att ha en GCHandle som pekar mot det hanterade objektet, som är känd som en stark länk. Det hanterade objektet innehåller en referens till den Container instansen. I sin tur har Container-instansen en hanterad referens till MyView-objektet.

I de fall ett inneslutet objekt har en länk till containern finns det flera tillgängliga alternativ för att hantera cirkelreferensen:

  • Undvik cirkelreferensen genom att behålla en svag referens till containern.
  • Anropa Dispose på objektet.
  • Bryt det slutna kretsloppet manuellt genom att ange länken till containern till null.
  • Ta bort det inneslutna objektet manuellt från containern.

Använda svaga referenser

Ett sätt att förhindra en avhängighetscykel är att använda en svag referens från barnet till föräldern. Koden ovan kan till exempel vara som du ser i följande exempel:

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

Här kommer det inneslutna objektet inte att hålla föräldern vid liv. Föräldern håller dock barnet vid liv genom anropet till container.AddSubView.

Detta sker också i iOS-API:er som använder delegerings- eller datakällmönstret, där en jämställd klass innehåller implementeringen. När du till exempel anger egenskapen Delegate eller DataSource i klassen UITableView.

När det gäller klasser som skapas enbart för att implementera ett protokoll, till exempel IUITableViewDataSource, kan du i stället för att skapa en underklass bara implementera gränssnittet i klassen och åsidosätta metoden och tilldela egenskapen DataSource till this.

Bortskaffa objekt med starka referenser

Om det finns en stark referens och det är svårt att ta bort beroendet gör du en Dispose-metod för att rensa den överordnade pekaren.

För containrar åsidosätter du metoden Dispose för att ta bort de inneslutna objekten, enligt följande exempel:

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

För ett underordnat objekt som behåller en stark referens till dess överordnade objekt avmarkerar du referensen till den överordnade i Dispose implementeringen:

class MyChild : UIView
{
    MyContainer _container;

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

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