Renderização lenta

A renderização da interface é o ato de gerar um frame do seu app e mostrá-lo na tela. Para garantir que a interação do usuário com o app seja suave, ele precisa renderizar frames em menos de 16 ms para atingir 60 quadros por segundo (QPS). Para entender por que 60 QPS é o melhor valor, consulte Padrões de desempenho do Android: por que 60 QPS?. Se você está tentando atingir 90 QPS, essa janela cai para 11 ms, e para 8 ms a 120 QPS.

Se você ultrapassar essa janela em 1 ms, o frame não vai aparecer com 1 ms de atraso, mas será descartado pelo Choreographer. Se o app tiver uma renderização lenta da interface, o sistema será forçado a pular frames, e o usuário vai perceber a renderização lenta do app. Isso é chamado de instabilidade. Esta página mostra como diagnosticar e corrigir a instabilidade.

Se você está desenvolvendo jogos que não usam o sistema View, ignore o Choreographer. Nesse caso, a biblioteca Frame Pacing ajuda os jogos OpenGL e Vulkan a renderizar e corrigir o ritmo dos frames no Android.

Para melhorar a qualidade do app, o Android faz um monitoramento automático em busca de instabilidades e mostra as informações no painel do Android vitals. Para saber como os dados são coletados, consulte Monitorar a qualidade técnica do app com o Android vitals.

Identificar instabilidade

Identificar qual parte do código do app está causando instabilidade pode ser difícil. Esta seção descreve três métodos para identificar a instabilidade:

A inspeção visual permite executar rapidamente todos os casos de uso no app em alguns minutos, mas não fornece tantos detalhes quanto o Systrace. O Systrace fornece mais detalhes, mas se você executá-lo em todos os casos de uso no app, poderá receber um número tão grande de dados que será difícil fazer uma análise. Tanto a inspeção visual quanto o Systrace detectam instabilidade no dispositivo local. Se a instabilidade não puder ser reproduzida em dispositivos locais, crie um monitoramento de desempenho personalizado para avaliar partes específicas do app em dispositivos executados em campo.

Inspeção visual

A inspeção visual ajuda a identificar os casos de uso que estão produzindo instabilidade. Para realizar uma inspeção visual, abra o app, percorra manualmente as diferentes partes dele e procure instabilidade na interface.

Confira algumas dicas para realizar inspeções visuais:

  • Execute uma versão de lançamento do app ou pelo menos uma versão não depurável. A execução do ART desativa várias otimizações importantes para que haja suporte a recursos de depuração. Por isso, confira se você está analisando algo semelhante ao que vai aparecer para o usuário.
  • Ative a Criação do perfil de renderização de GPU. A Criação do perfil de renderização de GPU mostra barras na tela que fornecem uma representação visual do tempo necessário para renderizar os frames de uma janela da interface em relação ao comparativo de 16 ms por frame. Cada barra tem componentes coloridos que mapeiam uma etapa do pipeline de renderização para que você saiba qual parte está demorando mais tempo. Por exemplo, se o frame gasta muito tempo com a entrada, analise o código do app referente à entrada do usuário.
  • Percorra os componentes que são fontes comuns de instabilidade, como RecyclerView.
  • Faça uma inicialização a frio do app.
  • Execute o app em um dispositivo mais lento para agravar o problema.

Quando você encontrar casos de uso que produzem instabilidade, poderá ter uma boa ideia do que está causando esse problema no app. Se precisar de mais informações, use o Systrace para ter mais detalhes.

Systrace

Embora o Systrace seja uma ferramenta que mostre o que todo o dispositivo está fazendo, ele pode ser útil para identificar instabilidade no app. O Systrace tem uma sobrecarga mínima do sistema, resultando em uma instabilidade realista durante a instrumentação.

Registre um trace com o Systrace enquanto executa o caso de uso de instabilidade no dispositivo. Para saber mais sobre como usar o Systrace, consulte Capturar rastros do sistema na linha de comando. O Systrace é dividido em processos e linhas de execução. Procure o processo do app no Systrace, que será parecido com a Figura 1.

Exemplo do Systrace
Figura 1. Exemplo do Systrace.

O exemplo do Systrace da Figura 1 contém as seguintes informações para identificar instabilidade:

  1. O Systrace mostra quando cada frame é exibido e codifica cada frame por cor para destacar os tempos de renderização lenta. Isso ajuda a encontrar frames instáveis com mais precisão do que a fornecida pela inspeção visual. Para saber mais, consulte Inspecionar frames e alertas da interface.
  2. O Systrace detecta problemas no app e mostra alertas em frames individuais e no painel de alertas. É melhor seguir as instruções do alerta.
  3. Partes do framework e das bibliotecas do Android, como RecyclerView, contêm marcadores de rastreamento. Portanto, a linha do tempo do Systrace mostra quando e em quanto tempo esses métodos são executados na linha de execução da interface.

Depois de analisar a saída do Systrace, poderá haver métodos no app suspeitos de estarem causando instabilidade. Por exemplo, se a linha do tempo mostrar que um frame lento é causado pela demora da RecyclerView, você poderá adicionar eventos de rastreamento personalizados ao código relevante e executar o Systrace novamente para receber mais informações. No novo Systrace, a linha do tempo mostra quando os métodos do app são chamados e quanto tempo eles levam para ser executados.

Se o Systrace não mostrar detalhes do motivo por que o trabalho da linha de execução de interface está demorando, use o Android CPU Profiler para registrar um rastreamento de método instrumentado ou de exemplo. Geralmente, os rastreamentos de método não são bons para identificar instabilidade porque produzem falsos positivos devido à sobrecarga pesada e não podem perceber quando linhas de execução estão sendo executadas ou estão bloqueadas. No entanto, os rastreamentos de método podem ajudar a identificar os métodos do app que estão demorando mais. Depois de identificar esses métodos, adicione marcadores de trace e execute novamente o Systrace para conferir se eles estão causando instabilidade.

Para saber mais, consulte Entender o Systrace.

Monitoramento de desempenho personalizado

Se não for possível reproduzir a instabilidade em um dispositivo local, crie um monitoramento de desempenho personalizado no app para identificar a origem da instabilidade nos dispositivos em campo.

Para fazer isso, colete os tempos de renderização do frame de partes específicas do app com o FrameMetricsAggregator e grave e analise os dados usando o Monitoramento de desempenho do Firebase.

Para saber mais, consulte Introdução ao Monitoramento de desempenho para Android.

Frames congelados

Os frames congelados são frames da IU que levam mais de 700 ms para serem renderizados. Isso é um problema porque seu app parece estar travado e não responde à entrada do usuário por quase um segundo enquanto o frame está renderizando. Recomendamos otimizar os apps para renderizar um frame em até 16 ms para garantir uma interface suave. No entanto, durante a inicialização do app ou ao fazer a transição para outra tela, é normal que o frame inicial demore mais de 16 ms para ser renderizado, porque seu app precisa inflar as visualizações, organizar a tela e realizar o desenho inicial do zero. É por isso que o Android rastreia frames congelados separadamente da renderização lenta. Nenhum frame no seu app deve levar mais de 700 ms para renderizar.

Para ajudar a melhorar a qualidade, o Android monitora automaticamente o app em busca de frames congelados e mostra as informações no painel do Android vitals. Para saber como os dados são coletados, consulte Monitorar a qualidade técnica do app com o Android vitals.

Os frames congelados são uma forma extrema de renderização lenta, por isso o procedimento para diagnosticar e corrigir o problema é o mesmo.

Como rastrear instabilidades

FrameTimeline (link em inglês) no Perfetto (links em inglês) pode ajudar a rastrear frames congelados.

Relação entre frames lentos ou congelados e ANRs

Frames lentos ou congelados e ANRs são diferentes formas de instabilidade que podem ser encontradas no app. Consulte a tabela abaixo para entender a diferença.

Frames lentos Frames congelados ANRs
Tempo de renderização Entre 16 ms e 700 ms Entre 700 ms e 5 s Maior que 5 s
Área de impacto visível para os usuários
  • Rolagem de RecyclerView com comportamento abrupto
  • Em telas com animações complexas que não são animadas corretamente
  • Durante a inicialização do app
  • Ao passar de uma tela para outra (por exemplo, uma transição de tela)
  • Enquanto a atividade estava em primeiro plano, o app não respondeu a um evento de entrada ou BroadcastReceiver, como eventos de pressionamento de tecla ou toque na tela, em um intervalo de cinco segundos.
  • Enquanto não havia uma atividade em primeiro plano, o BroadcastReceiver não terminou a execução em um período de tempo considerável.

Rastrear frames lentos e frames congelados separadamente

Durante a inicialização do app ou ao fazer a transição para uma tela diferente, é normal que o frame inicial demore mais de 16 ms para ser renderizado, porque o app precisa inflar as visualizações, organizar a tela e fazer o desenho inicial do zero.

Práticas recomendadas para priorizar e resolver instabilidades

Siga estas práticas recomendadas ao tentar resolver uma instabilidade no seu app:

  • Identifique e resolva os pontos de instabilidade mais fáceis de reproduzir.
  • Priorize ANRs. Embora os frames lentos ou congelados possam fazer com que o app pareça lento, os ANRs fazem com que ele pare de responder.
  • A renderização lenta é difícil de reproduzir, mas você pode começar eliminando frames congelados por 700ms. Isso é mais comum enquanto o app está inicializando ou mudando de tela.

Corrigir a instabilidade

Para corrigir a instabilidade, inspecione quais frames não estão sendo concluídos em 16 ms e verifique o que está errado. Confira se Record View#draw ou Layout estão demorando muito em alguns frames. Consulte Fontes comuns de instabilidade para saber mais sobre esses e outros problemas.

Para evitar instabilidade, execute tarefas de longa duração de forma assíncrona fora da linha de execução de interface. Esteja sempre ciente de qual linha de execução seu código está gerando e tenha cuidado ao postar tarefas não triviais na linha de execução principal.

Se você tem uma interface principal complexa e importante para o app, como a lista de rolagem central, faça testes de instrumentação que possam detectar automaticamente tempos de renderização lenta e execute esses testes com frequência para evitar regressões.

Fontes comuns de instabilidade

As seções a seguir explicam as fontes comuns de instabilidade em apps usando o sistema View, bem como práticas recomendadas para resolvê-las. Para saber mais sobre como corrigir problemas de desempenho com o Jetpack Compose, consulte Desempenho do Jetpack Compose.

Listas roláveis

ListView e RecyclerView, principalmente, são usadas com frequência para listas de rolagem complexas e mais suscetíveis à instabilidade. Ambas contêm marcadores do Systrace, então você pode usar a ferramenta para descobrir se elas estão contribuindo para a instabilidade no app. Transmita o argumento de linha de comando -a <your-package-name> para que as seções de rastreamento da RecyclerView sejam mostradas, bem como todos os marcadores de rastros adicionados. Se disponível, siga a orientação dos alertas gerados na saída do Systrace. Dentro do Systrace, clique em seções rastreadas pela RecyclerView para conferir uma explicação do trabalho que RecyclerView está fazendo.

RecyclerView: notifyDataSetChanged()

Caso você perceba todos os itens da RecyclerView sendo recuperados em um frame (e, portanto, sendo organizados e mostrados novamente), confira se você não chamou notifyDataSetChanged(), setAdapter(Adapter) ou swapAdapter(Adapter, boolean) para pequenas atualizações. Esses métodos sinalizam que há mudanças em todo o conteúdo da lista e aparecem no Systrace como RV FullInvalidate. Em vez disso, use SortedList ou DiffUtil para gerar atualizações mínimas quando o conteúdo for mudado ou adicionado.

Por exemplo, considere um app que recebe uma nova versão de uma lista de notícias de um servidor. Quando você posta essas informações no Adapter, é possível chamar notifyDataSetChanged(), conforme mostrado no exemplo a seguir:

Kotlin

fun onNewDataArrived(news: List<News>) {
    myAdapter.news = news
    myAdapter.notifyDataSetChanged()
}

Java

void onNewDataArrived(List<News> news) {
    myAdapter.setNews(news);
    myAdapter.notifyDataSetChanged();
}

A desvantagem disso é que, se houver uma mudança trivial, como um único item adicionado à parte de cima, a RecyclerView não terá conhecimento dela. Portanto, é recomendado descartar todo o estado do item armazenado em cache, gerando a necessidade de vincular tudo novamente.

Recomendamos o uso de DiffUtil, que calcula e envia atualizações mínimas para você:

Kotlin

fun onNewDataArrived(news: List<News>) {
    val oldNews = myAdapter.items
    val result = DiffUtil.calculateDiff(MyCallback(oldNews, news))
    myAdapter.news = news
    result.dispatchUpdatesTo(myAdapter)
}

Java

void onNewDataArrived(List<News> news) {
    List<News> oldNews = myAdapter.getItems();
    DiffResult result = DiffUtil.calculateDiff(new MyCallback(oldNews, news));
    myAdapter.setNews(news);
    result.dispatchUpdatesTo(myAdapter);
}

Para informar ao DiffUtil como inspecionar as listas, defina MyCallback como uma implementação de Callback.

RecyclerView: RecyclerViews aninhados

É comum aninhar várias instâncias de RecyclerView, principalmente com uma lista vertical de listas de rolagem horizontal. Um exemplo disso são as grades de apps na página principal da Play Store. Isso pode funcionar muito bem, mas também há muitas visualizações em movimento.

Caso haja uma grande quantidade de itens internos inflados ao rolar a página para baixo pela primeira vez, confira se você está compartilhando RecyclerView.RecycledViewPool entre instâncias internas (horizontais) de RecyclerView. Por padrão, cada RecyclerView tem o próprio pool de itens. No entanto, no caso de uma dúzia de itemViews na tela ao mesmo tempo, é problemático quando itemViews não podem ser compartilhadas pelas diferentes listas horizontais se todas as linhas estão mostrando tipos semelhantes de visualizações.

Kotlin

class OuterAdapter : RecyclerView.Adapter<OuterAdapter.ViewHolder>() {

    ...

    override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
        // Inflate inner item, find innerRecyclerView by ID.
        val innerLLM = LinearLayoutManager(parent.context, LinearLayoutManager.HORIZONTAL, false)
        innerRv.apply {
            layoutManager = innerLLM
            recycledViewPool = sharedPool
        }
        return OuterAdapter.ViewHolder(innerRv)
    }
    ...

Java

class OuterAdapter extends RecyclerView.Adapter<OuterAdapter.ViewHolder> {
    RecyclerView.RecycledViewPool sharedPool = new RecyclerView.RecycledViewPool();

    ...

    @Override
    public void onCreateViewHolder(ViewGroup parent, int viewType) {
        // Inflate inner item, find innerRecyclerView by ID.
        LinearLayoutManager innerLLM = new LinearLayoutManager(parent.getContext(),
                LinearLayoutManager.HORIZONTAL);
        innerRv.setLayoutManager(innerLLM);
        innerRv.setRecycledViewPool(sharedPool);
        return new OuterAdapter.ViewHolder(innerRv);

    }
    ...

Se você quiser otimizar ainda mais, também é possível chamar setInitialPrefetchItemCount(int) no LinearLayoutManager do componente interno RecyclerView. Se, por exemplo, você sempre tiver 3,5 itens visíveis em uma linha, chame innerLLM.setInitialItemPrefetchCount(4). Isso vai sinalizar à RecyclerView que, quando uma linha horizontal estiver prestes a aparecer na tela, ela precisará tentar pré-buscar os itens dentro dela se houver tempo livre na linha de execução de interface.

RecyclerView: inflação exagerada ou demora na criação

Na maioria dos casos, o recurso de pré-busca da RecyclerView pode ajudar a contornar o custo da inflação fazendo o trabalho com antecedência enquanto a linha de execução de interface está ociosa. Caso você observe inflação durante um frame, e não em uma seção chamada RV Prefetch, confira se está testando em um dispositivo com suporte e usando uma versão recente da Biblioteca de Suporte. A pré-busca só tem suporte a partir do Android 5.0 (nível 21 da API).

Se você observa com frequência que a inflação está causando instabilidade quando novos itens aparecem na tela, confira se não há mais tipos de visualização do que o necessário. Quanto menos tipos de visualização houver no conteúdo da RecyclerView, menos inflação precisará ser feita quando novos tipos de item aparecerem na tela. Se possível, mescle tipos de visualização sempre que for razoável. Se apenas um ícone, cor ou texto for diferente entre os tipos, você poderá fazer essa mudança no momento da vinculação e evitar inflação, reduzindo o consumo de memória do app ao mesmo tempo.

Caso não haja problemas com os tipos de visualização, tente reduzir o custo da sua inflação. Reduzir visualizações de contêiner e estruturais desnecessárias pode ajudar. Considere criar itemViews com ConstraintLayout, o que pode reduzir as visualizações estruturais.

Se você quer otimizar ainda mais o desempenho, suas hierarquias de itens são simples e você não precisa de recursos complexos de temas e estilo, chame os construtores. No entanto, muitas vezes não vale a pena fazer isso, porque você vai perder a simplicidade e os recursos de XML

RecyclerView: vinculação muito demorada

A vinculação, ou seja, onBindViewHolder(VH, int), precisa ser simples e levar menos de um milissegundo para tudo, exceto os itens mais complexos. Ela precisa extrair itens de objetos simples antigos do Java (POJO, na sigla em inglês) dos dados de itens internos do adaptador e chamar setters em visualizações no ViewHolder. Se RV OnBindView está demorando muito, verifique se você está fazendo um trabalho mínimo no código de vinculação.

Caso você esteja usando objetos POJO simples para reter dados no adaptador, evite gravar o código de vinculação em onBindViewHolder usando a biblioteca Data Binding.

RecyclerView ou ListView: layout/exibição demorando muito

Para problemas com exibição e layout, consulte as seções Desempenho do layout e Desempenho da renderização.

ListView: inflação

Você poderá desativar acidentalmente a reciclagem na ListView se não tomar cuidado. Caso você observe inflação toda vez que um item aparece na tela, confira se a implementação de Adapter.getView() está usando, vinculando novamente e retornando o parâmetro convertView. Se a implementação de getView() sempre inflar, o app não terá os benefícios da reciclagem na ListView. A estrutura de getView() precisa ser quase sempre semelhante à esta implementação:

Kotlin

fun getView(position: Int, convertView: View?, parent: ViewGroup): View {
    return (convertView ?: layoutInflater.inflate(R.layout.my_layout, parent, false)).apply {
        // Bind content from position to convertView.
    }
}

Java

View getView(int position, View convertView, ViewGroup parent) {

    if (convertView == null) {
        // Only inflate if no convertView passed.
        convertView = layoutInflater.inflate(R.layout.my_layout, parent, false)
    }
    // Bind content from position to convertView.
    return convertView;
}

Desempenho do layout

Se o Systrace mostra que o segmento Layout do Choreographer#doFrame está trabalhando muito ou com muita frequência, isso significa que você está enfrentando problemas de desempenho do layout. O desempenho do layout do app depende de qual parte da hierarquia de visualização tem parâmetros ou entradas de layout que mudam.

Desempenho do layout: custo

Se os segmentos forem mais longos que alguns milésimos de segundo, é possível que você esteja tendo o pior desempenho de aninhamento para RelativeLayouts ou weighted-LinearLayouts. Cada um desses layouts pode acionar várias transmissões de medida e layout dos filhos, portanto, aninhá-los pode levar ao comportamento O(n^2) na profundidade do aninhamento.

Evite RelativeLayout ou o recurso de ponderação de LinearLayout em todos os nós folha da hierarquia, exceto nos menores. Confira como fazer isso:

  • Reorganize suas visualizações estruturais.
  • Defina a lógica de layout personalizada. Acesse Otimizar hierarquias de layout para consultar um exemplo específico. Você pode tentar converter para o ConstraintLayout, que fornece recursos parecidos sem as desvantagens para o desempenho.

Desempenho do layout: frequência

O layout precisa acontecer quando conteúdo novo aparece na tela, por exemplo, quando um novo item aparece na RecyclerView. Se um layout significativo está acontecendo em cada frame, é possível que você esteja animando o layout e provavelmente causando frames perdidos.

Geralmente, as animações precisam ser executadas em propriedades de exibição de View, como estas:

Você pode mudar todas essas opções de maneira muito mais econômica que as propriedades de layout, como usando padding ou margens. Também é muito mais econômico mudar as propriedades de exibição de uma visualização chamando um setter, que aciona um invalidate() seguido por draw(Canvas) no frame seguinte. Isso vai regravar as operações de exibição para a visualização que é invalidada, sendo também muito mais econômico que o layout.

Desempenho de renderização

A interface do Android funciona em duas etapas:

  • Record View#draw na linha de execução de interface, que executa draw(Canvas) em todas as visualizações inválidas e pode invocar chamadas em visualizações personalizadas ou no código.
  • DrawFrame na RenderThread, que é executado na RenderThread nativa, mas funciona com base no trabalho gerado pela etapa Record View#draw.

Desempenho da renderização: linha de execução de interface

Se a etapa Record View#draw estiver demorando muito, normalmente é porque um bitmap está sendo pintado na linha de execução de interface. A pintura de um bitmap usa a renderização da CPU. Portanto, evite isso quando possível. Você pode usar o rastreamento de método com o Android CPU Profiler para conferir se esse é o problema.

A pintura de um bitmap geralmente é feita quando um app quer decorar um bitmap antes de mostrá-lo. Às vezes, a decoração pode ser a adição de cantos arredondados:

Kotlin

val paint = Paint().apply {
    isAntiAlias = true
}
Canvas(roundedOutputBitmap).apply {
    // Draw a round rect to define the shape:
    drawRoundRect(
            0f,
            0f,
            roundedOutputBitmap.width.toFloat(),
            roundedOutputBitmap.height.toFloat(),
            20f,
            20f,
            paint
    )
    paint.xfermode = PorterDuffXfermode(PorterDuff.Mode.MULTIPLY)
    // Multiply content on top to make it rounded.
    drawBitmap(sourceBitmap, 0f, 0f, paint)
    setBitmap(null)
    // Now roundedOutputBitmap has sourceBitmap inside, but as a circle.
}

Java

Canvas bitmapCanvas = new Canvas(roundedOutputBitmap);
Paint paint = new Paint();
paint.setAntiAlias(true);
// Draw a round rect to define the shape:
bitmapCanvas.drawRoundRect(0, 0,
        roundedOutputBitmap.getWidth(), roundedOutputBitmap.getHeight(), 20, 20, paint);
paint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.MULTIPLY));
// Multiply content on top to make it rounded.
bitmapCanvas.drawBitmap(sourceBitmap, 0, 0, paint);
bitmapCanvas.setBitmap(null);
// Now roundedOutputBitmap has sourceBitmap inside, but as a circle.

Se esse for o tipo de trabalho que você faz na linha de execução de interface, poderá fazê-lo na linha de execução de decodificação em segundo plano. Em alguns casos, como no exemplo anterior, você pode até mesmo fazer o trabalho no momento da exibição. Portanto, se o código Drawable ou View tiver esta aparência:

Kotlin

fun setBitmap(bitmap: Bitmap) {
    mBitmap = bitmap
    invalidate()
}

override fun onDraw(canvas: Canvas) {
    canvas.drawBitmap(mBitmap, null, paint)
}

Java

void setBitmap(Bitmap bitmap) {
    mBitmap = bitmap;
    invalidate();
}

void onDraw(Canvas canvas) {
    canvas.drawBitmap(mBitmap, null, paint);
}

Você poderá substituí-lo por este:

Kotlin

fun setBitmap(bitmap: Bitmap) {
    shaderPaint.shader = BitmapShader(bitmap, Shader.TileMode.CLAMP, Shader.TileMode.CLAMP)
    invalidate()
}

override fun onDraw(canvas: Canvas) {
    canvas.drawRoundRect(0f, 0f, width, height, 20f, 20f, shaderPaint)
}

Java

void setBitmap(Bitmap bitmap) {
    shaderPaint.setShader(
            new BitmapShader(bitmap, TileMode.CLAMP, TileMode.CLAMP));
    invalidate();
}

void onDraw(Canvas canvas) {
    canvas.drawRoundRect(0, 0, width, height, 20, 20, shaderPaint);
}

Você também pode fazer isso para proteção em segundo plano, como ao mostrar um gradiente sobre o bitmap e filtrar imagens com ColorMatrixColorFilter, duas outras operações comuns para modificar bitmaps.

Caso esteja mostrando algo sobre um bitmap por outro motivo, possivelmente usando-o como cache, tente mostrar no Canvas com aceleração de hardware transmitido diretamente para a View ou o Drawable. Se necessário, considere também chamar setLayerType() com LAYER_TYPE_HARDWARE para armazenar em cache a saída de renderização complexa e ainda aproveitar a renderização da GPU.

Desempenho de renderização: RenderThread

Algumas operações de Canvas têm gravação econômicas, mas acionam cálculos pesados na RenderThread. O Systrace geralmente as destaca com alertas.

Animar caminhos grandes

Quando Canvas.drawPath() é chamado no Canvas com aceleração de hardware transmitido para View, o Android mostra esses caminhos primeiro na CPU e os envia para a GPU. Se você tem caminhos grandes, evite editá-los frame a frame para que possam ser armazenados em cache e mostrados de forma eficiente. drawPoints(), drawLines() e drawRect/Circle/Oval/RoundRect() são mais eficientes. É melhor usá-los, mesmo que você faça mais chamadas de desenho.

Canvas.clipPath

clipPath(Path) aciona um comportamento de recorte pesado e geralmente precisa ser evitado. Quando possível, mostre formas em vez de recortar e gerar não retângulos. Isso funciona melhor e oferece suporte a antialias. Por exemplo, a chamada clipPath a seguir pode ser expressa de maneira diferente:

Kotlin

canvas.apply {
    save()
    clipPath(circlePath)
    drawBitmap(bitmap, 0f, 0f, paint)
    restore()
}

Java

canvas.save();
canvas.clipPath(circlePath);
canvas.drawBitmap(bitmap, 0f, 0f, paint);
canvas.restore();

Em vez disso, expresse o exemplo anterior da seguinte maneira:

Kotlin

paint.shader = BitmapShader(bitmap, Shader.TileMode.CLAMP, Shader.TileMode.CLAMP)
// At draw time:
canvas.drawPath(circlePath, mPaint)

Java

// One time init:
paint.setShader(new BitmapShader(bitmap, TileMode.CLAMP, TileMode.CLAMP));
// At draw time:
canvas.drawPath(circlePath, mPaint);
Uploads de bitmap

O Android mostra bitmaps como texturas OpenGL, e, na primeira vez que um bitmap aparece em um frame, ele é enviado para a GPU. Você pode conferir isso no Systrace como Texture upload(id) width x height. Isso pode levar vários milissegundos, como mostrado na Figura 2, mas é necessário para mostrar a imagem com a GPU.

Se levar muito tempo, primeiro confira os números de largura e altura no rastreamento. Confira se o bitmap mostrado não é significativamente maior do que a área em que ele é mostrado na tela. Se for, vai haver desperdício de tempo de upload e memória. Geralmente, as bibliotecas de carregamento de bitmap fornecem meios fáceis de solicitar um bitmap de tamanho adequado.

No Android 7.0, o código de carregamento de bitmap, geralmente feito por bibliotecas, pode chamar prepareToDraw() para acionar um upload antecipado antes que seja necessário. Dessa forma, o upload acontece cedo, enquanto a RenderThread está ociosa. Você pode fazer isso depois da decodificação ou ao vincular um bitmap a uma visualização, desde que conheça o bitmap. O ideal seria sua biblioteca de carregamento de bitmaps fazer isso, mas se você estiver gerenciando sua própria biblioteca ou quiser garantir que não vai atingir uploads em dispositivos mais recentes, chame prepareToDraw() no seu código.

Um app gasta um tempo significativo em um frame
  fazendo upload de um bitmap grande
Figura 2. Um app gasta um tempo significativo em um frame fazendo upload de um bitmap grande. Reduza o tamanho dele ou acione-o com antecedência ao decodificá-lo com prepareToDraw().

Atrasos na programação de linhas de execução

O programador de linhas de execução é a parte do sistema operacional Android responsável por decidir quais linhas de execução do sistema precisam ser executadas, quando isso vai acontecer e por quanto tempo.

Às vezes, ocorre instabilidade porque a linha de execução de interface do app está bloqueada ou não está em execução. O Systrace usa cores diferentes, como mostrado na Figura 3, para indicar quando uma linha de execução está em suspensão (cinza), é executável (azul: pode ser executada, mas ainda não foi escolhida pelo programador), está em execução (verde) ou está em suspensão ininterrupta (vermelho ou laranja). Isso é extremamente útil para depurar problemas de instabilidade causados por atrasos de programação da linha de execução.

Destaca um período em que a linha de execução de interface
  está em suspensão
Figura 3. Destaque de um período em que a linha de execução de interface está em suspensão.

Muitas vezes, as chamadas de binder, o mecanismo de PIC no Android, causam pausas longas na execução do app. Em versões mais recentes do Android, esse é um dos motivos mais comuns para a linha de execução de interface interromper a execução. Geralmente, a correção busca evitar chamadas de funções que fazem chamadas de binder. Se for inevitável, coloque o valor em cache ou mova o trabalho para as linhas de execução em segundo plano. À medida que as bases de código ficam maiores, você pode adicionar acidentalmente uma chamada de binder invocando algum método de nível inferior se não tomar cuidado. No entanto, elas podem ser encontradas e corrigidas pelo rastreamento.

Se você tiver transações de binder, poderá capturar as pilhas de chamadas com os seguintes comandos do adb:

$ adb shell am trace-ipc start
… use the app - scroll/animate ...
$ adb shell am trace-ipc stop --dump-file /data/local/tmp/ipc-trace.txt
$ adb pull /data/local/tmp/ipc-trace.txt

Às vezes, chamadas aparentemente inofensivas, como getRefreshRate(), podem acionar transações de binder e causar grandes problemas quando ocorrem com frequência. O rastreamento periódico pode ajudar a encontrar e corrigir esses problemas à medida que eles surgem.

Mostra a linha de execução de interface em suspensão devido a transações de
  binder em um movimento de RV. Mantenha sua lógica de vinculação focada e use trace-ipc para
  rastrear e remover chamadas de binder.
Figura 4. A linha de execução de interface está em suspensão devido a transações de binder em um movimento de RV. Mantenha sua lógica de vinculação simples e use trace-ipc para rastrear e remover chamadas de binder.

Se a atividade de binder não é mostrada, mas você ainda não encontra a linha de execução de interface, confira se não está aguardando algum bloqueio ou uma operação de outra linha de execução. Normalmente, a linha de execução da interface não precisa esperar resultados de outras linhas de execução. Elas é que precisam postar informações.

Alocação de objetos e coleta de lixo

A alocação de objetos e a coleta de lixo (GC, na sigla em inglês) se tornaram um problema menos significativo desde que o ART foi lançado como o ambiente de execução padrão do Android 5.0, mas ainda é possível sobrecarregar as linhas de execução com esse trabalho extra. Não há problema em alocar como resposta a um evento raro que não acontece muitas vezes por segundo, como um usuário tocando em um botão, mas lembre-se de que cada alocação tem um custo. Se a alocação estiver em uma repetição que é chamada com frequência, considere evitá-la para aliviar a carga de GC.

O Systrace vai mostrar se a coleta de lixo está sendo executada com frequência e o Android Memory Profiler poderá mostrar a origem das alocações. Se você evitar alocações sempre que possível, principalmente em repetições apertadas, a probabilidade de ter problemas será menor.

Mostra uma GC de 94 ms no HeapTaskDaemon
Figura 5. Uma GC de 94 ms na linha de execução HeapTaskDaemon.

Em versões recentes do Android, a coleta de lixo geralmente é executada em uma linha de execução em segundo plano chamada HeapTaskDaemon. Quantidades significativas de alocação podem significar mais recursos de CPU gastos em GC, como mostrado na Figura 5.