Share via


Melhorar o desempenho do aplicativo

O baixo desempenho do aplicativo se apresenta de várias maneiras. Ele pode fazer um aplicativo parecer sem resposta, causar uma rolagem lenta e reduzir a duração da bateria do dispositivo. No entanto, a otimização do desempenho engloba mais do que apenas a implementação de um código eficiente. A experiência do usuário com o desempenho do aplicativo também precisa ser considerada. Por exemplo, garantir que as operações sejam executadas sem impedir o usuário de realizar outras atividades pode ajudar a melhorar a experiência do usuário.

Há muitas técnicas para aumentar o desempenho e o desempenho percebido dos aplicativos .NET MAUI (.NET Multi-Platform App UI). Coletivamente, essas técnicas podem reduzir de maneira considerável a quantidade de trabalho sendo executada por uma CPU e a quantidade de memória consumida por um aplicativo.

Usar um criador de perfil

Ao desenvolver um aplicativo, é importante tentar apenas otimizar o código depois que ele tiver sido analisado. Criação de perfil é uma técnica para determinar onde as otimizações de código terão o maior efeito na redução de problemas de desempenho. O criador de perfil acompanha o uso de memória do aplicativo e registra o tempo de execução dos métodos no aplicativo. Esses dados ajudam a percorrer os caminhos de execução do aplicativo e o custo de execução do código, para que as melhores oportunidades de otimização possam ser descobertas.

Os aplicativos .NET MAUI podem ser analisados com dotnet-trace no Android, no iOS e no Mac e no Windows e com o PerfView no Windows. Para obter mais informações, confira Criação de perfil de aplicativos .NET MAUI.

As melhores práticas a seguir são recomendadas ao analisar um aplicativo:

  • Evite analisar um aplicativo em um simulador, pois o simulador pode distorcer o desempenho do aplicativo.
  • Idealmente, a criação de perfil deve ser executada em uma variedade de dispositivos, pois tomar medidas de desempenho em um único dispositivo nem sempre mostrará as características de desempenho de outros dispositivos. No entanto, no mínimo, de criação de perfil deve ser executada em um dispositivo que tem a menor especificação antecipada.
  • Feche todos os outros aplicativos para garantir que o impacto total do aplicativo que está sendo analisado esteja sendo medido, em vez dos outros aplicativos.

Usar associações compiladas

As associações compiladas aprimoram o desempenho da associação de dados em aplicativos .NET MAUI resolvendo expressões de associação em tempo de compilação, em vez de em runtime com reflexão. Quando você compila uma expressão de associação, é gerado um código compilado que, normalmente, resolve uma associação de 8 a 20 vezes mais rápido do que uma associação clássica. Para obter mais informações, confira Associações compiladas.

Reduzir associações desnecessárias

Não use associações para conteúdo que pode ser facilmente definido estaticamente. Não há nenhuma vantagem em associar dados que não precisam ser associados, pois associações não são econômicas. Por exemplo, a configuração Button.Text = "Accept" tem menos sobrecarga do que associar Button.Text a uma propriedade string viewmodel com o valor “Accept”.

Escolher o layout correto

Um layout que é capaz de exibir vários filhos, mas que tem apenas um único filho, é um desperdício. Por exemplo, o seguinte exemplo mostra um VerticalStackLayout com um só filho:

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

Isso é um desperdício e o elemento VerticalStackLayout deve ser removido, conforme mostrado no seguinte exemplo:

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

Além disso, não tente reproduzir a aparência de um layout específico usando combinações de outros layouts, pois isso resulta na execução de cálculos de layout desnecessários. Por exemplo, não tente reproduzir um layout de Grid usando uma combinação de elementos HorizontalStackLayout. O seguinte exemplo mostra um exemplo dessa prática não recomendada:

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

Isso é um desperdício porque cálculos de layout desnecessário são executados. Em vez disso, o layout desejado pode ser mais bem obtido por meio de um Grid, conforme mostrado no seguinte exemplo:

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

Otimizar os recursos de imagem

As imagens são alguns dos recursos mais caros que os aplicativos usam e geralmente são capturadas em altas resoluções. Embora isso crie imagens vibrantes cheias de detalhes, os aplicativos que exibem essas imagens normalmente exigem mais uso da CPU para decodificar a imagem e mais memória para armazenar a imagem decodificada. É dispendioso decodificar uma imagem de alta resolução na memória sendo que ele será reduzido para um tamanho menor para exibição. Em vez disso, reduza o uso da CPU e o volume de memória criando versões de imagens armazenadas próximas aos tamanhos de exibição previstos. Por exemplo, uma imagem exibida em uma exibição de lista provavelmente deve ter uma resolução menor do que uma imagem exibida em tela inteira.

Além disso, as imagens só devem ser criadas quando necessário e devem ser liberadas assim que o aplicativo não precisar mais delas. Por exemplo, se um aplicativo estiver exibindo uma imagem com a leitura dos dados de um fluxo, verifique se o fluxo é criado somente quando necessário e se ele é liberado quando não é mais necessário. Isso pode ser feito criando o fluxo quando a página é criada ou quando o evento Page.Appearing é acionado e, em seguida, descartando o fluxo quando o evento Page.Disappearing é acionado.

Ao baixar uma imagem para exibição com o método ImageSource.FromUri(Uri), verifique se a imagem baixada está armazenada em cache por um período adequado. Para obter mais informações, confira Cache de imagem.

Reduzir o tamanho da árvore visual

Reduzir o número de elementos em uma página tornará a renderização da página mais rápida. Há duas técnicas principais para realizar essa tarefa. A primeira é ocultar elementos que não estão visíveis. A propriedade IsVisible de cada elemento determina se o elemento deve fazer parte da árvore visual ou não. Portanto, se um elemento não estiver visível porque está oculto atrás de outros elementos, remova o elemento ou defina sua propriedade IsVisible como false.

A segunda técnica é remover elementos desnecessários. Por exemplo, a seguinte imagem mostra um layout de página contendo vários elementos Label:

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

O mesmo layout de página pode ser mantido com uma contagem de elementos reduzida, conforme mostrado no seguinte exemplo:

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

Reduzir o tamanho do dicionário de recursos do aplicativo

Todos os recursos usados no aplicativo devem ser armazenados no dicionário de recursos do aplicativo para evitar a duplicação. Isso ajudará a reduzir a quantidade de XAML que precisa ser analisada em todo o aplicativo. O seguinte exemplo mostra o recurso HeadingLabelStyle, que é usado em todo o aplicativo e, portanto, é definido no dicionário de recursos do aplicativo:

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

No entanto, o XAML específico a uma página não deve ser incluído no dicionário de recursos do aplicativo, pois os recursos serão analisados na inicialização do aplicativo em vez de quando exigidos por uma página. Se um recurso for usado por uma página que não seja a página de inicialização, ele deverá ser colocado no dicionário de recursos dessa página, ajudando, portanto, a reduzir o XAML analisado quando o aplicativo é iniciado. O seguinte exemplo mostra o recurso HeadingLabelStyle, que só está em uma página e, portanto, é definido no dicionário de recursos da página:

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

Para obter mais informações sobre os recursos de aplicativo, confira Definir o estilo de aplicativos usando o XAML.

Reduzir o tamanho do aplicativo

Quando o .NET MAUI cria seu aplicativo, um vinculador chamado ILLink pode ser usado para reduzir o tamanho geral dele. O ILLink reduz o tamanho analisando o código intermediário produzido pelo compilador. Ele remove métodos, propriedades, campos, eventos, classes e structs não utilizados para produzir um aplicativo que contém apenas as dependências de código e de assembly necessárias para executar o aplicativo.

Para obter mais informações sobre como configurar o comportamento do vinculador, confira Como vincular um aplicativo Android, Como vincular um aplicativo iOS e Como vincular um aplicativo Mac Catalyst.

Reduzir o período de ativação do aplicativo

Todos os aplicativos têm um período de ativação, que é o tempo entre quando o aplicativo é iniciado e quando o aplicativo está pronto para uso. Esse período de ativação dá aos usuários a primeira impressão do aplicativo e, portanto, é importante reduzir o período de ativação e a percepção do usuário sobre ele, para que ele obtenha uma primeira impressão favorável do aplicativo.

Antes que um aplicativo exiba a interface do usuário inicial, ele deve apresentar uma tela inicial para indicar ao usuário que o aplicativo está sendo iniciado. Se o aplicativo não puder exibir rapidamente a interface do usuário inicial, a tela inicial deverá ser usada para informar o usuário sobre o progresso durante o período de ativação, a fim de oferecer a garantia de que o aplicativo não travou. Essa garantia pode ser uma barra de progresso ou um controle semelhante.

Durante o período de ativação, os aplicativos executam a lógica de ativação, que geralmente inclui o carregamento e o processamento de recursos. O período de ativação pode ser reduzido, garantindo que os recursos necessários sejam empacotados no aplicativo, em vez de serem recuperados remotamente. Por exemplo, em algumas circunstâncias, pode ser apropriado durante o período de ativação carregar dados de espaço reservado armazenados localmente. Em seguida, depois que a interface do usuário inicial é exibida e o usuário é capaz de interagir com o aplicativo, os dados de espaço reservado podem ser substituídos progressivamente de uma fonte remota. Além disso, a lógica de ativação do aplicativo deve executar apenas o trabalho necessário para permitir que o usuário comece a usar o aplicativo. Isso pode ajudar se atrasar o carregamento de assemblies adicionais, já que assemblies são carregados na primeira vez em que eles são usados.

Escolher bem um contêiner de injeção de dependência

Os contêineres de injeção de dependência introduzem restrições de desempenho adicionais em aplicativos móveis. Efetuar o registro e a resolução de tipos usando um contêiner tem um custo de desempenho devido ao uso da reflexão pelo contêiner para criar cada tipo, especialmente se as dependências estiverem sendo reconstruídas para cada navegação de página no aplicativo. Se houver muitas dependências ou se elas forem profundas, o custo da criação poderá aumentar significativamente. Além disso, o registro de tipo, que costuma ocorrer durante a inicialização do aplicativo, pode ter um impacto perceptível no tempo de inicialização, dependendo do contêiner que está sendo usado. Para obter mais informações sobre a injeção de dependência em aplicativos .NET MAUI, confira Injeção de dependência.

Como alternativa, a injeção de dependência pode se tornar mais eficaz por meio da implementação manual usando fábricas.

Criar aplicativos do Shell

Os aplicativos .NET MAUI Shell fornecem uma experiência de navegação opinativa com base em submenus e guias. Se a experiência do usuário do aplicativo puder ser implementada com o Shell, será útil fazer isso. Os aplicativos Shell ajudam a evitar uma experiência de inicialização ruim, pois as páginas são criadas sob demanda em resposta à navegação em vez de na inicialização do aplicativo, o que ocorre com os aplicativos que usam uma TabbedPage. Para obter mais informações, confira Visão geral do Shell.

Otimizar o desempenho da ListView

Ao usar ListView, há várias experiências de usuário que devem ser otimizadas:

  • Inicialização – o intervalo de tempo que começa quando o controle é criado e termina quando itens são mostrados na tela.
  • Rolagem – a capacidade de rolar pela lista e garantir que a interface do usuário não fique atrasada com relação a gestos de toque.
  • Interação para adicionar, excluir e selecionar itens.

O controle ListView exige um aplicativo para fornecer dados e modelos de célula. Como isso é feito terá um grande impacto sobre o desempenho do controle. Para obter mais informações, confira Armazenar dados em cache.

usar programação assíncrona

A capacidade de resposta geral do seu aplicativo pode ser aprimorada e os gargalos de desempenho costumam ser evitados por meio da programação assíncrona. No .NET, o TAP (padrão assíncrono baseado em tarefa) é o padrão de design recomendado para operações assíncronas. No entanto, o uso incorreto do TAP pode resultar em aplicativos que não apresentam alto desempenho.

Conceitos básicos

As seguintes diretrizes gerais devem ser seguidas quando o TAP é usado:

  • Compreenda o ciclo de vida da tarefa, que é representado pela enumeração TaskStatus. Para obter mais informações, confira O significado de TaskStatus e Status da tarefa.
  • Use o método Task.WhenAll para aguardar de maneira assíncrona a conclusão de várias operações assíncronas, em vez de await individualmente uma série de operações assíncronas. Para obter mais informações, confira Task.WhenAll.
  • Use o método Task.WhenAny para aguardar de maneira assíncrona a conclusão de uma das várias operações assíncronas. Para obter mais informações, confira Task.WhenAny.
  • Use o método Task.Delay para produzir um objeto Task que seja concluído após o horário especificado. Isso é útil para cenários como sondagem de dados e atraso no tratamento da entrada do usuário por um tempo predeterminado. Para obter mais informações, confira Task.Delay.
  • Execute operações síncronas intensivas de CPU no pool de threads com o método Task.Run. Esse método é um atalho para o método TaskFactory.StartNew, com os argumentos mais otimizados definidos. Para obter mais informações, confira Task.Run.
  • Evite tentar criar construtores assíncronos. Em vez disso, use eventos de ciclo de vida ou uma lógica de inicialização separada para await corretamente qualquer inicialização. Para obter mais informações, confira Construtores assíncronos em blog.stephencleary.com.
  • Use o padrão de tarefa lenta para evitar aguardar a conclusão de operações assíncronas durante a inicialização do aplicativo. Para obter mais informações, confira AsyncLazy.
  • Crie um wrapper de tarefas para as operações assíncronas existentes que não usam o TAP, criando objetos TaskCompletionSource<T>. Esses objetos obtêm os benefícios da programação do Task e permitem que você controle o tempo de vida e a conclusão da Task. Para obter mais informações, confira A natureza de TaskCompletionSource.
  • Retorne um objeto Task, em vez de retornar um objeto Task aguardado, quando não houver a necessidade de processar o resultado de uma operação assíncrona. Isso é mais eficiente devido à menor alternância de contexto executada.
  • Use a biblioteca de fluxo de dados TPL (biblioteca de paralelismo de tarefas) em cenários como processamento de dados, à medida que eles se tornarem disponíveis ou quando você tiver várias operações que devem se comunicar entre si de maneira assíncrona. Para obter mais informações, confira Fluxo de dados (biblioteca de paralelismo de tarefas).

UI

As seguintes diretrizes devem ser seguidas quando o TAP é usado com controles de interface do usuário:

  • Chame uma versão assíncrona de uma API, se ela estiver disponível. Isso manterá o thread da interface do usuário desbloqueado, o que ajudará a aprimorar a experiência do usuário com o aplicativo.

  • Atualize os elementos da interface do usuário com os dados de operações assíncronas no thread da interface do usuário, a fim de evitar que exceções sejam geradas. No entanto, as atualizações na propriedade ListView.ItemsSource serão automaticamente empacotadas para o thread da interface do usuário. Para obter informações sobre como determinar se o código está em execução no thread da interface do usuário, confira Criar um thread no thread da interface do usuário.

    Importante

    Todas as propriedades de controle que são atualizadas por meio da associação de dados serão automaticamente empacotadas para o thread da interface do usuário.

Tratamento de erros

As seguintes diretrizes de tratamento de erro devem ser seguidas quando o TAP é usado:

  • Saiba mais sobre o tratamento de exceções assíncronas. As exceções sem tratamento geradas pelo código que está sendo executado de maneira assíncrona são propagadas de volta para o thread de chamada, exceto em alguns cenários. Para obter mais informações, confira Tratamento de exceções (biblioteca de paralelismo de tarefas).
  • Evite criar métodos async void e, em vez disso, crie métodos async Task. Isso facilita o tratamento de erro, a capacidade de composição e a estabilidade. A exceção a essa diretriz são os manipuladores de eventos assíncronos, que precisam retornar void. Para obter mais informações, confira Evitar o nulo assíncrono.
  • Não combine o bloqueio e o código assíncrono chamando os métodos Task.Wait, Task.Result ou GetAwaiter().GetResult, pois eles podem resultar na ocorrência de deadlock. No entanto, se essa diretriz precisar ser violada, a abordagem preferencial será chamar o método GetAwaiter().GetResult, porque ele preserva as exceções de tarefa. Para obter mais informações, confira Assíncrono todo o tempo e Tratamento de exceções de tarefa no .NET 4.5.
  • Use o método ConfigureAwait sempre que possível, para criar um código sem contexto . O código sem contexto tem um melhor desempenho para aplicativos móveis e é uma técnica útil para evitar o deadlock ao trabalhar com uma base de código parcialmente assíncrona. Para obter mais informações, confira Configurar o contexto.
  • Use tarefas de continuação para funcionalidades, como tratar exceções geradas pela operação assíncrona anterior e cancelar uma continuação antes que ela seja iniciada ou enquanto ela estiver em execução. Para obter mais informações, confira Encadeamento de tarefas com tarefas contínuas.
  • Use uma implementação ICommand assíncrona quando operações assíncronas forem invocadas por meio do ICommand. Isso garante que qualquer exceção na lógica de comando assíncrona possa ser manipulada. Para obter mais informações, confira Programação assíncrona: padrões para aplicativos MVVM assíncronos: comandos.

Atrasar o custo de criação de objetos

A inicialização lenta pode ser usada para adiar a criação de um objeto até que ele seja usado pela primeira vez. Essa técnica é usada principalmente para melhorar o desempenho, evitar a computação e reduzir os requisitos de memória.

Considere o uso da inicialização lenta para objetos que são caros de serem criados nos seguintes cenários:

  • O aplicativo pode não usar o objeto.
  • Outras operações caras devem ser concluídas antes que o objeto seja criado.

A classe Lazy<T> é usada para definir um tipo inicializado lentamente, conforme mostrado no seguinte exemplo:

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

A inicialização lenta ocorre na primeira vez que a propriedade Lazy<T>.Value é acessada. O tipo encapsulado é criado e retornado no primeiro acesso, então é armazenado para eventuais acessos futuros.

Para obter mais informações sobre a inicialização lenta, veja Inicialização lenta.

Liberar recursos IDisposable

A interface IDisposable fornece um mecanismo para liberar recursos. Ele fornece um método Dispose que deve ser implementado para liberar recursos explicitamente. IDisposable não é um destruidor e só deve ser implementado nas seguintes circunstâncias:

  • Quando a classe tem recursos não gerenciados. Recursos não gerenciados típicos que exigem liberação incluem arquivos, fluxos e conexões de rede.
  • Quando a classe tem recursos IDisposable gerenciados.

Os consumidores de tipo podem chamar a implementação IDisposable.Dispose para liberar recursos quando a instância não é mais necessária. Há duas abordagens para fazer isso:

  • Encapsulando o objeto IDisposable em uma instrução using.
  • Encapsulando a chamada IDisposable.Dispose em um bloco try/finally.

Encapsular o objeto IDisposable em uma instrução using

O seguinte exemplo mostra como encapsular um objeto IDisposable em uma instrução using:

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

A classe StreamReader implementa IDisposable e a instrução using fornece uma sintaxe conveniente, que chama o método StreamReader.Dispose no objeto StreamReader antes que ele saia do escopo. Dentro do bloco using, o objeto StreamReader é somente leitura e não pode ser reatribuído. A instrução using garante que o método Dispose seja chamado mesmo se ocorrer uma exceção, pois o compilador implementa a IL (linguagem intermediária) em um bloco try/finally.

Encapsular a chamada a IDisposable.Dispose em um bloco try/finally

O seguinte exemplo mostra como encapsular a chamada IDisposable.Dispose em um bloco try/finally:

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

A classe StreamReader implementa IDisposable e o bloco finally chama o método StreamReader.Dispose para liberar o recurso. Para obter mais informações, veja Interface IDisposable.

Cancelar a assinatura de eventos

Para evitar vazamentos de memória, os eventos devem ser cancelados antes que o objeto do assinante seja descartado. Até que a assinatura do evento seja cancelada, o delegado para o evento no objeto de publicação tem uma referência para o delegado que encapsula o manipulador de eventos do assinante. Desde que o objeto de publicação contenha essa referência, a coleta de lixo não recuperará a memória do objeto de assinante.

O seguinte exemplo mostra como cancelar a assinatura de um 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;
    }
}

A classe Subscriber cancela a assinatura do evento em seu método Dispose.

Ciclos de referência também podem ocorrer ao usar manipuladores de eventos e sintaxe lambda, já que expressões lambda podem fazer referência a objetos e mantê-los ativos. Portanto, uma referência ao método anônimo pode ser armazenada em um campo e usada para cancelar a assinatura do evento, conforme mostrado no seguinte exemplo:

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

O campo _handler mantém a referência para o método anônimo e é usado para assinatura e cancelamento de assinatura do evento.

Evitar referências circulares fortes no iOS e no Mac Catalyst

Em algumas situações, é possível criar ciclos de referência fortes que podem impedir que os objetos tenham sua memória recuperada pelo coletor de lixo. Por exemplo, considere o caso em que uma subclasse derivada de NSObject, como uma classe que herda de UIView, é adicionada a um contêiner derivado de NSObject e é fortemente referenciada de Objective-C, conforme mostrado no seguinte exemplo:

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 esse código cria a instância Container, o objeto C# terá uma referência forte a um objeto Objective-C. Da mesma forma, a instância MyView também terá uma referência forte a um objeto Objective-C.

Além disso, a chamada para container.AddSubview aumentará a contagem de referência na instância MyView não gerenciada. Quando isso acontece, o runtime do .NET para iOS cria uma instância de GCHandle para manter o objeto MyView no código gerenciado ativo, pois não há garantia de que os objetos gerenciados manterão uma referência a ele. De uma perspectiva de código gerenciado, o objeto MyView seria recuperado depois que a chamada AddSubview(UIView) não fosse para o GCHandle.

O objeto MyView não gerenciado terá um GCHandle apontando para o objeto gerenciado, conhecido como um link forte. O objeto gerenciado terá uma referência para a instância Container. Por sua vez, a instância Container terá uma referência gerenciada ao objeto MyView.

Em casos nos quais um objeto contido mantém um link para seu contêiner, há várias opções disponíveis para lidar com a referência circular:

  • Evite a referência circular mantendo uma referência fraca para o contêiner.
  • Chame Dispose nos objetos.
  • Interrompa manualmente o ciclo ao definir o link para o contêiner para null.
  • Remova manualmente o objeto contido do contêiner.

Usar referências fracas

Uma maneira de evitar um ciclo é usar uma referência fraca do filho para o pai. Por exemplo, o código acima pode ser mostrado no seguinte exemplo:

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

Aqui, o objeto contido não manterá o pai ativo. No entanto, o pai mantém a criança ativa por meio da chamada a container.AddSubView.

Isso também acontece nas APIs do iOS que usam o padrão delegado ou de fonte de dados, em que uma classe par contém a implementação. Por exemplo, ao definir a propriedade Delegate ou a DataSource na classe UITableView.

No caso das classes que são criadas somente para implementar um protocolo, por exemplo a IUITableViewDataSource, o que você pode fazer é, em vez de criar uma subclasse, apenas implementar a interface na classe e substituir o método, atribuindo a propriedade DataSource à this.

Descartar objetos com referências fortes

Se houver uma referência forte e for difícil remover a dependência, faça um método Dispose limpar o ponteiro pai.

No caso de contêineres, substitua o método Dispose para remover os objetos contidos, conforme mostrado no seguinte exemplo:

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

Para um objeto filho que mantém uma referência forte ao seu pai, limpe a referência ao pai na implementação Dispose:

class MyChild : UIView
{
    MyContainer _container;

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

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