Migliorare le prestazioni dell'app
Le prestazioni delle app scarse si presentano in molti modi. Può rendere un'app non risponde, può causare uno scorrimento lento e può ridurre la durata della batteria del dispositivo. Tuttavia, l'ottimizzazione delle prestazioni implica più della semplice implementazione di codice efficiente. È necessario considerare anche l'esperienza utente delle prestazioni dell'app. Ad esempio, assicurarsi che le operazioni vengano eseguite senza impedire all'utente di eseguire altre attività può contribuire a migliorare l'esperienza dell'utente.
Esistono molte tecniche per aumentare le prestazioni e le prestazioni percepite delle app .NET Multipiattaforma (.NET MAUI). Collettivamente queste tecniche possono ridurre notevolmente la quantità di lavoro eseguita da una CPU e la quantità di memoria utilizzata da un'app.
Usare un profiler
Quando si sviluppa un'app, è importante provare a ottimizzare il codice solo dopo che è stato profilato. La profilatura è una tecnica che consente di determinare i punti del codice in cui l'ottimizzazione può influire maggiormente sulla riduzione dei problemi di prestazioni. Il profiler tiene traccia dell'utilizzo della memoria dell'app e registra il tempo di esecuzione dei metodi nell'app. Questi dati consentono di spostarsi tra i percorsi di esecuzione dell'app e il costo di esecuzione del codice, in modo che sia possibile individuare le migliori opportunità di ottimizzazione.
Le app MAUI .NET possono essere profilate usando dotnet-trace
Android, iOS e Mac e Windows e con PerfView in Windows. Per altre informazioni, vedere Profilatura di app MAUI .NET.
Per la profilatura di un'app è opportuno seguire le procedure consigliate riportate di seguito:
- Evitare di profilare un'app in un simulatore, perché il simulatore può distorcere le prestazioni dell'app.
- In linea di principio, la profilatura deve essere eseguita in un'ampia gamma di dispositivi. La misurazione delle prestazioni in un dispositivo, infatti, non è necessariamente indicativa delle caratteristiche delle prestazioni di altri dispositivi. La profilatura, comunque, deve essere eseguita almeno in un dispositivo con le specifiche previste più basse.
- Chiudere tutte le altre app per assicurarsi che venga misurato l'impatto completo dell'app sottoposta a profilatura, anziché le altre app.
Usare binding compilati
Le associazioni compilate migliorano le prestazioni del data binding nelle app MAUI .NET risolvendo le espressioni di associazione in fase di compilazione, anziché in fase di esecuzione con reflection. La compilazione di un'espressione di binding genera codice compilato che in genere risolve un binding da 8 a 20 volte più velocemente che usando un binding classico. Per altre informazioni, vedere Binding compilati.
Ridurre i binding non necessari
Non usare i binding per il contenuto che può essere impostato facilmente in modo statico. Non vi è alcun vantaggio nell'associare i dati che non richiedono l'associazione poiché i binding non hanno un costo contenuto. Ad esempio, l'impostazione Button.Text = "Accept"
ha un sovraccarico inferiore rispetto all'associazione Button.Text a una proprietà viewmodel string
con valore "Accept".
Scegliere il layout corretto
Un layout in grado di visualizzare più elementi figlio, ma che ne include solo uno è dispendioso. Ad esempio, l'esempio seguente mostra un VerticalStackLayout oggetto con un singolo figlio:
<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>
Questo è sprecato e l'elemento VerticalStackLayout deve essere rimosso, come illustrato nell'esempio seguente:
<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>
Inoltre, non tentare di riprodurre l'aspetto di un layout specifico usando combinazioni di altri layout, poiché in questo modo vengono eseguiti calcoli di layout non necessari. Ad esempio, non tentare di riprodurre un Grid layout usando una combinazione di HorizontalStackLayout elementi. L'esempio seguente illustra un esempio di questa procedura non valida:
<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>
L'operazione risulta dispendiosa poiché vengono eseguiti calcoli di layout non necessari. Al contrario, il layout desiderato può essere ottenuto meglio usando un Grid, come illustrato nell'esempio seguente:
<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>
Ottimizzare le risorse immagine
Le immagini sono alcune delle risorse più costose usate dalle app e vengono spesso acquisite a risoluzioni elevate. Sebbene ciò crei immagini vibranti piene di dettagli, le app che visualizzano tali immagini richiedono in genere un maggiore utilizzo della CPU per decodificare l'immagine e più memoria per archiviare l'immagine decodificata. È uno spreco decodificare un'immagine ad alta risoluzione in memoria se poi per visualizzarla è necessario ridurne le dimensioni. Ridurre invece l'utilizzo della CPU e il footprint di memoria creando versioni delle immagini archiviate vicine alle dimensioni di visualizzazione stimate. Un'immagine visualizzata in una visualizzazione elenco molto probabilmente dovrà avere una risoluzione più bassa di un'immagine visualizzata a schermo intero.
Inoltre, le immagini devono essere create solo quando necessario e devono essere rilasciate non appena l'app non le richiede più. Ad esempio, se un'app visualizza un'immagine leggendo i dati da un flusso, assicurarsi che il flusso venga creato solo quando è necessario e assicurarsi che il flusso venga rilasciato quando non è più necessario. Questo risultato può essere ottenuto creando il flusso quando viene creata la pagina oppure quando viene generato l'evento Page.Appearing e successivamente eliminando il flusso quando viene generato l'evento Page.Disappearing.
Quando si scarica un'immagine per la visualizzazione con il ImageSource.FromUri(Uri) metodo , assicurarsi che l'immagine scaricata venga memorizzata nella cache per un periodo di tempo appropriato. Per altre informazioni, vedere Memorizzazione nella cache delle immagini.
Ridurre le dimensioni della struttura ad albero visuale
La riduzione del numero di elementi in una pagina consente di velocizzare il rendering della pagina. Esistono due tecniche principali per ottenere questo risultato. La prima consiste nel nascondere gli elementi che non sono visibili. La proprietà IsVisible di ogni elemento determina se l'elemento deve essere parte della struttura ad albero visuale o meno. Di conseguenza, se un elemento non è visibile perché è nascosto dietro altri elementi, rimuovere l'elemento o impostarne la proprietà IsVisible
su false
.
La seconda tecnica è rimuovere gli elementi non necessari. Ad esempio, di seguito è illustrato un layout di pagina contenente più Label elementi:
<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>
Lo stesso layout di pagina può essere mantenuto con un conteggio degli elementi ridotto, come illustrato nell'esempio seguente:
<VerticalStackLayout Padding="20,35,20,20"
Spacing="25">
<Label Text="Hello" />
<Label Text="Welcome to the App!" />
<Label Text="Downloading Data..." />
</VerticalStackLayout>
Ridurre le dimensioni del dizionario risorse dell'applicazione
Tutte le risorse usate in tutta l'app devono essere archiviate nel dizionario risorse dell'app per evitare la duplicazione. Ciò consentirà di ridurre la quantità di CODICE XAML che deve essere analizzato in tutta l'app. L'esempio seguente mostra la HeadingLabelStyle
risorsa, che viene usata a livello di app e quindi viene definita nel dizionario risorse dell'app:
<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>
Tuttavia, XAML specifico di una pagina non deve essere incluso nel dizionario risorse dell'app, perché le risorse verranno quindi analizzate all'avvio dell'app anziché quando richiesto da una pagina. Se una risorsa viene usata da una pagina che non è la pagina di avvio, deve essere inserita nel dizionario risorse per tale pagina, contribuendo quindi a ridurre il codice XAML analizzato all'avvio dell'app. L'esempio seguente mostra la HeadingLabelStyle
risorsa, che si trova solo in una singola pagina e quindi viene definita nel dizionario risorse della pagina:
<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>
Per altre informazioni sulle risorse dell'app, vedi App di stile con XAML.
Ridurre le dimensioni dell'app
Quando .NET MAUI compila l'app, è possibile usare un linker denominato ILLink per ridurre le dimensioni complessive dell'app. ILLink riduce le dimensioni analizzando il codice intermedio prodotto dal compilatore. Rimuove metodi, proprietà, campi, eventi, struct e classi inutilizzati per produrre un'app che contiene solo dipendenze di codice e assembly necessarie per eseguire l'app.
Per altre informazioni sulla configurazione del comportamento del linker, vedere Collegamento di un'app Android, Collegamento di un'app iOS e Collegamento di un'app Mac Catalyst.
Ridurre il periodo di attivazione dell'app
Tutte le app hanno un periodo di attivazione, ovvero il tempo tra l'avvio dell'app e il momento in cui l'app è pronta per l'uso. Questo periodo di attivazione fornisce agli utenti la prima impressione dell'app ed è quindi importante ridurre il periodo di attivazione e la percezione dell'utente, in modo da ottenere una prima impressione favorevole dell'app.
Prima che un'app visualizzi l'interfaccia utente iniziale, deve fornire una schermata iniziale per indicare all'utente che l'app è in fase di avvio. Se l'app non può visualizzare rapidamente l'interfaccia utente iniziale, la schermata iniziale deve essere usata per informare l'utente dello stato di avanzamento durante il periodo di attivazione, per garantire che l'app non sia bloccata. A tale scopo, è possibile usare un indicatore di stato o un controllo simile.
Durante il periodo di attivazione, le app eseguono la logica di attivazione, che spesso include il caricamento e l'elaborazione delle risorse. È possibile ridurre il periodo di attivazione verificando che le risorse necessarie siano presenti all'interno del pacchetto dell'app e che non sia quindi necessario recuperarle in remoto. In alcuni casi, ad esempio, durante il periodo di attivazione può risultare appropriato caricare i dati segnaposto archiviati localmente. Quindi, dopo la visualizzazione dell'interfaccia utente iniziale e quando l'utente è in grado di interagire con l'app, i dati segnaposto possono essere man mano sostituiti da dati scaricati da un'origine remota. Inoltre, la logica di attivazione dell'app deve eseguire solo il lavoro necessario per consentire all'utente di iniziare a usare l'app. Può essere utile ritardare il caricamento di assembly aggiuntivi, dato che gli assembly vengono caricati la prima volta che vengono usati.
Scegliere con attenzione un contenitore di inserimento delle dipendenze
I contenitori di inserimento delle dipendenze introducono vincoli di prestazioni aggiuntivi nelle app per dispositivi mobili. La registrazione e la risoluzione dei tipi con un contenitore comportano un costo in termini di prestazioni perché il contenitore usa la reflection per la creazione di ogni tipo, soprattutto se le dipendenze vengono ricostruite per la navigazione di ogni pagina nell'app. Se le dipendenze presenti sono numerose o complete, il costo della creazione può aumentare in modo significativo. Inoltre, la registrazione dei tipi, che in genere si verifica durante l'avvio dell'app, può avere un impatto notevole sul tempo di avvio, a seconda del contenitore in uso. Per altre informazioni sull'inserimento delle dipendenze nelle app MAUI .NET, vedere Inserimento delle dipendenze.
In alternativa, l'inserimento delle dipendenze può essere reso più efficiente implementandolo manualmente tramite factory.
Creare app shell
Le app .NET MAUI Shell offrono un'esperienza di spostamento con opinione basata su riquadri a comparsa e schede. Se l'esperienza utente dell'app può essere implementata con Shell, è utile farlo. Le app shell consentono di evitare un'esperienza di avvio scarsa, perché le pagine vengono create su richiesta in risposta allo spostamento anziché all'avvio dell'app, che si verifica con le app che usano un .TabbedPage Per altre informazioni, vedere Panoramica della shell.
Ottimizzare le prestazioni di ListView
Se si usa ListView esistono alcune esperienze utente che devono essere ottimizzate:
- Inizializzazione: l'intervallo di tempo che inizia quando si crea il controllo e termina quando gli elementi sono visualizzati sullo schermo.
- Scorrimento: la possibilità di scorrere l'elenco e verificare che l'interfaccia utente non resti indietro rispetto ai movimenti tocco.
- Interazione per l'aggiunta, l'eliminazione e la selezione di elementi.
Il ListView controllo richiede che un'app fornisca modelli di dati e celle. Il modo in cui viene eseguita questa operazione ha un notevole impatto sulle prestazioni del controllo. Per altre informazioni, vedere Memorizzare nella cache i dati.
Usare la programmazione asincrona
La velocità di risposta complessiva dell'app può essere migliorata e i colli di bottiglia delle prestazioni spesso evitati, usando la programmazione asincrona. In .NET il modello asincrono basato su attività (TAP) è il modello di progettazione consigliato per le operazioni asincrone. Tuttavia, l'uso non corretto del TAP può comportare app non con prestazioni non corrette.
Nozioni fondamentali
Quando si usa tap, è necessario seguire le linee guida generali seguenti:
- Comprendere il ciclo di vita dell'attività, rappresentato dall'enumerazione
TaskStatus
. Per altre informazioni, vedere Significato di TaskStatus e Stato attività. - Usare il
Task.WhenAll
metodo per attendere in modo asincrono il completamento di più operazioni asincrone, anziché singolarmenteawait
una serie di operazioni asincrone. Per altre informazioni, vedere Task.WhenAll. - Usare il
Task.WhenAny
metodo per attendere in modo asincrono il completamento di una delle più operazioni asincrone. Per altre informazioni, vedere Task.WhenAny. - Utilizzare il
Task.Delay
metodo per produrre unTask
oggetto che termina dopo l'ora specificata. Ciò è utile per scenari come il polling dei dati e la gestione ritardata dell'input dell'utente per un periodo di tempo predeterminato. Per altre informazioni, vedere Task.Delay. - Eseguire operazioni di CPU sincrone intensive nel pool di thread con il
Task.Run
metodo . Questo metodo è un collegamento per ilTaskFactory.StartNew
metodo, con gli argomenti più ottimali impostati. Per altre informazioni, vedere Task.Run. - Evitare di tentare di creare costruttori asincroni. Usare invece eventi del ciclo di vita o logica di inizializzazione separata per eseguire correttamente
await
qualsiasi inizializzazione. Per altre informazioni, vedere Costruttori asincroni in blog.stephencleary.com. - Usare il modello di attività differita per evitare il completamento delle operazioni asincrone durante l'avvio dell'app. Per altre informazioni, vedere AsyncLazy.
- Creare un wrapper di attività per le operazioni asincrone esistenti, che non usano tap, creando
TaskCompletionSource<T>
oggetti. Questi oggetti ottengono i vantaggi dellaTask
programmabilità e consentono di controllare la durata e il completamento dell'oggetto associatoTask
. Per altre informazioni, vedere Natura di TaskCompletionSource. - Restituisce un
Task
oggetto, anziché restituire un oggetto attesoTask
, quando non è necessario elaborare il risultato di un'operazione asincrona. Si tratta di un'operazione più efficiente a causa di un cambio di contesto minore. - Usare la libreria del flusso di dati TPL (Task Parallel Library) in scenari come l'elaborazione dei dati quando diventa disponibile o quando si dispone di più operazioni che devono comunicare tra loro in modo asincrono. Per altre informazioni, vedere Flusso di dati (Task Parallel Library).For more information, see Dataflow (Task Parallel Library).
INTERFACCIA UTENTE
Quando si usa TAP con i controlli dell'interfaccia utente, è necessario seguire le linee guida seguenti:
Chiamare una versione asincrona di un'API, se disponibile. In questo modo il thread dell'interfaccia utente verrà sbloccato, che consentirà di migliorare l'esperienza dell'utente con l'app.
Aggiornare gli elementi dell'interfaccia utente con i dati delle operazioni asincrone nel thread dell'interfaccia utente, per evitare che vengano generate eccezioni. Tuttavia, gli aggiornamenti alla
ListView.ItemsSource
proprietà verranno automaticamente sottoposto a marshalling al thread dell'interfaccia utente. Per informazioni su come determinare se il codice è in esecuzione nel thread dell'interfaccia utente, vedere Creare un thread nel thread dell'interfaccia utente.Importante
Tutte le proprietà di controllo aggiornate tramite il data binding verranno automaticamente sottoposto a marshalling al thread dell'interfaccia utente.
Gestione errori
Quando si usa TAP, è necessario seguire le seguenti linee guida per la gestione degli errori:
- Informazioni sulla gestione asincrona delle eccezioni. Le eccezioni non gestite generate dal codice in esecuzione in modo asincrono vengono propagate nuovamente al thread chiamante, ad eccezione di alcuni scenari. Per altre informazioni, vedere Gestione delle eccezioni (Task Parallel Library).
- Evitare di creare
async void
metodi e creareasync Task
metodi. In questo modo è possibile semplificare la gestione degli errori, la componibilità e la testabilità. L'eccezione a questa linea guida è costituita da gestori eventi asincroni, che devono restituirevoid
. Per altre informazioni, vedere Evitare Async Void. - Non combinare il blocco e il codice asincrono chiamando i
Task.Wait
metodi ,Task.Result
oGetAwaiter().GetResult
, perché possono causare un deadlock. Tuttavia, se questa linea guida deve essere violata, l'approccio preferito consiste nel chiamare ilGetAwaiter().GetResult
metodo perché mantiene le eccezioni dell'attività. Per altre informazioni, vedere Async All the Way and Task Exception Handling in .NET 4.5.For more information, see Async All the Way and Task Exception Handling in .NET 4.5. - Usare il
ConfigureAwait
metodo, quando possibile, per creare codice senza contesto. Il codice senza contesto offre prestazioni migliori per le app per dispositivi mobili ed è una tecnica utile per evitare deadlock quando si usa una codebase parzialmente asincrona. Per altre informazioni, vedere Configurare il contesto. - Usare le attività di continuazione per funzionalità come la gestione delle eccezioni generate dall'operazione asincrona precedente e l'annullamento di una continuazione prima dell'avvio o durante l'esecuzione. Per altre informazioni, vedere Concatenamento di attività tramite attività continue.
- Usare un'implementazione asincrona quando vengono richiamate operazioni asincrone ICommand da ICommand. In questo modo è possibile gestire eventuali eccezioni nella logica dei comandi asincrona. Per altre informazioni, vedere Programmazione asincrona: Modelli per applicazioni MVVM asincrone: comandi.
Ritardare il costo della creazione di oggetti
Per rinviare la creazione di un oggetto fino al momento in cui viene usato per la prima volta, è possibile usare l'inizializzazione differita. Questa tecnica viene usata in particolare per migliorare le prestazioni, evitare calcoli e ridurre i requisiti di memoria.
È consigliabile usare l'inizializzazione differita per gli oggetti che sono costosi da creare negli scenari seguenti:
- L'app potrebbe non usare l'oggetto .
- Prima della creazione dell'oggetto è necessario completare altre operazioni dal costo elevato.
La Lazy<T>
classe viene usata per definire un tipo inizializzato lazy, come illustrato nell'esempio seguente:
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)
{
...
}
L'inizializzazione differita viene eseguita la prima volta che si accede alla proprietà Lazy<T>.Value
. Il tipo con wrapping viene creato e restituito in occasione del primo accesso e viene quindi archiviato per l'eventuale accesso futuro.
Per altre informazioni sull'inizializzazione differita, vedere Inizializzazione differita.
Rilasciare risorse IDisposable
L'interfaccia IDisposable
rappresenta un meccanismo che consente il rilascio delle risorse in modo esplicito tramite l'implementazione del metodo Dispose
. IDisposable
non è un distruttore e deve essere implementato solo nelle circostanze seguenti:
- Quando la classe è proprietaria di risorse non gestite. Tra le risorse non gestite che richiedono il rilascio troviamo file, flussi e connessioni di rete.
- Quando la classe è proprietaria di risorse
IDisposable
gestite.
I consumer di tipi possono quindi chiamare l'implementazione di IDisposable.Dispose
per liberare risorse quando l'istanza non è più necessaria. Per raggiungere questo obiettivo sono possibili due approcci:
- Wrapping dell'oggetto
IDisposable
in un'istruzioneusing
. - Wrapping della chiamata a
IDisposable.Dispose
in un bloccotry
/finally
.
Eseguire il wrapping dell'oggetto IDisposable in un'istruzione using
Nell'esempio seguente viene illustrato come eseguire il wrapping di un IDisposable
oggetto in un'istruzione using
:
public void ReadText(string filename)
{
string text;
using (StreamReader reader = new StreamReader(filename))
{
text = reader.ReadToEnd();
}
...
}
La classe StreamReader
implementa l'oggetto IDisposable
e l'istruzione using
rappresenta una sintassi comoda per chiamare il metodo StreamReader.Dispose
per l'oggetto StreamReader
prima che quest'ultimo diventi esterno all'ambito. All'interno del blocco using
l'oggetto StreamReader
è di sola lettura e non può essere riassegnato. L'istruzione using
assicura anche che il metodo Dispose
venga chiamato anche se si verifica un'eccezione, dato che il compilatore implementa il linguaggio intermedio (IL) per un blocco try
/finally
.
Eseguire il wrapping della chiamata a IDisposable.Dispose in un blocco try/finally
L'esempio seguente illustra come eseguire il wrapping della chiamata a IDisposable.Dispose
in un try
/finally
blocco:
public void ReadText(string filename)
{
string text;
StreamReader reader = null;
try
{
reader = new StreamReader(filename);
text = reader.ReadToEnd();
}
finally
{
if (reader != null)
reader.Dispose();
}
...
}
La classe StreamReader
implementa IDisposable
e il blocco finally
chiama il metodo StreamReader.Dispose
per rilasciare la risorsa. Per altre informazioni, vedere Interfaccia IDisposable.
Annullare la sottoscrizione agli eventi
Per evitare perdite di memoria, è necessario annullare le sottoscrizioni agli eventi prima dell'eliminazione dell'oggetto sottoscrittore corrispondente. Finché non si annulla la sottoscrizione di un evento, il delegato per l'evento nell'oggetto che esegue la pubblicazione contiene un riferimento al delegato che incapsula il gestore eventi del sottoscrittore. Finché l'oggetto che esegue la pubblicazione include tale riferimento, l'operazione di Garbage Collection non può recuperare la memoria occupata dall'oggetto sottoscrittore.
L'esempio seguente illustra come annullare la sottoscrizione a un evento:
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;
}
}
La classe Subscriber
annulla la sottoscrizione all'evento all'interno del metodo Dispose
della classe stessa.
Quando si usano gestori degli eventi e la sintassi lambda, possono anche verificarsi cicli di riferimento, perché le espressioni lambda possono fare riferimento agli oggetti, mantenendoli attivi. Pertanto, un riferimento al metodo anonimo può essere archiviato in un campo e usato per annullare la sottoscrizione all'evento, come illustrato nell'esempio seguente:
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;
}
}
Il campo _handler
mantiene il riferimento al metodo anonimo e viene usato per la sottoscrizione di eventi e per l'annullamento di tali sottoscrizioni.
Evitare riferimenti circolari sicuri in iOS e Mac Catalyst
In alcune situazioni è possibile creare cicli di riferimento sicuri che potrebbero impedire al Garbage Collector di recuperare la memoria degli oggetti. Si consideri ad esempio il caso in cui una NSObjectsottoclasse derivata da , ad esempio una classe che eredita da UIView, viene aggiunta a un NSObject
contenitore derivato da ed è fortemente a cui viene fatto riferimento da Objective-C, come illustrato nell'esempio seguente:
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));
Quando questo codice crea l'istanza Container
, l'oggetto C# avrà un riferimento sicuro a un Objective-C oggetto . Analogamente, l'istanza MyView
avrà anche un riferimento sicuro a un Objective-C oggetto .
La chiamata a container.AddSubview
aumenterà inoltre il conteggio dei riferimenti nell'istanza non gestita di MyView
. In questo caso, il runtime .NET per iOS crea un'istanza GCHandle
per mantenere attivo l'oggetto MyView
nel codice gestito, perché non esiste alcuna garanzia che tutti gli oggetti gestiti manterranno un riferimento a esso. Dal punto di vista del codice gestito, l'oggetto MyView
verrebbe recuperato dopo la chiamata ad AddSubview(UIView) se non fosse per GCHandle
.
L'oggetto MyView
non gestito avrà un elemento GCHandle
che punta all'oggetto gestito, noto come collegamento sicuro. L'oggetto gestito conterrà un riferimento all'istanza di Container
. L'istanza di Container
avrà a sua volta un riferimento gestito all'oggetto MyView
.
Nei casi in cui un oggetto contenuto mantiene un collegamento al contenitore, sono disponibili diverse opzioni per gestire il riferimento circolare:
- Evitare il riferimento circolare mantenendo un riferimento debole al contenitore.
- Chiamare
Dispose
sugli oggetti. - Interrompere manualmente il ciclo impostando il collegamento al contenitore su
null
. - Rimuovere manualmente l'oggetto contenuto dal contenitore.
Usare riferimenti deboli
Un modo per evitare un ciclo è l'uso di un riferimento debole dall'elemento figlio all'elemento padre. Ad esempio, il codice precedente può essere come illustrato nell'esempio seguente:
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));
In questo caso, l'oggetto contenuto non mantiene attivo l'elemento padre. Tuttavia, l'elemento padre mantiene attivo l'elemento figlio tramite la chiamata a container.AddSubView
.
Ciò si verifica anche nelle API iOS che usano il modello delegato o origine dati, in cui una classe peer contiene l'implementazione. Ad esempio, quando si imposta la Delegate proprietà o l'oggetto DataSource UITableView nella classe .
Nel caso delle classi create esclusivamente per l'implementazione di un protocollo, ad esempio IUITableViewDataSource, invece di creare una sottoclasse, è sufficiente implementare l'interfaccia nella classe, eseguire l'override del metodo e assegnare la proprietà DataSource
a this
.
Eliminare gli oggetti con riferimenti sicuri
Se esiste un riferimento sicuro ed è difficile rimuovere la dipendenza, fare in modo che un metodo Dispose
cancelli il puntatore padre.
Per i contenitori, eseguire l'override del Dispose
metodo per rimuovere gli oggetti contenuti, come illustrato nell'esempio seguente:
class MyContainer : UIView
{
public override void Dispose()
{
// Brute force, remove everything
foreach (var view in Subviews)
{
view.RemoveFromSuperview();
}
base.Dispose();
}
}
Per un oggetto figlio che conserva il riferimento sicuro all'elemento padre, cancellare il riferimento al padre nell'implementazione Dispose
:
class MyChild : UIView
{
MyContainer _container;
public MyChild(MyContainer container)
{
_container = container;
}
public override void Dispose()
{
_container = null;
}
}