Compartilhar 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, pode causar rolagem lenta e pode reduzir a duração da bateria do dispositivo. No entanto, otimizar o desempenho envolve mais do que apenas implementar um código eficiente. A experiência do usuário com o desempenho do aplicativo também deve ser considerada. Por exemplo, garantir que as operações sejam executadas sem impedir que o usuário execute 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 (interface do usuário de aplicativo multiplataforma) . Coletivamente, essas técnicas podem reduzir consideravelmente a quantidade de trabalho que está 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 perfilado. A 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 perfilador monitora o uso de memória do aplicativo e registra o tempo de execução dos métodos do aplicativo. Esses dados ajudam a navegar pelos caminhos de execução do aplicativo e pelo custo de execução do código, para que as melhores oportunidades de otimização possam ser descobertas.

Os aplicativos .NET MAUI podem ser perfilados usando dotnet-trace no Android, iOS e Mac e Windows e com o PerfView no Windows. Para obter mais informações, consulte perfilagem de aplicativos .NET MAUI.

As seguintes práticas recomendadas devem ser seguidas ao fazer o perfil de um aplicativo:

  • Evite criar perfil de 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 fazer medidas de desempenho em um dispositivo nem sempre mostrará as características de desempenho de outros dispositivos. No entanto, no mínimo, a criação de perfil deve ser executada em um dispositivo que tenha a especificação mais baixa prevista.
  • Feche todos os outros aplicativos para garantir que o impacto total do aplicativo que está sendo perfilado esteja sendo medido, em vez dos outros aplicativos.

Usar associações compiladas

As associações compiladas melhoram o desempenho da vinculação de dados em aplicativos .NET MAUI ao resolver expressões de associação em tempo de compilação, em vez de em tempo de execução com reflexão. Compilar uma expressão de associação gera um código compilado que normalmente resolve uma associação 8 a 20 vezes mais rápida do que usar uma associação clássica. Para obter mais informações, consulte vinculações compiladas.

Reduzir associações desnecessárias

Não use associações para conteúdo que possa ser facilmente definido estaticamente. Não há nenhuma vantagem em associar dados que não precisam ser associados, pois as associações não são econômicas. Por exemplo, definir 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 capaz de exibir vários filhos, mas que só tem um único filho, é um desperdício. Por exemplo, o exemplo a seguir mostra um VerticalStackLayout com um único 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 exemplo a seguir:

<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 em cálculos de layout desnecessários sendo executados. Por exemplo, não tente reproduzir um layout de Grid usando uma combinação de elementos HorizontalStackLayout. O exemplo a seguir mostra um exemplo dessa prática incorreta:

<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 são executados cálculos de layout desnecessários. Em vez disso, o layout desejado pode ser melhor obtido usando um Grid, conforme mostrado no exemplo a seguir:

<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 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. É um desperdício decodificar uma imagem de alta resolução na memória quando ela será dimensionada 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 um modo de exibição de lista provavelmente deve ser 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 lendo seus dados de um fluxo, verifique se o fluxo é criado somente quando for necessário e verifique se o fluxo é liberado quando ele não for mais necessário. Isso pode ser feito criando o fluxo quando a página é criada ou quando o evento Page.Appearing é acionado e 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 é armazenada em cache por um período adequado de tempo. Para obter mais informações, consulte cache de imagem.

Reduzir o número de elementos em uma página

Reduzir o número de elementos em uma página fará com que a página seja renderizada mais rapidamente. Há duas técnicas principais para conseguir isso. A primeira é ocultar elementos que não estão visíveis. A propriedade IsVisible de cada elemento determina se o elemento deve estar visível na tela. 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. Definir a propriedade IsVisible em um elemento para false mantém o elemento na árvore visual, mas exclui-o dos cálculos de renderização e layout.

A segunda técnica é remover elementos desnecessários. Por exemplo, o seguinte 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 exemplo a seguir:

<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 em todo o 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 exemplo a seguir 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 processados 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 exemplo a seguir mostra o recurso HeadingLabelStyle, que está apenas em uma única 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 recursos de aplicativo, consulte Estilize aplicativos usando 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 do aplicativo. O ILLink reduz o tamanho analisando o código intermediário produzido pelo compilador. Ele remove métodos, propriedades, campos, eventos, structs e classes não utilizados para produzir um aplicativo que contém apenas dependências de código e assembly necessárias para executar o aplicativo.

Para obter mais informações sobre como configurar o comportamento do vinculador, consulte Vincular um aplicativo Android, Vincular um aplicativo iOSe 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 fornece 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 eles obtenham uma primeira impressão favorável do aplicativo.

Antes que um aplicativo exiba sua interface do usuário inicial, ele deve fornecer uma tela inicial para indicar ao usuário que o aplicativo está sendo iniciado. Se o aplicativo não puder exibir rapidamente sua 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, para oferecer garantia de que o aplicativo não foi travado. 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. Depois que a interface do usuário inicial for exibida e o usuário puder interagir com o aplicativo, os dados de placeholder podem ser progressivamente substituídos por dados 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 ele atrasar o carregamento de assemblies adicionais, pois os assemblies são carregados na primeira vez em que são usados.

Escolha um contêiner de injeção de dependência com cuidado

Os contêineres de injeção de dependência introduzem limitações adicionais de desempenho em aplicativos móveis. Registrar e resolver tipos com um contêiner tem um custo de desempenho devido ao uso de reflexão do 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 dependências profundas, o custo de criação poderá aumentar significativamente. Além disso, o registro de tipo, que geralmente ocorre 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, consulte Injeção de dependência.

Como alternativa, a injeção de dependência pode se tornar mais eficaz implementando-a manualmente usando fábricas.

Criar aplicativos 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á benéfico 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 aplicativos que usam um TabbedPage. Para obter mais informações, consulte visão geral do Shell.

Otimizar o desempenho do ListView

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

  • Inicialização – o intervalo de tempo que começa quando o controle é criado e termina quando os itens são exibidos na tela.
  • Rolagem – a capacidade de rolar pela lista e garantir que a interface do usuário não atrase em relação aos gestos de toque.
  • interação para adicionar, excluir e selecionar itens.

O controle ListView requer um aplicativo para fornecer dados e modelos de célula. Como isso é alcançado terá um grande impacto no desempenho do controle. Para obter mais informações, consulte dados de cache.

Usar programação assíncrona

A capacidade de resposta geral do seu aplicativo pode ser aprimorada e os gargalos de desempenho geralmente são evitados usando 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 nãoperformantes.

Fundamentos

As seguintes diretrizes gerais devem ser seguidas ao usar o TAP:

  • Entenda o ciclo de vida da tarefa, que é representado pela enumeração TaskStatus. Para obter mais informações, consulte O significado de Status da Tarefa e Status da Tarefa.
  • Use o método Task.WhenAll para aguardar assíncronamente a conclusão de várias operações assíncronas, em vez de executar uma série de operações assíncronas individualmente com await. Para obter mais informações, consulte Task.WhenAll.
  • Use o método Task.WhenAny para aguardar de forma assíncrona uma das várias operações assíncronas para concluir. Para obter mais informações, consulte 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, consulte Task.Delay.
  • Execute operações de CPU síncronas intensivas 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 ideais definidos. Para obter mais informações, consulte Task.Run.
  • Evite tentar criar construtores assíncronos. Em vez disso, use eventos de ciclo de vida ou lógica de inicialização separada para await corretamente qualquer inicialização. Para obter mais informações, consulte construtores assíncronos no blog.stephencleary.com.
  • Use o padrão de tarefa lento para evitar aguardar a conclusão de operações assíncronas durante a inicialização do aplicativo. Para obter mais informações, consulte AsyncLazy.
  • Crie um wrapper de tarefa para operações assíncronas existentes, que não usam o TAP, criando TaskCompletionSource<T> objetos. Esses objetos aproveitam os benefícios da programação Task e permitem controlar a durabilidade e a finalização do Taskassociado. Para obter mais informações, consulte The Nature of TaskCompletionSource.
  • Retorne um objeto Task, em vez de retornar um objeto Task aguardado, quando não houver necessidade de processar o resultado de uma operação assíncrona. Isso tem um desempenho melhor devido à menor alternância de contexto.
  • Use a biblioteca de fluxo de dados TPL (Biblioteca Paralela de Tarefas) em cenários como o processamento de dados à medida que eles estiverem disponíveis ou quando você tiver várias operações que devem se comunicar entre si de forma assíncrona. Para obter mais informações, consulte Dataflow (Biblioteca Paralela da Tarefa).

IU (Interface do Usuário)

As diretrizes a seguir devem ser seguidas ao usar o TAP com controles de interface do usuário:

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

  • Atualize os elementos da interface do usuário com dados de operações assíncronas no thread da interface do usuário, para evitar que exceções sejam geradas. No entanto, as atualizações na propriedade ListView.ItemsSource serão automaticamente encaminhadas 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, consulte Criar um thread no thread da interface do usuário.

    Importante

    Todas as propriedades de controle atualizadas por meio da associação de dados serão automaticamente transferidas para a linha de execução da interface do usuário.

Tratamento de erros

As seguintes diretrizes de tratamento de erros devem ser seguidas ao usar o TAP:

  • Saiba mais sobre o tratamento de exceções assíncronas. Exceções sem tratamento geradas por código que está sendo executado de forma assíncrona são propagadas de volta para o thread de chamada, exceto em determinados cenários. Para obter mais informações, consulte Tratamento de Exceções (Biblioteca Paralela de Tarefas).
  • Evite criar métodos async void e, em vez disso, crie métodos async Task. Elas facilitam o tratamento de erros, a composição e a capacidade de teste. A exceção a essa diretriz são manipuladores de eventos assíncronos, que devem retornar void. Para obter mais informações, consulte EvitarNulo Assíncrono.
  • Não misture o bloqueio e o código assíncrono chamando os métodos Task.Wait, Task.Resultou GetAwaiter().GetResult, pois eles podem resultar em 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, consulte Async All the Way e Task Exception Handling in .NET 4.5.
  • Use o método ConfigureAwait sempre que possível para criar código sem contexto. O código sem contexto tem melhor desempenho para aplicativos móveis e é uma técnica útil para evitar deadlock ao trabalhar com uma base de código parcialmente assíncrona. Para obter mais informações, consulte Configurar Contexto.
  • Use tarefas de continuação para funcionalidades como lidar com exceções geradas pela operação assíncrona anterior e cancelar uma continuação antes de ser iniciada ou enquanto ela estiver em execução. Para obter mais informações, consulte Encadeamento de tarefas usando tarefas contínuas.
  • Use uma implementação assíncrona ICommand quando operações assíncronas são invocadas do ICommand. Isso garante que quaisquer exceções na lógica de comando assíncrona possam ser tratadas. Para obter mais informações, consulte 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 usar a inicialização lenta para objetos que são caros de criar 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 lento, conforme mostrado no exemplo a seguir:

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 em que a propriedade Lazy<T>.Value é acessada. O tipo encapsulado é criado e retornado no primeiro acesso e armazenado para acessos futuros.

Para obter mais informações sobre a inicialização lenta, consulte 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 explicitamente os recursos. IDisposable não é um destruidor e só deve ser implementado nas seguintes circunstâncias:

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

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

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

Encapsular o objeto IDisposable em uma instrução using

O exemplo a seguir 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 IDisposablee a instrução using fornece uma sintaxe conveniente que chama o método StreamReader.Dispose no objeto StreamReader antes de sair do escopo. No bloco using, o objeto StreamReader é somente leitura e não pode ser reatribuído. A instrução using também garante que o método Dispose seja chamado mesmo se ocorrer uma exceção, pois o compilador implementa a IL (linguagem intermediária) para um bloco try/finally.

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

O exemplo a seguir mostra como encapsular a chamada para IDisposable.Dispose em um bloco de 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 IDisposablee o bloco finally chama o método StreamReader.Dispose para liberar o recurso. Para obter mais informações, consulte Interface IDisposable.

Cancelar assinatura de eventos

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

O exemplo a seguir 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.

Os ciclos de referência também podem ocorrer ao usar manipuladores de eventos e sintaxe lambda, pois as expressões lambda podem referenciar e manter os objetos 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 exemplo a seguir:

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 ao método anônimo e é usado para assinatura de evento e cancelamento da assinatura.

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

Em algumas situações, é possível criar ciclos de referência fortes que poderiam 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 NSObjecte é fortemente referenciada de Objective-C, conforme mostrado no exemplo a seguir:

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 de 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 não gerenciada de MyView. Quando isso acontece, o runtime do .NET para iOS cria uma instância GCHandle para manter o objeto MyView no código gerenciado ativo, pois não há garantia de que quaisquer objetos gerenciados manterão uma referência a ele. De uma perspectiva de código gerenciado, o objeto MyView seria recuperado após a chamada AddSubview(UIView) se não fosse pelo GCHandle.

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

Em circunstâncias em que 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 em objetos.
  • Interromper manualmente o ciclo definindo o link para o contêiner como 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 exemplo a seguir:

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 vivo. No entanto, o pai mantém a criança viva através da chamada para 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 de par contém a implementação. Por exemplo, ao definir a propriedade Delegate ou a DataSource na classe UITableView.

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

Descartar objetos com referências fortes

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

Para contêineres, substitua o método Dispose para remover os objetos contidos, conforme mostrado no exemplo a seguir:

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 pai, desmarque a referência ao pai na implementação do Dispose:

class MyChild : UIView
{
    MyContainer _container;

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

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