Melhorar o Xamarin.Forms desempenho do aplicativo

Evoluir 2016: Otimizando o desempenho do aplicativo com Xamarin.Forms

O baixo desempenho de aplicativo se apresenta de várias maneiras. Ele pode fazer com que o aplicativo pareça não responder, deixar a rolagem lenta e reduzir a vida útil 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 quanto ao desempenho do aplicativo também deve 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 de Xamarin.Forms aplicativos. Coletivamente, essas técnicas podem reduzir de forma considerável a quantidade de trabalho que está sendo executado por uma CPU e a quantidade de memória consumida por um aplicativo.

Observação

Antes de ler esse artigo, você deve primeiro ler Desempenho de plataforma cruzada, que discute técnicas específicas sem plataforma para melhorar o uso de memória e o desempenho de aplicativos criados usando a plataforma Xamarin.

Habilitar o compilador de XAML

Opcionalmente, XAML pode ser compilado direto na IL (linguagem intermediária) com o compilador XAML (XAMLC). XAMLC oferece vários benefícios:

  • Executa verificação de tempo de compilação de XAML, notificando o usuário de quaisquer erros.
  • Elimina parte da carga e do tempo de instanciação para elementos XAML.
  • Ajuda a reduzir o tamanho do arquivo do assembly final não incluindo mais arquivos .XAML.

O XAMLC está habilitado por padrão em novas Xamarin.Forms soluções. No entanto, talvez seja necessário habilitá-lo em soluções mais antigas. Para saber mais, consulte Compilação de XAML.

Usar associações compiladas

As associações compiladas melhoram o desempenho da associação de dados em Xamarin.Forms aplicativos resolvendo expressões de associação em tempo de compilação, em vez de em runtime com reflexão. A compilação de uma expressão de associação gera código compilado que normalmente resolve uma associação 8 a 20 vezes mais rápido do que ao usar uma associação clássica. Para saber mais, 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 a associação Button.Text a uma propriedade viewmodel string com o valor "Accept".

Usar renderizadores rápidos

Renderizadores rápidos reduzem a inflação e renderizam os custos de Xamarin.Forms controles no Android, nivelando a hierarquia de controle nativa resultante. Isso aprimora o desempenho criando menos objetos, o que resulta em uma árvore visual menos complexa e em menos uso de memória.

A partir da Xamarin.Forms 4.0, todos os aplicativos direcionados FormsAppCompatActivity usam renderizadores rápidos por padrão. Para obter mais informações, veja Renderizadores Rápidos.

Habilitar o rastreamento de inicialização no Android

A compilação AOT (Ahead of Time) no Android minimiza a sobrecarga e o uso de memória da inicialização de aplicativo JIT (Just-in-Time), com o custo de criar um APK muito maior. Uma alternativa é usar o rastreamento de inicialização, que proporciona uma compensação entre o tamanho do APK do Android e o tempo de inicialização, quando comparado à compilação AOT convencional.

Em vez de compilar o máximo possível do aplicativo para código não gerenciado, o rastreamento de inicialização compila apenas o conjunto de métodos gerenciados que representam as partes mais caras da inicialização do aplicativo em um aplicativo em branco Xamarin.Forms . Essa abordagem resulta na redução do tamanho do APK, quando comparado à compilação AOT convencional, enquanto ainda fornece melhorias de inicialização semelhantes.

Habilitar a compactação de layout

A compactação de layout remove os layouts especificados da árvore visual, em uma tentativa de melhorar o desempenho de renderização da página. O benefício de desempenho que isso oferece varia dependendo da complexidade de uma página, da versão do sistema operacional que está sendo usado e do dispositivo no qual o aplicativo está sendo executado. No entanto, os maiores ganhos de desempenho serão observados em versões mais antigas. Para obter mais informações, confira Layout de Compactação.

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 de código mostra um StackLayout com um único filho:

<ContentPage xmlns="http://xamarin.com/schemas/2014/forms"
             xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
             x:Class="DisplayImage.HomePage">
    <StackLayout>
        <Image Source="waterfront.jpg" />
    </StackLayout>
</ContentPage>

Isso é um desperdício e o elemento StackLayout deve ser removido, conforme mostrado no exemplo de código a seguir:

<ContentPage xmlns="http://xamarin.com/schemas/2014/forms"
             xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
             x:Class="DisplayImage.HomePage">
    <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 Grid usando uma combinação de instâncias StackLayout. O código a seguir mostra um exemplo dessa má prática:

<ContentPage xmlns="http://xamarin.com/schemas/2014/forms"
             xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
             x:Class="Details.HomePage"
             Padding="0,20,0,0">
    <StackLayout>
        <StackLayout Orientation="Horizontal">
            <Label Text="Name:" />
            <Entry Placeholder="Enter your name" />
        </StackLayout>
        <StackLayout Orientation="Horizontal">
            <Label Text="Age:" />
            <Entry Placeholder="Enter your age" />
        </StackLayout>
        <StackLayout Orientation="Horizontal">
            <Label Text="Occupation:" />
            <Entry Placeholder="Enter your occupation" />
        </StackLayout>
        <StackLayout Orientation="Horizontal">
            <Label Text="Address:" />
            <Entry Placeholder="Enter your address" />
        </StackLayout>
    </StackLayout>
</ContentPage>

Isso é um desperdício porque cálculos de layout desnecessário são executados. Em vez disso, o layout desejado pode ser melhor obtido usando um Grid, conforme mostra o exemplo de código a seguir:

<ContentPage xmlns="http://xamarin.com/schemas/2014/forms"
             xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
             x:Class="Details.HomePage"
             Padding="0,20,0,0">
    <Grid>
        <Grid.ColumnDefinitions>
            <ColumnDefinition Width="100" />
            <ColumnDefinition Width="*" />
        </Grid.ColumnDefinitions>
        <Grid.RowDefinitions>
            <RowDefinition Height="30" />
            <RowDefinition Height="30" />
            <RowDefinition Height="30" />
            <RowDefinition Height="30" />
        </Grid.RowDefinitions>
        <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 o desempenho do layout

Para obter o melhor desempenho possível do layout, siga estas diretrizes:

  • Reduzir a profundidade das hierarquias de layout especificando os valores de propriedade Margin, permitindo a criação de layouts com menos exibições de encapsulamento. Para saber mais, consulte Margens e preenchimento.
  • Ao usar um Grid, tente garantir que o menor número de linhas e colunas possível seja definido para o tamanho Auto. Cada linha ou coluna dimensionada automaticamente fará o mecanismo de layout realizar cálculos de layout adicionais. Em vez disso, use linhas e colunas de tamanho fixo, se possível. Como alternativa, defina linhas e colunas para ocupar uma quantidade proporcional de espaço com o valor de enumeração GridUnitType.Star, desde que a árvore pai siga essas diretrizes de layout.
  • Não defina as propriedades VerticalOptions e HorizontalOptions de um layout, a menos que necessário. Os valores padrão de LayoutOptions.Fill e LayoutOptions.FillAndExpand permitem a melhor otimização de layout. Alterar essas propriedades tem um custo e consome memória, mesmo ao configurá-las como os valores padrão.
  • Evite usar um RelativeLayout sempre que possível. Isso resultará em a CPU precisar realizar significativamente mais trabalho.
  • Ao usar um AbsoluteLayout, evite usar a propriedade AbsoluteLayout.AutoSize sempre que possível.
  • Ao usar um StackLayout, garanta que apenas um filho seja definido como LayoutOptions.Expands. Essa propriedade garante que o filho especificado ocupe o maior espaço que o StackLayout pode dar a ele e é um desperdício executar esses cálculos mais de uma vez.
  • Evite chamar os métodos da classe Layout, uma vez que eles resultam na execução de cálculos de layout de alto custo. Em vez disso, é provável que o comportamento de layout desejado possa ser obtido configurando as propriedades TranslationX e TranslationY. Como alternativa, subclasse da classe Layout<View> para obter o comportamento de layout desejado.
  • Não atualize nenhuma instância Label com mais frequência do que o necessário, pois a alteração do tamanho do rótulo pode resultar no recálculo de todo o layout de tela.
  • Não defina a propriedade Label.VerticalTextAlignment, a menos que necessário.
  • Defina o LineBreakMode de quaisquer instâncias Label como NoWrap sempre que possível.

usar programação assíncrona

A capacidade de resposta geral do 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. Portanto, as diretrizes a seguir devem ser seguidas ao usar o TAP.

Conceitos básicos

  • Entenda o ciclo de vida da tarefa, que é representado pela TaskStatus enumeração . Para obter mais informações, consulte O significado de TaskStatus e Task status.

  • Use o Task.WhenAll método para aguardar assíncronamente a conclusão de várias operações assíncronas, em vez de individualmente await uma série de operações assíncronas. Para obter mais informações, consulte Task.WhenAll.

  • Use o Task.WhenAny método para aguardar assíncronamente a conclusão de uma das várias operações assíncronas. Para obter mais informações, consulte Task.WhenAny.

  • Use o Task.Delay método para produzir um Task objeto que é concluído após a hora especificada. Isso é útil para cenários como sondagem de dados e atraso na manipulação 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 Task.Run método . Esse método é um atalho para o TaskFactory.StartNew método , 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 fazer a inicialização corretamente await . Para obter mais informações, consulte Construtores assíncronos no 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, consulte AsyncLazy.

  • Crie um wrapper de tarefas para operações assíncronas existentes, que não usam o TAP, criando TaskCompletionSource<T> objetos. Esses objetos obtêm os benefícios da Task programação e permitem que você controle o tempo de vida e a conclusão do associado Task. Para obter mais informações, consulte A Natureza de TaskCompletionSource.

  • Retornar um Task objeto, em vez de retornar um objeto aguardado Task , quando não houver necessidade de processar o resultado de uma operação assíncrona. Isso é mais eficaz devido à menor alternância de contexto que está sendo executada.

  • Use a biblioteca de fluxo de dados TPL (Biblioteca Paralela de Tarefas) em cenários como o processamento de dados conforme eles se tornam 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 Fluxo de dados (Biblioteca Paralela de Tarefas).

Interface do usuário

  • Chame uma versão assíncrona de uma API, se ela estiver disponível. Isso manterá o thread de interface do usuário desbloqueado, 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 para a ListView.ItemsSource propriedade serão automaticamente marshaladas 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 Xamarin.Essentials: MainThread.

    Importante

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

Tratamento de erros

  • 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 async void métodos e, em vez disso, crie async Task métodos. Elas permitem facilitar 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 Evitar Void Assíncrono.
  • Não misture o bloqueio e o código assíncrono chamando os Task.Waitmétodos , Task.Resultou GetAwaiter().GetResult , pois eles podem resultar em deadlock. No entanto, se essa diretriz precisar ser violada, a abordagem preferencial será chamar o GetAwaiter().GetResult método porque preserva as exceções de tarefa. Para obter mais informações, consulte Async All the Way and Task Exception Handling in .NET 4.5.
  • Use o ConfigureAwait método 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 está em execução. Para obter mais informações, consulte Encadeamento de tarefas usando tarefas contínuas.
  • Use uma implementação assíncrona quando operações assíncronas ICommand forem 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.

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

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 geralmente ocorre durante a inicialização do aplicativo, pode ter um impacto perceptível sobre o tempo de inicialização dependendo do contêiner que está sendo usado.

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

Criar aplicativos de Shell

Xamarin.Forms Os aplicativos 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 Shell, será benéfico fazê-lo. Aplicativos de Shell ajudam a evitar uma experiência de inicialização ruim, pois as páginas são criadas sob demanda em resposta à navegação, e não na inicialização do aplicativo, o que ocorre com aplicativos que usam uma `TabbedPage'. Para obter mais informações, consulte Xamarin.Forms Shell.

Usar CollectionView em vez de ListView

CollectionView é uma exibição para apresentar listas de dados usando especificações de layout diferentes. Ela fornece uma alternativa mais flexível e de alto desempenho a ListView. Para obter mais informações, consulte Xamarin.Forms CollectionView.

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 requer 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, consulte Desempenho da ListView.

Otimizar os recursos de imagem

Exibir recursos de imagem pode aumentar significativamente o volume de memória de um aplicativo. Portanto, eles só devem ser criados quando necessário e devem ser liberados assim que o aplicativo não exigi-los mais. Por exemplo, se um aplicativo estiver exibindo uma imagem lendo seus dados de um fluxo, certifique-se de que esse fluxo seja criado somente quando necessário e liberado quando 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, em seguida, descartando o fluxo quando o evento Page.Disappearing é acionado.

Ao fazer o download de uma imagem para exibição com o método ImageSource.FromUri, armazene em cache a imagem baixada garantindo que a propriedade UriImageSource.CachingEnabled esteja definida como true. Para saber mais, consulte Trabalhando com imagens.

Para saber mais, consulte Otimizar recursos 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, o código a seguir mostra um layout da página contendo múltiplos objetos Label:

<StackLayout>
    <StackLayout Padding="20,20,0,0">
        <Label Text="Hello" />
    </StackLayout>
    <StackLayout Padding="20,20,0,0">
        <Label Text="Welcome to the App!" />
    </StackLayout>
    <StackLayout Padding="20,20,0,0">
        <Label Text="Downloading Data..." />
    </StackLayout>
</StackLayout>

O mesmo layout da página poderá ser mantido com uma contagem de elementos reduzida, conforme mostrado no exemplo de código a seguir:

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

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 duplicação. Isso ajudará a reduzir a quantidade de XAML que precisa ser analisada em todo o aplicativo. O seguinte exemplo de código mostra o recurso HeadingLabelStyle, que é usado em todo o aplicativo e então é definido no dicionário de recursos do aplicativo:

<Application xmlns="http://xamarin.com/schemas/2014/forms"
             xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
             x:Class="Resources.App">
     <Application.Resources>
         <ResourceDictionary>
            <Style x:Key="HeadingLabelStyle" TargetType="Label">
                <Setter Property="HorizontalOptions" Value="Center" />
                <Setter Property="FontSize" Value="Large" />
                <Setter Property="TextColor" Value="Red" />
            </Style>
         </ResourceDictionary>
     </Application.Resources>
</Application>

No entanto, o XAML específico de uma página não deve ser incluído no dicionário de recursos do aplicativo, uma vez que os recursos então serão analisados na inicialização do aplicativo, em vez de quando exigido 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 para essa página, ajudando, assim, a reduzir o XAML analisado quando o aplicativo é iniciado. O seguinte exemplo de código mostra o recurso HeadingLabelStyle, que está em apenas uma única página e então é definido no dicionário de recursos da página:

<ContentPage xmlns="http://xamarin.com/schemas/2014/forms"
             xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
             x:Class="Test.HomePage"
             Padding="0,20,0,0">
    <ContentPage.Resources>
        <ResourceDictionary>
          <Style x:Key="HeadingLabelStyle" TargetType="Label">
              <Setter Property="HorizontalOptions" Value="Center" />
              <Setter Property="FontSize" Value="Large" />
              <Setter Property="TextColor" Value="Red" />
          </Style>
        </ResourceDictionary>
    </ContentPage.Resources>
    ...
</ContentPage>

Para saber mais sobre os recursos de aplicativo, consulte Estilos de XAML.

Usar o padrão de renderizador personalizado

A maioria das Xamarin.Forms classes de renderizador expõe o OnElementChanged método , que é chamado quando um Xamarin.Forms controle personalizado é criado para renderizar o controle nativo correspondente. Então, classes de renderizador personalizadas em cada projeto da plataforma substituem esse método para instanciar e personalizar o controle nativo. O método SetNativeControl é usado para instanciar o controle nativo e esse método também atribuirá a referência de controle à propriedade Control.

No entanto, em algumas circunstâncias, o método OnElementChanged pode ser chamado várias vezes. Portanto, para evitar perdas de memória, que podem ter um impacto no desempenho, é necessário ter cuidado ao instanciar um novo controle nativo. A abordagem a ser usada ao instanciar um novo controle nativo em um renderizador personalizado é mostrada no exemplo de código a seguir:

protected override void OnElementChanged (ElementChangedEventArgs<NativeListView> e)
{
  base.OnElementChanged (e);

  if (e.OldElement != null)
  {
    // Unsubscribe from event handlers and cleanup any resources
  }

  if (e.NewElement != null)
  {
    if (Control == null)
    {
      // Instantiate the native control with the SetNativeControl method
    }
    // Configure the control and subscribe to event handlers
  }
}

Um novo controle nativo deve ser instanciado apenas uma vez, quando a propriedade Control é null. Além disso, o controle só deve ser criado, configurado e manipuladores de eventos inscritos quando o renderizador personalizado é anexado a um novo Xamarin.Forms elemento. Da mesma forma, a inscrição de quaisquer manipuladores de evento inscritos só deve ser cancelada quando o elemento ao qual o renderizador está anexado for alterado. Adotar essa abordagem ajudará a criar um renderizador personalizado de desempenho eficiente que não sofra perdas de memória.

Importante

O método SetNativeControl só deverá ser invocado se a propriedade e.NewElement não for null e a propriedade Control for null.

Para obter mais informações sobre renderizadores personalizados, consulte Como personalizar controles em cada plataforma.