Renderização lenta

Renderização da IU é o ato de gerar um frame do seu app e exibi-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 (veja o vídeo por que 60 fps?). Se o app realizar uma renderização lenta da IU, o sistema será forçado a ignorar frames e o usuário perceberá oscilações no app. Chamamos isso de instabilidade.

Para ajudar você a melhorar a qualidade, o Android monitora o app automaticamente em busca de instabilidade e exibe as informações no painel do "Android vitals". Para informações sobre como os dados são coletados, consulte os documentos do Play Console.

Se seu app está enfrentando instabilidade, esta página fornece orientações sobre como diagnosticar e corrigir o problema.

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 que você execute 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ê o executasse para todos os casos de uso no app, receberia um número tão grande de dados que seria difícil analisá-los. Tanto a inspeção visual quanto o Systrace detectam instabilidade no dispositivo local. Se a instabilidade não puder ser reproduzida em dispositivos locais, você poderá criar um monitoramento de desempenho personalizado para avaliar partes específicas do app em dispositivos executados em campo.

Com 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 IU. Veja algumas dicas ao realizar inspeções visuais:

  • Execute uma versão de lançamento do app ou pelo menos uma versão não depurável. O tempo de execução de ART desativa várias otimizações importantes para que haja compatibilidade com recursos de depuração. Por isso, verifique se você está analisando algo semelhante ao que o usuário vê.
  • Ative a Criação do perfil de renderização de GPU. A Criação do perfil de renderização de GPU exibe barras na tela que fornecem uma representação visual rápida do tempo necessário para renderizar os frames de uma janela da IU 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ê possa ver 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.
  • Há certos componentes, como o RecyclerView, que são uma fonte comum de instabilidade. Se seu app usa esses componentes, é recomendável percorrer essas partes do app.
  • Às vezes, a instabilidade pode ser reproduzida somente com uma inicialização a frio do app.
  • Tente executar o app em um dispositivo mais lento para realçar o problema.

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

Com o Systrace

Embora o Systrace seja uma ferramenta que mostra o que todo o dispositivo está fazendo, ele pode ser útil para identificar instabilidade no seu app. O Systrace tem uma sobrecarga mínima do sistema, por isso você experimentará uma instabilidade realista durante a instrumentação.

Registre um trace com o Systrace enquanto executa o caso de uso de instabilidade no dispositivo. Veja no Tutorial do Systrace instruções de como usá-lo. O Systrace é dividido em processos e linhas de execução. Procure o processo do seu app no Systrace, que será parecido com a figura 1.

Figura 1: Systrace.

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

  1. O Systrace mostra quando cada frame é desenhado e codifica cada frame por cor para destacar os tempos de renderização lenta. Isso ajuda a encontrar frames individuais instáveis com mais precisão do que a fornecida pela inspeção visual. Para mais informações, consulte Como inspecionar frames.
  2. O Systrace detecta problemas no app e exibe alertas em frames individuais e no painel de alertas. Seguir as instruções do alerta é a melhor opção.
  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 esses métodos são executados na linha de execução de IU e em quanto tempo.

Depois de analisar a saída do Systrace, poderá haver métodos no app que você suspeita que estão causando instabilidade. Por exemplo, se a linha do tempo mostrar que um frame lento é causado pela demora do RecyclerView, você poderá adicionar marcadores de rastros ao código relevante e executar novamente o Systrace para receber mais informações. No novo Systrace, a linha do tempo exibe quando os métodos do app foram chamados e quanto tempo levaram para ser executados.

Se o Systrace não mostrar detalhes do motivo pelo qual o trabalho da linha de execução de IU está demorando, será necessário usar o Android CPU Profiler para gravar um trace de método amostrado ou instrumentado. Geralmente, os traces de método não são bons para identificar instabilidade, porque geram falsos positivos devido a sobrecarga pesada e não podem ver quando linhas de execução estão sendo executadas ou estão bloqueadas. No entanto, os traces de método podem ajudar a identificar os métodos do seu app que estão demorando mais. Depois de identificar esses métodos, adicione marcadores de trace e execute novamente o Systrace para ver se esses métodos estão causando instabilidade.

Para mais informações, consulte Compreensão do Systrace.

Com o monitoramento de desempenho personalizado

Se não for possível reproduzir instabilidade em um dispositivo local, você poderá criar um monitoramento de desempenho personalizado no seu app para ajudar a 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 Usar o Monitoramento de desempenho do Firebase com o "Android vitals".

Corrigir a instabilidade

Para corrigir a instabilidade, inspecione quais frames não estão concluindo em 16,7 ms e verifique o que deu errado. O recurso Record View#draw está demorando demais em alguns frames, ou talvez o Layout? Consulte esses e outros problemas na seção Fontes comuns de instabilidade abaixo.

Para evitar a instabilidade, as tarefas de longa execução precisam ser executadas de forma assíncrona fora da linha de execução de IU. Esteja sempre ciente de qual linha de execução seu código está executando e tenha cuidado ao postar tarefas não triviais na linha de execução principal.

Se você tem uma IU principal complexa e importante para o app (como a lista de rolagem central), grave testes de instrumentação que possam detectar automaticamente tempos de renderização lenta e execute esses testes com frequência para evitar regressões. Para mais informações, consulte o Codelab de testes de desempenho automatizado (link em inglês).

Fontes comuns de instabilidade

As seções a seguir explicam as fontes comuns de instabilidade em apps e as práticas recomendadas para resolvê-las.

Listas roláveis

Os componentes ListView e, especialmente, RecyclerView são usados frequentemente para listas de rolagem complexas, mais suscetíveis a instabilidade. Ambos contêm marcadores do Systrace, então você pode usar a ferramenta para descobrir se eles estão contribuindo para a instabilidade no app. Certifique-se de passar o argumento de linha de comando -a <your-package-name> para que as seções de rastreamento do RecyclerView sejam exibidas, assim como todos os marcadores de rastreamento adicionados. Se disponível, siga a orientação dos alertas gerados na saída do Systrace. Dentro do Systrace, você pode clicar nas seções rastreadas pelo RecyclerView para ver uma explicação do trabalho que ele está fazendo.

RecyclerView: notifyDataSetChanged

Se você vir todos os itens do RecyclerView sendo recuperados em um frame (e, portanto, sendo organizados e desenhados novamente), certifique-se de não chamar notifyDataSetChanged(), setAdapter(Adapter) ou swapAdapter(Adapter, boolean) para pequenas atualizações. Esses métodos indicam que todo o conteúdo da lista mudou e aparecerá no Systrace como RV FullInvalidate. Em vez disso, use SortedList ou DiffUtil para gerar atualizações mínimas quando conteúdo for mudado ou adicionado.

Por exemplo, considere um app que recebe uma nova versão de uma lista de notícias do servidor. Quando você posta essa informação no Adapter, é possível chamar notifyDataSetChanged() como mostrado abaixo:

Kotlin

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

Java

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

Mas isso tem uma grande desvantagem: se for uma mudança trivial (talvez um único item adicionado ao topo), o RecyclerView não tomará conhecimento. Isso porque a instrução é descartar todo o estado dos itens em cache e, portanto, recuperar tudo.

É muito melhor usar o DiffUtil, que calculará e enviará 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);
    }
    

Basta definir MyCallback como uma implementação de DiffUtil.Callback para informar a DiffUtil como inspecionar suas listas.

RecyclerView: RecyclerViews aninhados

É comum aninhar RecyclerViews, especialmente com uma lista vertical contendo listas de rolagem horizontal, como 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, verifique se você está compartilhando os RecyclerView.RecycledViewPools entre RecyclerViews internos (horizontais). Por padrão, cada RecyclerView tem seu próprio pool de itens. No entanto, com uma dúzia de itemViews na tela ao mesmo tempo, é problemático quando os itemViews não podem ser compartilhados pelas listas horizontais, se todas as linhas estiverem exibindo 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 pode chamar setInitialPrefetchItemCount(int) no LinearLayoutManager interno do RecyclerView. Se, por exemplo, sempre houver 3,5 itens visíveis em uma linha, chame innerLLM.setInitialItemPrefetchCount(4);. Isso sinalizará ao RecyclerView que, quando uma linha horizontal estiver prestes a aparecer na tela, ele precisará tentar pré-buscar os itens dentro dela, se houver tempo livre na linha de execução de IU.

RecyclerView: excesso de inflação/criação demorando muito

O recurso de pré-busca do RecyclerView deve ajudar a solucionar o custo da inflação, na maioria dos casos, fazendo o trabalho com antecedência, enquanto a linha de execução de IU está ociosa. Caso você observe inflação durante um frame (e não em uma seção chamada RV Prefetch), certifique-se de que está fazendo o teste em um dispositivo recente e usando uma versão recente da Biblioteca de Suporte. A pré-busca atualmente só é compatível com o Android 5.0, API de nível 21 e posteriores.

Se você observa frequentemente que a inflação está causando instabilidade quando novos itens aparecem na tela, verifique se não há mais tipos de visualização do que o necessário. Quanto menos tipos de visualizações houver em um conteúdo do RecyclerView, menos inflação precisará ser feita quando novos tipos de itens aparecerem na tela. Se possível, mescle tipos de visualização quando for razoável. Se apenas um ícone, cor ou texto for diferente entre os tipos, você poderá fazer essa alteração no tempo de 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. Crie itemViews com ConstraintLayout, o que pode facilitar a redução de visualizações estruturais. Se você quer realmente otimizar o desempenho, se suas hierarquias de itens são simples e você não precisa de recursos complexos de temas e estilo, chame você mesmo os construtores. No entanto, muitas vezes não vale a pena fazer isso, porque você perderá a simplicidade e os recursos de XML.

RecyclerView: vinculação muito demorada

É preciso que a vinculação, ou seja, onBindViewHolder(VH, int), seja muito simples e leve muito menos de um milissegundo para todos os itens, exceto os mais complexos. Ela precisa simplesmente pegar os itens POJO dos dados de itens internos do adaptador e chamar setters em visualizações do ViewHolder. Se o RV OnBindView está demorando muito, verifique se você está fazendo um trabalho mínimo no seu código de vinculação.

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

RecyclerView ou ListView: layout/desenho demorando muito

Para problemas com desenho e layout, consulte as seções sobre Layout e Desempenho da renderização.

ListView: inflação

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

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á fazendo muito trabalho 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 View 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 LinearLayouts ponderados. Cada um desses layouts pode acionar várias passagens de medida/layout dos filhos, portanto, aninhá-los pode levar ao comportamento O(n^2) na profundidade do aninhamento. Tente evitar RelativeLayout ou o recurso de ponderação de LinearLayout em todos os nós de folha da hierarquia, exceto nos menores. Há algumas maneiras de fazer isso:

  • Você pode reorganizar as visualizações estruturais.
  • Você pode definir uma lógica de layout personalizada. Consulte o guia de como otimizar seu layout para ver um exemplo específico.
  • Você pode tentar converter para o ConstraintLayout, que fornece recursos semelhantes, sem as desvantagens para o desempenho.

Desempenho do layout: frequência

Espera-se que o layout aconteça quando novo conteúdo aparece na tela, por exemplo, quando um novo item rola para a visualização no RecyclerView. Se um layout significativo está acontecendo em cada frame, é possível que você esteja animando o layout, o que provavelmente causa frames perdidos. Geralmente, as animações precisam ser executadas nas propriedades de desenho de View (por exemplo: setTranslationX/Y/Z(), setRotation(), setAlpha() etc…). Todas elas podem ser alteradas de maneira muito mais econômica do que as propriedades de layout, como padding ou margens. Também é muito mais barato mudar as propriedades de desenho de uma visualização, geralmente chamando um setter, que aciona um invalidate(), seguido por um draw(Canvas) no frame seguinte. Isso regravará as operações de desenho para a visualização que é invalidada e também é geralmente muito mais econômico que o layout.

Desempenho de renderização

A IU do Android funciona em duas fases: Record View#draw, na linha de execução de IU, e DrawFrame, no RenderThread. A primeira executa o draw(Canvas) em cada View invalidada e pode invocar chamadas para visualizações personalizadas ou para seu código. A segunda é executada no RenderThread nativo, mas funciona com base no trabalho gerado pela fase de Record View#draw.

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

Se a fase Record View#draw estiver demorando muito, normalmente um bitmap será pintado na linha de execução de IU. A pintura em um bitmap usa a renderização da CPU, então você precisa evitar isso quando possível. Você pode usar o rastreamento de métodos com o Android CPU Profiler para ver se esse é o problema.

A pintura em um bitmap geralmente é feita quando um app quer decorar um bitmap antes de exibi-lo. Às vezes, é feita uma decoração como a adição de cantos arredondados:

Kotlin

    val paint = Paint().apply {
        isAntiAlias = true
    }
    Canvas(roundedOutputBitmap).apply {
        // draw a round rect to define 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 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 IU, poderá fazê-lo na linha de execução de decodificação em segundo plano. Em alguns casos como esse, você poderá até mesmo fazer o trabalho no momento do desenho. Então, se seu código de Drawable ou View for parecido com este:

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

Observe que isso pode também ser feito para proteção em segundo plano (desenhando um gradiente na parte superior do bitmap) e filtragem de imagem (com ColorMatrixColorFilter), duas outras operações comuns feitas para modificar bitmaps.

Se você está desenhando em um bitmap por outro motivo, possivelmente usando-o como cache, desenhe diretamente para o Canvas com aceleração de hardware passado para a View ou Drawable. Se necessário, chame também setLayerType() com LAYER_TYPE_HARDWARE para armazenar em cache a renderização complexa e aproveitar a renderização da GPU.

Desempenho de renderização: RenderThread

Algumas operações de Canvas são baratas para gravar, mas acionam cálculos dispendiosos no RenderThread. O Systrace geralmente as destaca com alertas.

Canvas.saveLayer()

Evite Canvas.saveLayer(): ele pode acionar renderizações caras, sem cache e fora da tela de cada frame. Embora o desempenho tenha sido aprimorado no Android 6.0, com otimizações para evitar a renderização de chave de destino na GPU, ainda é recomendável evitar essa API cara, se possível, ou no mínimo passar o Canvas.CLIP_TO_LAYER_SAVE_FLAG ou chamar uma variante que não aceite sinalizações.

Animar caminhos grandes

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

Canvas.clipPath

O clipPath(Path) aciona um comportamento de recorte caro e geralmente precisa ser evitado. Quando possível, opte por desenhar formas, em vez de recortar e gerar não retângulos. Isso funciona melhor e é compatível com anti-alias. Por exemplo, a seguinte chamada de clipPath:

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

Pode ser expressa como:

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 exibe bitmaps como texturas OpenGL, e, na primeira vez que um bitmap é exibido em um frame, ele é enviado para a GPU. Você pode ver isso no Systrace como Upload width x height Texture. Isso pode levar vários milissegundos (veja a Figura 2), mas é necessário para exibir a imagem com a GPU.

Se levar muito tempo, primeiro verifique os números de largura e altura no rastreamento. Certifique-se de que o bitmap exibido não seja significativamente maior do que a área em que ele é mostrado na tela. Se for, 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 o RenderThread está ocioso. Isso pode ser feito após a decodificação ou ao vincular um bitmap a um View, contanto que você conheça o bitmap. Idealmente, sua biblioteca de carregamento de bitmaps fará isso, mas se você estiver gerenciando sua própria biblioteca ou quiser garantir que não atinja uploads em dispositivos mais recentes, chame prepareToDraw() no seu código.

Figura 2: um app gasta mais de 10 ms em um frame que está enviando um bitmap de 1,8 megapixel. Reduza o tamanho dele ou acione-o com antecedência, quando decodificado 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 devem ser executadas, quando e por quanto tempo. Às vezes, ocorre instabilidade porque a linha de execução de IU do app está bloqueada ou não está em execução. O Systrace usa cores diferentes (veja a figura 3) para indicar quando uma linha de execução está em Em suspensão (cinza), Executável (azul: pode ser executada, mas o programador ainda não a executou), Em execução (verde) ou Em suspensão ininterrupta (vermelho ou laranja). Isso é extremamente útil para depurar problemas de instabilidade causados por atrasos de programação de linha de execução.

Figura 3: destaca um período em que a linha de execução de IU está em suspensão.

Muitas vezes, pausas longas na execução do app são causadas por chamadas do binder, o mecanismo de comunicação entre processos (IPC) do Android. Em versões recentes do Android, esse é um dos motivos mais comuns para a linha de execução de IU parar de ser executada. Geralmente, a correção visa evitar chamadas de funções que fazem chamadas de binder. Se for inevitável, coloque o valor em cache ou mova o trabalho para linhas de execução em segundo plano. À medida que as bases de código ficam maiores, é fácil adicionar acidentalmente uma chamada de binder invocando algum método de nível inferior se você não tiver cuidado, mas é igualmente fácil encontrá-las e corrigi-las por meio de rastreamento.

Se você tiver transações de binder, poderá capturar as pilhas de chamadas com os seguintes comandos 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 são chamadas com frequência. O rastreamento periódico pode ajudar você a encontrar e corrigir esses problemas rapidamente à medida que surgirem.

Figura 4: mostra a linha de execução de IU inativa 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 você não observar atividade de binder, mas ainda não observar a linha de execução de IU, certifique-se de não aguardar algum bloqueio ou outra operação de outra linha de execução. Normalmente, a linha de execução de IU não precisa esperar por 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) tornaram-se um problema menos significativo desde que o ART foi introduzido 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á problemas em alocar em resposta a um evento raro que não acontece muitas vezes por segundo, como um usuário clicando 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 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 quando puder, especialmente em repetições apertadas, provavelmente não terá problemas.

Figura 5: mostra uma coleta de lixo 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. Observe que quantidades significativas de alocação podem significar mais recursos de CPU gastos em GC, como mostrado na figura 5.