Práticas recomendadas para otimização de memória no Android

androidHead
Práticas recomendadas para otimização de memória no Android

Postado por HC.COM

Para aqueles que acompanham a evolução do Android, é evidente que o futuro do ecossistema baseado em Android vai muito além de apenas telefones e tablets. O sistema operacional já está entrando em uma série de outros dispositivos inteligentes, como o Google Glass, por exemplo, em um movimento em direção ao que está sendo chamado de "a internet das coisas" ou IoT. Desenvolver um novo produto OEM baseado no Android como um sistema operacional integrado faz muito sentido em comparação a, digamos, usar apenas o Linux, conforme abordamos antes. No entanto, fazer com que o Android funcione efetivamente em diversas plataformas é bastante desafiador. Enquanto os telefones e tablets estão ficando muito poderosos (com processadores quad core e 2 + GB de RAM se tornando o padrão de fato), este certamente não é o caso com muitos outros dispositivos IoT onde, devido às margens de custo, a necessidade do dia ainda é menor CPUs alimentadas e menos RAM (já que a RAM é uma parte cara de qualquer BOM de dispositivo). Embora existam muitos mecanismos para reduzir a pegada do Android e reduzir a sobrecarga de memória (como o modo Android sem cabeça, configurações do Android com pouca memória, etc.), é importante garantir que o código do aplicativo também use efetivamente a memória disponível. Este artigo aborda as práticas recomendadas para uso de memória.
Android & Ram
Existem dois tipos de memórias quando se trata de Android: Clean RAM e Dirty RAM.
Memória RAM limpa
Ao contrário dos PCs, o Android não oferece espaço de troca para memória, mas usa paginação e mapeamento de memória. Quaisquer arquivos ou recursos presentes no disco, como código, são mantidos em páginas com mmap. O Android sabe que essas páginas podem ser recuperadas do disco, portanto, elas podem ser paginadas se o sistema precisar de memória em outro lugar.
Memória RAM suja
RAM suja é a memória que não pode ser removida. Pode ser caro, especialmente quando executado em um processo em segundo plano. A maior parte da memória em um aplicativo em execução é memória suja e é com ela que você deve tomar cuidado.
Para otimizar o uso de memória, o Android tenta compartilhar alguns recursos de estrutura ou classes comuns na memória entre os processos. Portanto, sempre que um dispositivo é inicializado, um processo chamado zigoto carrega o código de estrutura comum. Cada novo processo de aplicativo é então separado do processo zigoto para que possa acessar todas as páginas de RAM compartilhadas carregadas por ele. Ao investigar o uso de RAM de um aplicativo, é importante manter o uso de memória compartilhada em mente, uma vez que devemos olhar apenas para a memória suja privada que está sendo usada por nosso aplicativo. Isso é refletido por USS (tamanho de conjunto único) e PSS (tamanho de conjunto proporcional) em 'meminfo.'
Outra coisa importante a se ter em mente ao investigar oportunidades de otimização de memória é que o Android divide os processos do aplicativo com base nos processos em execução e em cache. Um processo em execução é o aplicativo principal em execução no dispositivo ou um aplicativo com um serviço sendo executado ativamente em segundo plano. Todos os outros aplicativos iniciados irão para a lista de processos em cache para permitir uma alternância mais fácil e rápida entre os aplicativos. Por exemplo, se um aplicativo é iniciado e, em seguida, o usuário pressiona o botão ‘Home’, o processo desse aplicativo será adicionado à lista de processos em cache, ou seja, se não tiver um serviço em execução. Esteja ciente de que o sistema eliminará um ou mais processos em cache se precisar de mais memória para qualquer processo em execução. Os processos em cache podem ser eliminados na ordem LRU (usado menos recentemente). No entanto, também existem algumas outras opções, como eliminar qualquer processo em cache que dará o ganho máximo de memória para o sistema.
Cada aplicativo no Android tem um limite máximo de tamanho de heap (varia para cada dispositivo). Você pode verificar a API getMemoryClass () do serviço ActivityManager, ela informará o tamanho máximo de heap disponível para qualquer aplicativo em um dispositivo. A maioria dos dispositivos com Android 2.3 ou posterior retornará este tamanho como 24 MB ou superior. Por exemplo, o tamanho em um Galaxy S3 é 64 MB, enquanto em um dispositivo Nexus5, é 192 MB. O Android sempre iniciará um processo de aplicativo com um tamanho de heap médio e o aumentará até o limite máximo nesse dispositivo para um aplicativo. Se um aplicativo atingir o tamanho máximo de heap e precisar alocar mais memória, o sistema lançará um OutOfMemoryError.
Otimização de memória, práticas recomendadas para Android
Então, o que você pode fazer para evitar que o sistema fique sem memória? Continue lendo para obter algumas diretrizes gerais para melhorar o uso de memória e o desempenho geral do Android.
Evite criar objetos desnecessários
A regra básica aqui é que a coleta de lixo não é gratuita. Quanto mais objetos um aplicativo alocar, mais frequentemente o coletor de lixo será forçado a executar - o que consome os recursos necessários para aumentar a experiência do usuário e a capacidade de resposta. Objetos temporários também podem machucar. Um grande número de pequenas alocações também pode causar fragmentação de heap.
Por exemplo:
unncessary_object_in_android
No segundo loop do trecho de código acima, estamos criando um novo objeto chunk para cada iteração do loop. Portanto, ele irá essencialmente criar 10.000 objetos do tipo ‘Chunk’ e ocupar muita memória. Imagine, se acabamos de perder o ciclo de rotina de GC antes de criar esses objetos, então esses objetos ficariam por aí até o próximo GC.
O mesmo código pode ser escrito como a versão abaixo:
optimizing_unnecessry_object_in_android1
Apenas um objeto!
Outro exemplo:
Tente reaproveitar o mesmo objeto ao passar pelas funções, conforme abaixo:
reusing_object_in_android2
Observe como a string é anexada ao mesmo objeto StringBuilder diretamente, sem criar nenhum objeto temporário de curto prazo para String e StringBuilder.
Esteja ciente da sobrecarga de memória da linguagem
Ajuda saber o custo e a sobrecarga das construções de linguagem que estamos usando.
Por exemplo:
Um objeto com apenas uma variável int leva 16 bytes no mínimo no Android:
single_variable_object_android3
hashmap_android4
Portanto, cada entrada em um HashMap ocuparia 32 bytes.
Objetos vs. Tipos primitivos
Como mencionado acima, como um objeto em caixa “Inteiro” ocupa 4 vezes mais memória do que o “int” primitivo, devemos sempre tentar usar tipos primitivos sempre que possível. Da mesma forma, um objeto booleano em caixa ocupa muito mais memória do que o tipo booleano primitivo. No exemplo abaixo, a chamada da API retorna um valor “int” primitivo, mas atribuímos esse valor a um objeto Integer. Esta atribuição executará uma operação de autoboxing do objeto int para o objeto Integer. Para uma única chamada, isso pode não importar muito. No entanto, se o estivermos usando com frequência, por exemplo, em um loop interno, ele pode ocupar muita memória desnecessariamente.
android_autoboxing5
Integer (16 bytes) vs int(4 bytes)
Boolean(16 bytes) vs boolean(4 bytes)
vs bit-field(1 bit) //even better!
Enums Vs. Ints
Puro e simples - sempre evite usar enums no Android. Em vez disso, use variáveis “finais estáticas” para constantes. Enums geralmente requerem mais do que o dobro de memória que constantes estáticas.
enums_in_android6
VS
static_final_variables7
Evite classes desnecessárias / classes internas
Cada classe em Java, incluindo classes internas anônimas que criam um objeto e escrevem métodos de acesso internamente, usa cerca de 500 bytes de código.
unnecessary_classes_in_android8
Portanto, esses tipos de ouvintes devem ser cancelados assim que não forem necessários.
Custo oculto de abstrações
Em geral, escrever código com várias camadas de abstração é considerado uma boa prática de programação para linguagens orientadas a objetos. No entanto, quanto mais código for escrito, mais tempo de execução e memória será necessário. Portanto, tente não exagerar nas camadas. Use abstrações apenas onde elas fornecerem um benefício significativo. Por exemplo, nos casos em que se escreve uma biblioteca para ser usada por outros aplicativos, faz sentido usar abstrações para expor apenas certas áreas de funcionalidade.
Cuidado com os serviços
Os serviços são úteis para executar operações em segundo plano, mas são muito caros. Você nunca deve manter um serviço em execução, a menos que seja absolutamente necessário. A melhor maneira de gerenciar automaticamente o ciclo de vida do serviço é usar um IntentService, que será encerrado automaticamente após a conclusão do trabalho. Para outros serviços, é responsabilidade do desenvolvedor do aplicativo garantir que stopService ou stopSelf seja chamado após a conclusão do trabalho.
Liberar memória quando a interface do usuário ficar oculta
Quando o usuário navega para uma atividade diferente, libere os recursos associados a essa atividade nos callbacks onPause e onStop. Esses recursos são geralmente uma rede ou conexão de banco de dados, um receptor de transmissão, etc.
Se o usuário navegar para um aplicativo diferente e todos os componentes de IU do aplicativo estiverem ocultos, o aplicativo receberá o retorno de chamada onTrimMemory () em todas as atividades se o sistema Android precisar eliminar qualquer processo em cache para recuperar alguma memória para um processo em execução. Ouça o nível TRIM_MEMORY_UI_HIDDEN e libere os recursos da IU aqui. Por exemplo, visualizações de texto, visualizações de imagens, etc.
release_memory_hidden_UI
Otimize o uso da memória de bitmaps
Os bitmaps costumam ser o maior usuário de RAM em um aplicativo. Um bitmap carregado na memória ocupa muito mais RAM do que o tamanho da imagem que vemos no sistema de arquivos porque:
tamanho do bitmap = largura * altura * profundidade (geralmente 4 bytes)
Tendo isso em mente, o bitmap deve ser carregado na RAM apenas no tamanho e resolução da tela do dispositivo atual. Portanto, devemos reduzi-lo se o bitmap original estiver em resolução mais alta. No Android 2.3.3 e inferior, os dados de pixel de apoio para um bitmap eram armazenados na memória nativa e havia a necessidade de escrever finalizadores em código Java para liberar essa alocação de memória nativa. Portanto, costumava-se levar mais de um ciclo de GC para liberar memória de bitmap. Portanto, era recomendado usar a reciclagem em bitmaps após usá-los para liberar a memória o mais rápido possível.
No entanto, a partir do Android 3.0 (API de nível 11), os dados de pixel são armazenados no heap Dalvik junto com o bitmap associado. Portanto, não há necessidade de chamar recycle (). Mas ainda é útil otimizar a grande quantidade de memória usada pelo bitmap e devemos tentar reutilizá-los sempre que possível. A API de nível 11 apresenta o campo BitmapFactory.Options.inBitmap. Se esta opção for definida, os métodos de decodificação de BitmapFactory que usam o objeto Opções tentarão reutilizar um bitmap existente ao carregar o conteúdo.
optimize_bitmaps
Use recipientes de dados otimizados
O Android forneceu alguns contêineres de dados otimizados no SDK e bibliotecas de suporte, como SparseArray, SparseBooleanArray e ArrayMap. Esses contêineres podem substituir o Hashmap em que as chaves são do tipo primitivo, como int, Boolean e assim por diante. Como o HashMap precisa de um objeto Integer para armazenar ints, ele ocupa muito mais memória do que realmente é necessário, especialmente se tivermos um grande número de entradas em nosso mapa. ArrayMap também consome menos memória, porém é mais lento no acesso que o HashMap, portanto, deve ser usado apenas ao trabalhar com um número menor de elementos, como  <100.
Aqui estão alguns exemplos de contêineres de dados otimizados que podemos usar como substitutos para seus equivalentes HashMap:
HashMap Array Class
<Integer, Object> SparseArray
<Integer, Boolean> SparseBooleanArray
<Integer, Intege> SparseIntArray
<Integer, Long> SparseLongArray
<Long, Object> LongSparseArray
Use matrizes brutas, como int [], em seções críticas de desempenho do código ou onde estamos trabalhando com centenas de milhares de elementos ao mesmo tempo, se possível.
Proguard And Zipalign
A ferramenta ProGuard reduz, otimiza e ofusca o código removendo o código não utilizado e renomeando classes, campos e métodos com nomes semanticamente obscuros. O ProGuard pode tornar o código mais compacto, exigindo que menos páginas de RAM sejam mapeadas. Mas você deve estar ciente de como o ProGuard funciona antes de usá-lo no aplicativo. Por exemplo, por padrão, o ProGuard eliminará funções JNI nativas, classes ou métodos carregados dinamicamente e código que faz parte de alguma biblioteca referenciada internamente por outra biblioteca no projeto. Portanto, é importante configurar o arquivo de configuração do ProGuard para adicionar regras para manter todas as classes e métodos necessários no projeto.
Ao preparar uma versão de lançamento do aplicativo, é sempre importante executar a ferramenta ZipAlign no APK para que seja realinhado. Isso é necessário para maximizar nosso código estático e recursos a serem mapeados pelo Android. O Eclipse provavelmente já faz isso automaticamente, no entanto, devemos tomar cuidado se estamos construindo os APKs por conta própria usando o Ant.
Para recapitular, algumas dicas gerais de desempenho para otimização de memória:
  • Tente evitar variáveis ou objetos estáticos tanto quanto possível, se eles não forem constantes finais. Variáveis estáticas representam a ameaça de ter referências em outras classes, das quais podemos esquecer e, portanto, causar um vazamento de memória.
  • Prefira métodos estáticos em vez de métodos virtuais onde nenhum dos campos de membro do objeto não é acessado. As invocações estáticas são mais rápidas porque salvam a consulta dalvik do método.
  • Evite getters e setters internos. O acesso direto ao campo é muito mais rápido no Android do que a pesquisa de método virtual. Se um aplicativo não estiver expondo APIs como bibliotecas, ele deve evitar o uso de acessadores.
  • Um erro comum é salvar objetos de “Contexto” em qualquer lugar do aplicativo. Se o desenvolvedor esquecer de liberar até mesmo uma referência de um contexto, isso aumenta a chance de um vazamento de atividade inteira.
  • Combine as chamadas de registro e cancelamento de registro dos ouvintes, receptores em pares correspondentes no ciclo de vida da atividade. Se um receptor de transmissão foi registrado no método onStart, ele deve ter seu registro cancelado apenas no método onStop. O mesmo vale para onCreate-onDestroy e onResume-onPause.
  • No Eclipse ou Android Studio, o ADT vem com uma ferramenta de análise de lint. Ele fornece dicas e truques para otimizar o código do aplicativo no momento da compilação. Sempre preste atenção a essas dicas e avisos e tente incorporá-los, se possível.
  • Para passar informações entre componentes do mesmo aplicativo, evite usar mecanismos IPC como Intents e ContentProviders, se possível. Trabalhe com manipuladores, em vez de interfaces de ouvinte.
Ferramentas para medir o uso de memória Android
Para analisar o uso de memória de um aplicativo no Android, existem várias ferramentas de perfil de memória disponíveis. O Android SDK fornece duas maneiras principais de traçar o perfil do uso de memória de um aplicativo: a guia Allocation Tracker no DDMS e heap dumps. O Allocation Tracker é útil quando queremos ter uma noção de quais tipos de alocações estão acontecendo em um determinado período de tempo, mas não fornece nenhuma informação sobre o estado geral do heap do aplicativo.
Para coletar um despejo de heap:
Observação - a versão DDMS integrada ao Eclipse faz a conversão do hprof automaticamente, portanto, esta etapa não é necessária.
Medir o desempenho do app com o Android Profiler
Ao analisar o despejo de heap, procure vazamentos de memória causados por:
  • Referências de longa duração a uma Activity, Context, View, Drawable e outros objetos que podem conter uma referência à Activity ou Context do contêiner.
  • Classes internas não estáticas (como um Runnable, que pode conter a instância Activity).
  • Gerenciamento de memória para aplicativos Android Gerenciamento de memória para aplicativos Android no Youtube
Estatísticas do processo: entendendo como seu aplicativo usa RAM Estatísticas do processo: entendendo como seu aplicativo usa RAM
Para observar como um aplicativo é dividido entre diferentes tipos de RAM, podemos usar o seguinte comando adb:
adb shell dumpsys meminfo <package_name>
Este comando lista todas as alocações atuais de um aplicativo, medidas em kilobytes. Os detalhes importantes são a memória usada pelo USS (Private Dirty + Private Clean) e o PSS total. Ele também mostrará o número de atividades atualmente em execução, o número de objetos de visualização alocados e objetos de fichário compartilhados entre os processos.
Kitkat (4.4) introduziu um novo serviço chamado Procstats para ajudar a entender melhor o uso de memória em um dispositivo. Há uma tela de interface do usuário encontrada no menu Opções do desenvolvedor para que você possa ver o uso de memória por todos os aplicativos. Para executar Procstats a partir da linha de comando:
adb shell dumpsys procstats <package_name>
(Você acha este artigo interessante? Você pode verificar Embedded Android para ler mais sobre o que fazemos neste espaço.)