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 (confira 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 do app, o Android faz um monitoramento automático em busca de instabilidades 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.
Caso seu app esteja 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, é possível 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 suporte a recursos de depuração. Por isso, verifique se você está analisando algo semelhante ao que o usuário verá.
- 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. Caso seu app use esses componentes, é recomendável percorrer essas partes dele. - À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 que o problema fique mais fácil de identificar.
Depois de encontrar casos de uso que produzem 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 haverá 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:
- 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 ver mais informações, consulte inspecionar frames e alertas da IU.
- 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.
- 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 rastreamento de método amostrado ou instrumentado. Geralmente, os rastreamentos de método não são bons para identificar instabilidade, porque geram falsos positivos devido a 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 seu app que estão demorando mais. Depois de identificar esses métodos, adicione marcadores de rastro e execute novamente o Systrace para ver se esses métodos estão causando instabilidade.
Para mais informações, consulte Noções básicas sobre o 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 ver mais informações, consulte o Codelab de testes de desempenho automatizado.
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 à 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 rastros 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
Caso você perceba 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 um 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 RecyclerView
s,
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.RecycledViewPool
s 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 mais recentes.
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 as 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ê 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 biblioteca Data Binding.
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 não tomar cuidado. 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á 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 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 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. Acesse Otimizar hierarquias de layout para consultar um exemplo específico. Você pode tentar converter para o ConstraintLayout, que fornece recursos parecidos e sem as desvantagens para o desempenho.
Desempenho do layout: frequência
Espera-se que o layout aconteça quando conteúdo novo 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 é executada no 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 é porque um bitmap está sendo 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étodo com o Android CPU Profiler para conferir 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 também pode 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.
Caso esteja 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
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 antialias. 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 Texture upload(id) width x height. 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 muito tempo em um frame fazendo upload de um
bitmap grande. 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 (confira 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 que não está aguardando 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 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á 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.