Dicas e ferramentas para otimizar aplicativos Android
Publicados: 2022-03-11Os dispositivos Android têm muitos núcleos, então escrever aplicativos suaves é uma tarefa simples para qualquer um, certo? Errado. Como tudo no Android pode ser feito de várias maneiras diferentes, escolher a melhor opção pode ser difícil. Se você deseja escolher o método mais eficiente, precisa saber o que está acontecendo nos bastidores. Felizmente, você não precisa confiar em seus sentimentos ou olfato, pois há muitas ferramentas por aí que podem ajudá-lo a encontrar gargalos medindo e descrevendo o que está acontecendo. Aplicativos bem otimizados e suaves melhoram muito a experiência do usuário e também consomem menos bateria.
Vamos ver alguns números primeiro para considerar a importância da otimização. De acordo com uma postagem do Nimbledroid, 86% dos usuários (inclusive eu) desinstalaram aplicativos depois de usá-los apenas uma vez devido ao baixo desempenho. Se você estiver carregando algum conteúdo, terá menos de 11 segundos para mostrá-lo ao usuário. Apenas cada terceiro usuário lhe dará mais tempo. Você também pode receber muitas críticas ruins no Google Play por causa disso.
A primeira coisa que todo usuário percebe repetidamente é o tempo de inicialização do aplicativo. De acordo com outro post do Nimbledroid, dos 100 principais aplicativos, 40 iniciam em menos de 2 segundos e 70 iniciam em menos de 3 segundos. Portanto, se possível, você geralmente deve exibir algum conteúdo o mais rápido possível e atrasar um pouco as verificações e atualizações de antecedentes.
Lembre-se sempre, a otimização prematura é a raiz de todo mal. Você também não deve perder muito tempo com micro otimização. Você verá o maior benefício de otimizar o código que é executado com frequência. Por exemplo, isso inclui a função onDraw()
, que executa cada quadro, idealmente 60 vezes por segundo. Desenhar é a operação mais lenta que existe, então tente redesenhar apenas o que você precisa. Mais sobre isso virá mais tarde.
Dicas de desempenho
Chega de teoria, aqui está uma lista de algumas das coisas que você deve considerar se o desempenho for importante para você.
1. String vs. StringBuilder
Digamos que você tenha uma String e, por algum motivo, queira anexar mais Strings a ela 10 mil vezes. O código poderia ser algo assim.
String string = "hello"; for (int i = 0; i < 10000; i++) { string += " world"; }
Você pode ver nos monitores do Android Studio como algumas concatenações de String podem ser ineficientes. Há toneladas de coletas de lixo (GC) acontecendo.
Esta operação leva cerca de 8 segundos no meu dispositivo bastante bom, que possui o Android 5.1.1. A maneira mais eficiente de atingir o mesmo objetivo é usar um StringBuilder, como este.
StringBuilder sb = new StringBuilder("hello"); for (int i = 0; i < 10000; i++) { sb.append(" world"); } String string = sb.toString();
No mesmo dispositivo isso acontece quase instantaneamente, em menos de 5ms. As visualizações de CPU e Memória são quase totalmente planas, então você pode imaginar o quão grande é essa melhoria. Observe, porém, que para conseguir essa diferença, tivemos que anexar 10 mil Strings, o que você provavelmente não faz com frequência. Portanto, caso você esteja adicionando apenas algumas Strings uma vez, não verá nenhuma melhoria. Aliás, se você fizer:
String string = "hello" + " world";
Ele é convertido internamente em um StringBuilder, então funcionará bem.
Você pode estar se perguntando, por que a concatenação de Strings é tão lenta? Isso é causado pelo fato de Strings serem imutáveis, portanto, uma vez criadas, elas não podem ser alteradas. Mesmo que você pense que está alterando o valor de uma String, na verdade você está criando uma nova String com o novo valor. Em um exemplo como:
String myString = "hello"; myString += " world";
O que você obterá na memória não é 1 String “hello world”, mas na verdade 2 Strings. A String myString conterá “hello world”, como seria de esperar. No entanto, a String original com o valor “hello” ainda está viva, sem nenhuma referência a ela, esperando para ser coletada como lixo. Esta é também a razão pela qual você deve armazenar senhas em uma matriz de caracteres em vez de uma String. Se você armazenar uma senha como uma String, ela permanecerá na memória em formato legível por humanos até o próximo GC por um período de tempo imprevisível. De volta à imutabilidade descrita acima, a String permanecerá na memória mesmo que você atribua outro valor a ela após usá-la. Se você, no entanto, esvaziar o array char depois de usar a senha, ele desaparecerá de todos os lugares.
2. Escolhendo o tipo de dados correto
Antes de começar a escrever o código, você deve decidir quais tipos de dados usará para sua coleção. Por exemplo, você deve usar um Vector
ou um ArrayList
? Bem, depende do seu caso de uso. Se você precisar de uma coleção thread-safe, que permitirá que apenas um thread trabalhe com ela, você deve escolher um Vector
, pois ele é sincronizado. Em outros casos, você provavelmente deve ficar com um ArrayList
, a menos que você realmente tenha um motivo específico para usar vetores.
Que tal o caso em que você quer uma coleção com objetos únicos? Bem, você provavelmente deveria escolher um Set
. Eles não podem conter duplicatas por design, então você não terá que cuidar disso sozinho. Existem vários tipos de conjuntos, então escolha um que se adeque ao seu caso de uso. Para um grupo simples de itens exclusivos, você pode usar um HashSet
. Se você deseja preservar a ordem dos itens em que foram inseridos, escolha um LinkedHashSet
. Um TreeSet
classifica os itens automaticamente, portanto, você não precisará chamar nenhum método de classificação nele. Ele também deve classificar os itens de forma eficiente, sem que você precise pensar em algoritmos de classificação.
— As 5 Regras de Programação de Rob Pike
Classificar inteiros ou strings é bastante simples. No entanto, e se você quiser classificar uma classe por alguma propriedade? Digamos que você está escrevendo uma lista de refeições que você come e armazena seus nomes e datas. Como você classificaria as refeições por timestamp do menor para o maior? Felizmente, é bem simples. Basta implementar a interface Comparable
na classe Meal
e substituir a função compareTo()
. Para classificar as refeições do menor para o maior, poderíamos escrever algo assim.
@Override public int compareTo(Object object) { Meal meal = (Meal) object; if (this.timestamp < meal.getTimestamp()) { return -1; } else if (this.timestamp > meal.getTimestamp()) { return 1; } return 0; }
3. Atualizações de localização
Existem muitos aplicativos por aí que coletam a localização do usuário. Você deve usar a API do Google Location Services para essa finalidade, que contém muitas funções úteis. Há um artigo separado sobre como usá-lo, então não vou repeti-lo.
Gostaria apenas de enfatizar alguns pontos importantes do ponto de vista do desempenho.
Em primeiro lugar, use apenas a localização mais precisa que você precisa. Por exemplo, se você estiver fazendo alguma previsão do tempo, não precisará da localização mais precisa. Obter apenas uma área muito difícil com base na rede é mais rápido e mais eficiente em termos de bateria. Você pode conseguir isso definindo a prioridade como LocationRequest.PRIORITY_LOW_POWER
.
Você também pode usar uma função de LocationRequest
chamada setSmallestDisplacement()
. Definir isso em metros fará com que seu aplicativo não seja notificado sobre a mudança de local se for menor que o valor fornecido. Por exemplo, se você tiver um mapa com restaurantes próximos ao seu redor e definir o menor deslocamento para 20 metros, o aplicativo não fará solicitações de verificação de restaurantes se o usuário estiver apenas andando em uma sala. Os pedidos seriam inúteis, pois não haveria nenhum novo restaurante próximo de qualquer maneira.
A segunda regra é solicitar atualizações de local apenas com a frequência necessária. Isso é bastante autoexplicativo. Se você está realmente construindo esse aplicativo de previsão do tempo, não precisa solicitar a localização a cada poucos segundos, pois provavelmente não tem previsões tão precisas (entre em contato comigo se tiver). Você pode usar a função setInterval()
para definir o intervalo necessário no qual o dispositivo atualizará seu aplicativo sobre o local. Se vários aplicativos continuarem solicitando a localização do usuário, todos os aplicativos serão notificados a cada nova atualização de local, mesmo se você tiver um conjunto setInterval()
mais alto. Para evitar que seu aplicativo seja notificado com muita frequência, sempre defina um intervalo de atualização mais rápido com setFastestInterval()
.
E, finalmente, a terceira regra é solicitar atualizações de localização apenas se você precisar delas. Se você estiver exibindo alguns objetos próximos no mapa a cada x segundos e o aplicativo ficar em segundo plano, não será necessário saber a nova localização. Não há motivo para atualizar o mapa se o usuário não puder vê-lo de qualquer maneira. Certifique-se de parar de ouvir atualizações de localização quando apropriado, de preferência em onPause()
. Você pode então retomar as atualizações em onResume()
.
4. Solicitações de rede
Há uma grande chance de que seu aplicativo esteja usando a Internet para fazer download ou upload de dados. Se for, você tem vários motivos para prestar atenção ao tratamento de solicitações de rede. Um deles são os dados móveis, que são muito limitados a muitas pessoas e você não deve desperdiçá-los.
A segunda é a bateria. Ambas as redes WiFi e móveis podem consumir bastante se forem usadas demais. Digamos que você queira baixar 1 kb. Para fazer uma solicitação de rede, você precisa ativar o rádio celular ou WiFi e, em seguida, pode baixar seus dados. No entanto, o rádio não adormecerá imediatamente após a operação. Ele permanecerá em um estado bastante ativo por cerca de 20 a 40 segundos a mais, dependendo do seu dispositivo e operadora.
Então, o que você pode fazer sobre isso? Lote. Para evitar acordar o rádio a cada dois segundos, faça uma pré-busca de coisas que o usuário possa precisar nos próximos minutos. A maneira correta de fazer o batching é altamente dinâmica dependendo do seu aplicativo, mas se for possível, você deve baixar os dados que o usuário pode precisar nos próximos 3-4 minutos. Também é possível editar os parâmetros do lote com base no tipo de internet do usuário ou no estado de cobrança. Por exemplo, se o usuário estiver usando Wi-Fi durante o carregamento, você poderá pré-buscar muito mais dados do que se o usuário estiver na Internet móvel com bateria fraca. Levar todas essas variáveis em consideração pode ser uma coisa difícil, que poucas pessoas fariam. Felizmente, porém, existe o GCM Network Manager para o resgate!
O GCM Network Manager é uma classe realmente útil com muitos atributos personalizáveis. Você pode agendar facilmente tarefas repetidas e pontuais. Ao repetir tarefas, você pode definir o intervalo de repetição mais baixo e mais alto. Isso permitirá agrupar não apenas suas solicitações, mas também solicitações de outros aplicativos. O rádio precisa ser ativado apenas uma vez por determinado período e, enquanto estiver ativo, todos os aplicativos na fila baixam e carregam o que deveriam. Este gerente também está ciente do tipo de rede do dispositivo e do estado de carregamento, para que você possa ajustar de acordo. Você pode encontrar mais detalhes e amostras neste artigo, peço que você verifique. Uma tarefa de exemplo se parece com isso:
Task task = new OneoffTask.Builder() .setService(CustomService.class) .setExecutionWindow(0, 30) .setTag(LogService.TAG_TASK_ONEOFF_LOG) .setUpdateCurrent(false) .setRequiredNetwork(Task.NETWORK_STATE_CONNECTED) .setRequiresCharging(false) .build();
A propósito, desde o Android 3.0, se você fizer uma solicitação de rede no thread principal, obterá um NetworkOnMainThreadException
. Isso definitivamente irá avisá-lo para não fazer isso novamente.
5. Reflexão
Reflexão é a capacidade de classes e objetos examinarem seus próprios construtores, campos, métodos e assim por diante. Ele é usado geralmente para compatibilidade com versões anteriores, para verificar se um determinado método está disponível para uma versão específica do sistema operacional. Se você precisar usar a reflexão para esse propósito, certifique-se de armazenar a resposta em cache, pois usar a reflexão é bastante lento. Algumas bibliotecas amplamente usadas também usam o Reflection, como o Roboguice para injeção de dependência. Essa é a razão pela qual você deve preferir o Dagger 2. Para mais detalhes sobre reflexão, você pode conferir um post separado.
6. Autoboxing
Autoboxing e unboxing são processos de conversão de um tipo primitivo em um tipo de objeto, ou vice-versa. Na prática, significa converter um int em um Integer. Para conseguir isso, o compilador usa a função Integer.valueOf()
internamente. A conversão não é apenas lenta, os objetos também consomem muito mais memória do que seus equivalentes primitivos. Vejamos alguns códigos.
Integer total = 0; for (int i = 0; i < 1000000; i++) { total += i; }
Embora isso leve 500ms em média, reescrevê-lo para evitar o autoboxing irá acelerá-lo drasticamente.
int total = 0; for (int i = 0; i < 1000000; i++) { total += i; }
Esta solução é executada em cerca de 2ms, o que é 25 vezes mais rápido. Se você não acredita em mim, teste. Os números serão obviamente diferentes por dispositivo, mas ainda deve ser muito mais rápido. E também é um passo muito simples para otimizar.
Ok, você provavelmente não cria uma variável do tipo Integer como esta com frequência. Mas e os casos em que é mais difícil evitar? Como em um mapa, onde você tem que usar objetos, como Map<Integer, Integer>
? Veja a solução que muitas pessoas usam.
Map<Integer, Integer> myMap = new HashMap<>(); for (int i = 0; i < 100000; i++) { myMap.put(i, random.nextInt()); }
Inserir 100k ints aleatórios no mapa leva cerca de 250ms para ser executado. Agora veja a solução com SparseIntArray.
SparseIntArray myArray = new SparseIntArray(); for (int i = 0; i < 100000; i++) { myArray.put(i, random.nextInt()); }
Isso leva muito menos, cerca de 50ms. É também um dos métodos mais fáceis para melhorar o desempenho, pois nada complicado precisa ser feito e o código também permanece legível. Enquanto a execução de um aplicativo claro com a primeira solução consumia 13 MB da minha memória, o uso de ints primitivos consumia menos de 7 MB, portanto, apenas a metade.
SparseIntArray é apenas uma das coleções legais que podem ajudá-lo a evitar o autoboxing. Um mapa como Map<Integer, Long>
pode ser substituído por SparseLongArray
, pois o valor do mapa é do tipo Long
. Se você olhar o código fonte de SparseLongArray
, verá algo bem interessante. Sob o capô, é basicamente apenas um par de matrizes. Você também pode usar um SparseBooleanArray
de forma semelhante.
Se você ler o código-fonte, deve ter notado uma nota dizendo que SparseIntArray
pode ser mais lento que HashMap
. Eu tenho experimentado muito, mas para mim SparseIntArray
sempre foi melhor em termos de memória e desempenho. Acho que ainda depende de você escolher, experimentar seus casos de uso e ver qual se adapta mais a você. Definitivamente, tenha os SparseArrays
em sua cabeça ao usar mapas.
7. No Sorteio
Como eu disse acima, quando você estiver otimizando o desempenho, provavelmente verá o maior benefício na otimização do código que é executado com frequência. Uma das funções que roda muito é onDraw()
. Pode não surpreendê-lo que seja responsável por desenhar vistas na tela. Como os dispositivos costumam rodar a 60 fps, a função é executada 60 vezes por segundo. Cada quadro tem 16 ms para ser totalmente manipulado, incluindo sua preparação e desenho, então você deve realmente evitar funções lentas. Apenas o thread principal pode desenhar na tela, então você deve evitar fazer operações caras nele. Se você congelar o thread principal por vários segundos, poderá obter a infame caixa de diálogo Application Not Responding (ANR). Para redimensionar imagens, trabalho de banco de dados, etc., use um thread em segundo plano.
Já vi algumas pessoas tentando encurtar seu código, achando que assim seria mais eficiente. Esse definitivamente não é o caminho a seguir, pois código mais curto não significa código mais rápido. Sob nenhuma circunstância você deve medir a qualidade do código pelo número de linhas.

Uma das coisas que você deve evitar no onDraw()
é alocar objetos como Paint. Prepare tudo no construtor, para que esteja pronto ao desenhar. Mesmo se você tiver onDraw()
otimizado, você deve chamá-lo apenas com a frequência necessária. O que é melhor do que chamar uma função otimizada? Bem, não chamando nenhuma função. Caso você queira desenhar texto, existe uma função auxiliar bem legal chamada drawText()
, onde você pode especificar coisas como o texto, as coordenadas e a cor do texto.
8. Suportes de visualização
Você provavelmente conhece este, mas eu não posso ignorá-lo. O padrão de design Viewholder é uma maneira de tornar as listas de rolagem mais suaves. É um tipo de cache de visualização, que pode reduzir seriamente as chamadas para findViewById()
e inflar visualizações armazenando-as. Pode parecer algo assim.
static class ViewHolder { TextView title; TextView text; public ViewHolder(View view) { title = (TextView) view.findViewById(R.id.title); text = (TextView) view.findViewById(R.id.text); } }
Em seguida, dentro da função getView()
do seu adaptador, você pode verificar se tem uma visualização utilizável. Se não, você cria um.
ViewHolder viewHolder; if (convertView == null) { convertView = inflater.inflate(R.layout.list_item, viewGroup, false); viewHolder = new ViewHolder(convertView); convertView.setTag(viewHolder); } else { viewHolder = (ViewHolder) convertView.getTag(); } viewHolder.title.setText("Hello World");
Você pode encontrar muitas informações úteis sobre esse padrão na Internet. Ele também pode ser usado nos casos em que sua exibição de lista tem vários tipos diferentes de elementos, como alguns cabeçalhos de seção.
9. Redimensionando Imagens
Provavelmente, seu aplicativo conterá algumas imagens. Caso você esteja baixando alguns JPGs da web, eles podem ter resoluções realmente enormes. No entanto, os dispositivos em que serão exibidos serão muito menores. Mesmo que você tire uma foto com a câmera do seu dispositivo, ela precisa ser reduzida antes de ser exibida, pois a resolução da foto é muito maior que a resolução da tela. Redimensionar imagens antes de exibi-las é uma coisa crucial. Se você tentar exibi-los em resoluções completas, ficará sem memória rapidamente. Há muito escrito sobre a exibição de bitmaps de forma eficiente nos documentos do Android, vou tentar resumir.
Então você tem um bitmap, mas você não sabe nada sobre isso. Há um sinalizador útil de Bitmaps chamado inJustDecodeBounds ao seu serviço, que permite descobrir a resolução do bitmap. Vamos supor que seu bitmap seja 1024x768, e o ImageView usado para exibi-lo seja apenas 400x300. Você deve continuar dividindo a resolução do bitmap por 2 até que ainda seja maior que o ImageView fornecido. Se você fizer isso, ele reduzirá a resolução do bitmap por um fator de 2, fornecendo um bitmap de 512x384. O bitmap reduzido usa 4x menos memória, o que o ajudará muito a evitar o famoso erro OutOfMemory.
Agora que você sabe como fazer isso, você não deve fazê-lo. … Pelo menos, não se seu aplicativo depende muito de imagens, e não são apenas 1-2 imagens. Definitivamente evite coisas como redimensionar e reciclar imagens manualmente, use algumas bibliotecas de terceiros para isso. Os mais populares são Picasso by Square, Universal Image Loader, Fresco by Facebook ou meu favorito, Glide. Há uma enorme comunidade ativa de desenvolvedores em torno dele, então você também pode encontrar muitas pessoas úteis na seção de problemas no GitHub.
10. Modo Estrito
O Strict Mode é uma ferramenta de desenvolvedor bastante útil que muitas pessoas não conhecem. Geralmente é usado para detectar solicitações de rede ou acessos ao disco do encadeamento principal. Você pode definir quais problemas o Strict Mode deve procurar e qual penalidade deve acionar. Uma amostra do google se parece com isso:
public void onCreate() { if (DEVELOPER_MODE) { StrictMode.setThreadPolicy(new StrictMode.ThreadPolicy.Builder() .detectDiskReads() .detectDiskWrites() .detectNetwork() .penaltyLog() .build()); StrictMode.setVmPolicy(new StrictMode.VmPolicy.Builder() .detectLeakedSqlLiteObjects() .detectLeakedClosableObjects() .penaltyLog() .penaltyDeath() .build()); } super.onCreate(); }
Se você deseja detectar todos os problemas que o Strict Mode pode encontrar, também pode usar detectAll()
. Tal como acontece com muitas dicas de desempenho, você não deve tentar corrigir cegamente todos os relatórios do Strict Mode. Apenas investigue e, se tiver certeza de que não é um problema, deixe-o em paz. Certifique-se também de usar o Strict Mode apenas para depuração e sempre desative-o nas compilações de produção.
Desempenho de depuração: a maneira profissional
Vamos agora ver algumas ferramentas que podem te ajudar a encontrar gargalos, ou pelo menos mostrar que algo está errado.
1. Monitorar Android
Esta é uma ferramenta incorporada ao Android Studio. Por padrão, você pode encontrar o Android Monitor no canto inferior esquerdo e alternar entre 2 guias. Logcat e Monitores. A seção Monitores contém 4 gráficos diferentes. Rede, CPU, GPU e Memória. Eles são bastante auto-explicativos, então vou passar rapidamente por eles. Aqui está uma captura de tela dos gráficos obtidos ao analisar alguns JSON à medida que é baixado.
A parte de rede mostra o tráfego de entrada e saída em KB/s. A parte da CPU exibe o uso da CPU em porcentagem. O monitor da GPU exibe quanto tempo leva para renderizar os quadros de uma janela de interface do usuário. Este é o monitor mais detalhado desses 4, então se você quiser mais detalhes sobre ele, leia isto.
Por fim, temos o monitor de memória, que você provavelmente usará mais. Por padrão, mostra a quantidade atual de memória livre e alocada. Você também pode forçar uma coleta de lixo com ele, para testar se a quantidade de memória usada diminui. Ele possui um recurso útil chamado Dump Java Heap, que criará um arquivo HPROF que pode ser aberto com o Visualizador e Analisador HPROF. Isso permitirá que você veja quantos objetos você alocou, quanta memória é ocupada pelo quê e talvez quais objetos estão causando vazamentos de memória. Aprender a usar este analisador não é a tarefa mais simples que existe, mas vale a pena. A próxima coisa que você pode fazer com o Monitor de Memória é fazer um rastreamento de alocação cronometrado, que você pode iniciar e parar como desejar. Pode ser útil em muitos casos, por exemplo, ao rolar ou girar o dispositivo.
2. Sobrecarga da GPU
Esta é uma ferramenta auxiliar simples, que você pode ativar nas Opções do desenvolvedor depois de ativar o modo de desenvolvedor. Selecione Depurar overdraw da GPU, “Mostrar áreas de overdraw” e sua tela ficará com algumas cores estranhas. Está tudo bem, é isso que deve acontecer. As cores significam quantas vezes uma determinada área foi overdrawn. True color significa que não houve overdraw, é isso que você deve buscar. Azul significa um overdraw, verde significa dois, rosa três, vermelho quatro.
Embora ver cores verdadeiras seja o melhor, você sempre verá alguns overdraws, especialmente em torno de textos, gavetas de navegação, diálogos e muito mais. Portanto, não tente se livrar dele totalmente. Se o seu aplicativo for azulado ou esverdeado, provavelmente não há problema. No entanto, se você vir muito vermelho em algumas telas simples, deve investigar o que está acontecendo. Pode haver muitos fragmentos empilhados uns sobre os outros, se você continuar adicionando-os em vez de substituir. Como mencionei acima, o desenho é a parte mais lenta dos aplicativos, então não faz sentido desenhar algo se houver mais de 3 camadas desenhadas nele. Sinta-se à vontade para conferir seus aplicativos favoritos com ele. Você verá que até mesmo aplicativos com mais de um bilhão de downloads têm áreas vermelhas, então vá com calma quando estiver tentando otimizar.
3. Renderização de GPU
Esta é outra ferramenta das opções do desenvolvedor, chamada de renderização de GPU de perfil. Ao selecioná-lo, escolha “Na tela como barras”. Você notará algumas barras coloridas aparecendo na tela. Como cada aplicativo tem barras separadas, estranhamente a barra de status tem suas próprias, e caso você tenha botões de navegação de software, eles também têm suas próprias barras. De qualquer forma, as barras são atualizadas à medida que você interage com a tela.
As barras consistem em 3-4 cores e, de acordo com os documentos do Android, seu tamanho realmente importa. Quanto menor, melhor. Na parte inferior você tem o azul, que representa o tempo usado para criar e atualizar as listas de exibição da View. Se esta parte for muito alta, significa que há muito desenho de vista personalizado, ou muito trabalho feito nas funções onDraw()
. Se você tiver o Android 4.0+, verá uma barra roxa acima da azul. Isso representa o tempo gasto na transferência de recursos para o thread de renderização. Em seguida, vem a parte vermelha, que representa o tempo gasto pelo renderizador 2D do Android emitindo comandos para OpenGL para desenhar e redesenhar listas de exibição. No topo está a barra laranja, que representa o tempo que a CPU está esperando para que a GPU termine seu trabalho. Se for muito alto, o aplicativo está fazendo muito trabalho na GPU.
Se você for bom o suficiente, há mais uma cor acima do laranja. É uma linha verde representando o limite de 16 ms. Como seu objetivo deve ser rodar seu aplicativo a 60 fps, você tem 16 ms para desenhar cada quadro. Se você não fizer isso, alguns quadros podem ser ignorados, o aplicativo pode ficar irregular e o usuário definitivamente perceberia. Preste atenção especial às animações e rolagem, é aí que a suavidade é mais importante. Mesmo que você possa detectar alguns quadros ignorados com essa ferramenta, ela não o ajudará a descobrir exatamente onde está o problema.
4. Visualizador de hierarquia
Esta é uma das minhas ferramentas favoritas, pois é realmente poderosa. Você pode iniciá-lo no Android Studio através de Ferramentas -> Android -> Monitor de dispositivo Android, ou também na pasta sdk/tools como “monitor”. Você também pode encontrar um executável do hierarachyviewer autônomo, mas como está obsoleto, você deve abrir o monitor. No entanto, você abre o Android Device Monitor, mude para a perspectiva do Hierarchy Viewer. Se você não vir nenhum aplicativo em execução atribuído ao seu dispositivo, há algumas coisas que você pode fazer para corrigi-lo. Tente também verificar este tópico de problemas, existem pessoas com todos os tipos de problemas e todos os tipos de soluções. Algo deve funcionar para você também.
Com o Hierarchy Viewer, você pode obter uma visão geral realmente organizada de suas hierarquias de visualização (obviamente). Se você vir todos os layouts em um XML separado, poderá identificar facilmente visualizações inúteis. No entanto, se você continuar combinando os layouts, pode facilmente ficar confuso. Uma ferramenta como essa facilita a identificação, por exemplo, de algum RelativeLayout, que tem apenas 1 filho, outro RelativeLayout. Isso torna um deles removível.
Evite chamar requestLayout()
, pois isso causa a travessia de toda a hierarquia de visualizações, para descobrir o tamanho de cada visualização. Se houver algum conflito com as medidas, a hierarquia pode ser percorrida várias vezes, o que se acontecer durante alguma animação, certamente fará com que alguns quadros sejam ignorados. Se você quiser saber mais sobre como o Android desenha seus pontos de vista, você pode ler isto. Vamos dar uma olhada em uma visão como visto no Hierarchy Viewer.
O canto superior direito contém um botão para maximizar a visualização da visualização específica em uma janela independente. Abaixo dele, você também pode ver a visualização real da visualização no aplicativo. O próximo item é um número, que representa quantos filhos a determinada visão tem, incluindo a própria visão. Se você selecionar um nó (de preferência o raiz) e pressionar “Obter tempos de layout” (3 círculos coloridos), você terá mais 3 valores preenchidos, juntamente com círculos coloridos aparecendo rotulados como medida, layout e desenho. Pode não ser chocante que a fase de medição represente o tempo necessário para medir a exibição fornecida. A fase de layout é sobre o tempo de renderização, enquanto o desenho é a operação de desenho real. Esses valores e cores são relativos entre si. Verde significa que a visualização é renderizada nos 50% superiores de todas as visualizações na árvore. Amarelo significa renderização nos 50% mais lentos de todas as visualizações na árvore, vermelho significa que a visualização fornecida é uma das mais lentas. Como esses valores são relativos, sempre haverá valores vermelhos. Você simplesmente não pode evitá-los.
Sob os valores, você tem o nome da classe, como “TextView”, um ID interno da visualização do objeto e o android:id da visualização, que você define nos arquivos XML. Recomendo que você crie o hábito de adicionar IDs a todas as visualizações, mesmo que não faça referência a eles no código. Isso tornará a identificação das visualizações no Hierarchy Viewer muito simples e, caso você tenha testes automatizados em seu projeto, também tornará o direcionamento dos elementos muito mais rápido. Isso economizará algum tempo para você e seus colegas escrevê-los. Adicionar IDs a elementos adicionados em arquivos XML é bastante simples. Mas e os elementos adicionados dinamicamente? Bem, acaba por ser muito simples também. Basta criar um arquivo ids.xml dentro de sua pasta de valores e digitar os campos obrigatórios. Pode parecer assim:
<resources> <item name="item_title" type="id"/> <item name="item_body" type="id"/> </resources>
Em seguida, no código, você pode usar setId(R.id.item_title)
. Não poderia ser mais simples.
Há mais algumas coisas para prestar atenção ao otimizar a interface do usuário. Em geral, você deve evitar hierarquias profundas enquanto prefere as rasas, talvez amplas. Não use layouts que você não precisa. Por exemplo, você provavelmente pode substituir um grupo de LinearLayouts
aninhados por um RelativeLayout
ou um TableLayout
. Sinta-se à vontade para experimentar diferentes layouts, não use sempre LinearLayout
e RelativeLayout
. Tente também criar algumas visualizações personalizadas quando necessário, pois isso pode melhorar significativamente o desempenho se for bem feito. Por exemplo, você sabia que o Instagram não usa TextViews para exibir comentários?
Você pode encontrar mais informações sobre o Hierarchy Viewer no site Android Developers com descrições de diferentes painéis, usando a ferramenta Pixel Perfect, etc. o botão “Capturar as camadas da janela”. Cada visualização estará em uma camada separada, então é muito simples ocultá-la ou alterá-la no Photoshop ou no GIMP. Ah, esse é outro motivo para adicionar um ID a todas as visualizações possíveis. Isso fará com que as camadas tenham nomes que realmente façam sentido.
Você encontrará muito mais ferramentas de depuração nas opções do desenvolvedor, então aconselho a ativá-las e ver o que elas estão fazendo. O que poderia dar errado?
O site de desenvolvedores do Android contém um conjunto de práticas recomendadas para desempenho. Eles cobrem muitas áreas diferentes, incluindo gerenciamento de memória, sobre o qual eu realmente não falei. Eu ignorei silenciosamente, porque lidar com memória e rastrear vazamentos de memória é uma história totalmente separada. Usar uma biblioteca de terceiros para exibir imagens com eficiência ajudará muito, mas se você ainda tiver problemas de memória, confira Leak canary made by Square ou leia isto.
Empacotando
Então, essa foi a boa notícia. A má notícia é que otimizar aplicativos Android é muito mais complicado. Há muitas maneiras de fazer tudo, então você deve estar familiarizado com os prós e contras delas. Geralmente não há nenhuma solução de bala de prata que tenha apenas benefícios. Somente entendendo o que está acontecendo nos bastidores você poderá escolher a solução que é melhor para você. Só porque seu desenvolvedor favorito diz que algo é bom, isso não significa necessariamente que é a melhor solução para você. Há muito mais áreas para discutir e mais ferramentas de criação de perfil que são mais avançadas, então podemos abordá-las na próxima vez.
Certifique-se de aprender com os principais desenvolvedores e as principais empresas. Você pode encontrar algumas centenas de blogs de engenharia neste link. Obviamente, não são apenas coisas relacionadas ao Android, portanto, se você estiver interessado apenas no Android, precisará filtrar o blog específico. Eu recomendo os blogs do Facebook e Instagram. Mesmo que a interface do Instagram no Android seja questionável, o blog de engenharia deles tem alguns artigos muito legais. Para mim é incrível que seja tão fácil ver como as coisas são feitas em empresas que lidam com centenas de milhões de usuários diariamente, então não ler seus blogs parece loucura. O mundo está mudando muito rápido, então se você não estiver constantemente tentando melhorar, aprender com os outros e usar novas ferramentas, ficará para trás. Como disse Mark Twain, uma pessoa que não lê não tem vantagem sobre aquela que não sabe ler.