Aplicativo para Android na memória. Relatório de otimização para Yandex.Luncher

O sistema leve Android Go aumentou os requisitos para aplicativos pré-instalados - o tamanho e a memória usados. Enfrentamos o desafio de atender a esses requisitos. Realizamos várias otimizações e decidimos alterar seriamente a arquitetura de nosso shell gráfico - Yandex.Luncher. O chefe da equipe de desenvolvimento de aplicativos móveis Alexander Starchenko compartilhou essa experiência.


- Meu nome é Alexander, sou de São Petersburgo, da equipe que desenvolve o Yandex.Loncher e o Yandex.Phone. Hoje vou contar como otimizamos a memória no Launcher. Primeiro, explicarei brevemente o que é o Launcher. A seguir, discutimos os motivos pelos quais precisamos otimizar a memória. Depois disso, consideraremos como medir corretamente a memória e em que ela consiste. Então vamos seguir praticando. Vou falar sobre como otimizamos a memória no Launcher e como chegamos a uma solução radical para o problema. E no final, falarei sobre como monitoramos o uso da memória, como a mantemos sob controle.



"Launcher" ou "Launcher" - não é tão importante. Nós da Yandex costumava chamá-lo de Launcher e, no relatório, usarei a palavra "Launcher".



Outro ponto importante: o Launcher é amplamente distribuído por meio de predefinições, ou seja, quando você compra um novo telefone, o Yandex.Loncher pode se tornar o único gerenciador de aplicativos, o gerenciador de desktop doméstico do seu telefone.

Agora, pelas razões pelas quais precisamos otimizar a memória. Vou começar com a nossa razão. Em suma, este é o Android Go. E agora mais. No final de 2017, o Google lançou o Android Oreo e sua versão especial, a edição Android Oreo Go. Sobre o que é especial? Esta versão foi projetada para low-end, para telefones baratos com até um gigabyte de RAM. O que mais ela é especial? Para aplicativos pré-instalados nesta versão do Android, o Google apresenta requisitos adicionais. Em particular - os requisitos para o consumo de RAM. Grosso modo, algum tempo após o lançamento, a memória do aplicativo é removida e o tamanho não deve exceder de 30 a 50 megabytes para o Launcher, dependendo do tamanho da tela do telefone. 30 nos menores, 50 nos telões.

Note-se também que o Google continua a desenvolver essa área e já existe uma edição do Android Pie Go.

Que outros motivos poderiam ter para otimizar o uso da memória? Primeiro, é menos provável que seu aplicativo faça o download. Em segundo lugar, ele funcionará mais rápido, pois será menos provável que funcione com o coletor de lixo e a memória será alocada com menos frequência. Objetos extras não serão criados, visualizações extras não serão infladas, etc. Indiretamente, a julgar pela nossa experiência, isso levará a uma diminuição no tamanho do apk do seu aplicativo. Tudo isso junto fornecerá mais instalações e melhores classificações no Google Play.

Ok, agora sabemos por que otimizar a memória. Vamos ver por que meios medi-lo e em que consiste.

Link do slide

Provavelmente muitos de vocês já viram esta foto. Esta é uma captura de tela do perfil do Android Studio, de uma exibição de memória. Essa ferramenta é descrita em detalhes suficientes em developer.android.com. Provavelmente muitos de vocês já os usaram. Quem não usou - tente.

O que é bom aqui? Está sempre à mão. É conveniente usar no processo de desenvolvimento. No entanto, tem algumas desvantagens. Nem todas as alocações do seu aplicativo são visíveis aqui. Por exemplo, as fontes baixadas não são visíveis aqui. Além disso, com a ajuda dessa ferramenta, é inconveniente ver quais classes estão carregadas na memória e você não pode usá-la no modo automático, ou seja, não pode configurar algum tipo de teste automático com base no perfil do Android Studio.

Links do slide: primeiro , segundo

A ferramenta a seguir existe desde o desenvolvimento do Android no Eclipse, é o Memory Analyzer, MAT, para abreviar. Ele é fornecido como um aplicativo independente e é compatível com despejos de memória que você pode salvar no Android Studio.

Para fazer isso, você precisará usar um pequeno utilitário, um conversor profissional. Ele vem com a edição Android Go e tem várias vantagens. Por exemplo, ele pode criar caminhos para raízes gs. Nos ajudou muito a ver exatamente quais classes são carregadas pelo Launcher e quando elas são carregadas. Não foi possível fazer isso usando o Android Studio Profiler.



A próxima ferramenta é o utilitário dumpsys, especificamente dumpsys meminfo. Aqui você vê parte da saída deste comando. Ele fornece um conhecimento relativamente alto do consumo de memória. No entanto, tem certas vantagens. É conveniente usar no modo automático. Você pode configurar facilmente testes que simplesmente chamam este comando. Também mostra a memória imediatamente para todos os processos. E mostra todos os locais. Até onde sabemos, o Google usa o valor da memória dessa ferramenta no processo de teste.

Vamos usar um exemplo de saída para descrever brevemente em que consiste a memória do aplicativo. O primeiro é o Java Heap, todos os locais do seu código Java e Kotlin. Geralmente esta seção é grande o suficiente. Em seguida é a pilha nativa. Aqui estão as alocações do código nativo. Mesmo se você não usar explicitamente o código nativo em seu aplicativo, as alocações estarão presentes aqui, pois muitos objetos Android - a mesma exibição - alocam memória nativa. A próxima seção é Código. Tudo relacionado ao código chega aqui: bytecode, fontes. O código também pode ser muito grande se você usar muitas bibliotecas de terceiros não otimizadas. A seguir, é apresentada a pilha de software Java e código nativo, geralmente pequena. Em seguida, vem a memória gráfica. Isso inclui Superfície, texturas, ou seja, a memória que se espalha entre a CPU e a GPU é usada para renderização. A seguir, a seção Outros Privados. Isso inclui tudo o que não caiu nas seções acima, tudo o que o sistema não pôde espalhar sobre elas. Geralmente, esses são alguns tipos de alocações nativas. A seguir, é a seção Sistema, essa é a parte da memória do sistema atribuída ao seu aplicativo.

E no final, temos TOTAL, esta é a soma de todas as seções listadas. Queremos reduzi-lo.



O que mais é importante saber sobre a medição de memória? Primeiro de tudo, nosso aplicativo não controla totalmente todas as alocações. Ou seja, nós, como desenvolvedores, não temos controle total sobre qual código será baixado.

O seguinte. A memória do aplicativo pode saltar bastante. Durante o processo de medição, você pode observar fortes diferenças nas leituras. Isso pode ser devido ao tempo gasto, bem como a vários cenários. Nesse sentido, quando otimizamos a memória, analisamos, é muito importante fazer isso nas mesmas condições. Idealmente, no mesmo dispositivo. Melhor ainda, se você tiver a opção de ligar para o Garbage Collector.

Ótimo. Sabemos por que precisamos otimizar a memória, como medi-la corretamente e em que consiste. Vamos praticar, e eu vou lhe dizer como otimizamos a memória no Launcher.



Essa foi a situação a princípio. Tínhamos três processos, que no total alocavam cerca de 120 megabytes. Isso é quase quatro vezes mais do que gostaríamos de receber.



Em relação à alocação do processo principal, havia uma grande seção do Java Heap, muitos gráficos, código grande e um Native Heap bastante grande.



Primeiro, abordamos o problema de maneira bastante ingênua e decidimos seguir algumas recomendações do Google a partir de alguns recursos, tentar resolver o problema rapidamente. Chamamos atenção para os métodos sintéticos que são gerados durante o processo de compilação. Tínhamos mais de 2 mil deles. Em algumas horas, todos os excluímos e removemos a memória.



E eles obtiveram um ganho de cerca de um ou dois megabytes na seção de código. Ótimo.

Em seguida, voltamos nossa atenção para enum. Como você sabe, enum é uma classe. E, como o Google finalmente admitiu, o enum não é muito eficiente em termos de memória. Traduzimos todo o enum para InDef e StringDef. Aqui você pode contestar que o ProgArt ajudará aqui. Mas, de fato, o ProgArt não substituirá todo o enum por tipos primitivos. É melhor fazer você mesmo. A propósito, tivemos mais de 90 enum, bastante.



Essa otimização já levou dias, já que a maioria tinha que ser feita manualmente, e ganhamos cerca de três a seis megabytes na seção heap Java.

Em seguida, chamamos a atenção para a coleção. Usamos coleções Java bastante padrão, como o HashMap. Tínhamos mais de 150 deles e todos foram criados no início do Launcher. Os substituímos por SparseArray, SimpleArrayMap e ArrayMap e começamos a criar coleções com um tamanho predeterminado para que os slots vazios não fossem alocados. Ou seja, passamos o tamanho da coleção para o construtor.



Isso também deu um certo ganho, e essa otimização também levou dias, a maioria dos quais fizemos manualmente.

Depois, demos um passo mais específico. Vimos que temos três processos. Como sabemos, mesmo um processo vazio no Android consome cerca de 8 a 10 megabytes de memória, bastante.

Detalhes sobre os processos foram contados pelo meu colega Arthur Vasilov. Não faz muito tempo, na conferência Mosdroid, foi o seu relatório , também sobre o Android Go.



O que tínhamos depois dessas otimizações? No dispositivo de teste principal, observamos o consumo de memória na região de 80 a 100 megabytes, não ruim o suficiente, mas ainda não o suficiente. Começamos a medir a memória em outros dispositivos. Descobrimos que em dispositivos mais rápidos, o consumo de memória era muito maior. Aconteceu que tínhamos muitas inicializações pendentes diferentes. Depois de algum tempo, o Launcher aumentou algumas visualizações, iniciou algumas bibliotecas, etc.



O que fizemos? Primeiro, analisamos a visualização, todos os layouts. Removidas todas as visualizações que foram infladas com a visibilidade perdida. Eles os colocaram em layouts separados, começaram a inflá-los programaticamente. Aqueles que não precisávamos, geralmente parávamos de inflar até o momento em que o usuário precisava deles. Prestamos atenção à otimização de imagens. Paramos de carregar imagens que o usuário não vê no momento. No caso do Launcher, estas eram imagens-ícones de aplicativos na lista completa de aplicativos. Até a sua abertura, não os enviamos. Isso nos deu uma vitória muito boa na seção de gráficos.

Também verificamos nossos caches de imagem na memória. Verificou-se que nem todas eram ótimas; nem todas as imagens correspondentes à tela do telefone em que o Launcher estava executando estavam armazenadas na memória.

Depois disso, começamos a analisar a seção de código e percebemos que tínhamos muitas classes bastante pesadas de algum lugar. Descobriu-se que estas são principalmente classes de biblioteca. Encontramos algumas coisas estranhas em algumas bibliotecas. Uma das bibliotecas criou o HashMap e, em um inicializador estático, o obstruiu com um número suficientemente grande de objetos.



Outra biblioteca também carregou arquivos de áudio em um bloco estático, que ocupava cerca de 700 kilobytes de memória.



Paramos de inicializar essas bibliotecas e começamos a trabalhar com elas somente quando essas funções são realmente necessárias aos usuários. Todas essas otimizações levaram várias semanas. Testamos muito, verificamos que não apresentamos problemas adicionais. Mas também obtivemos uma vitória bastante boa, cerca de 25 dos 40 megabytes nas seções Native, Heap, Code e Java Heap.

Mas isso não foi suficiente. O consumo de memória ainda não caiu para 30 megabytes. Parecia que tínhamos esgotado todas as opções para algumas otimizações automáticas e seguras simples.

Decidimos considerar soluções radicais. Aqui vimos duas opções - a criação de um aplicativo lite separado ou o processamento da arquitetura do Launcher e a transição para uma arquitetura modular com a capacidade de fazer montagens do Launcher sem módulos adicionais. A primeira opção é bastante longa e cara. Provavelmente, a criação de um aplicativo resultará em um aplicativo separado completo para você, que precisará ser totalmente suportado e desenvolvido. Por outro lado, a opção com uma arquitetura modular também é bastante cara, arriscada, mas ainda é mais rápida, já que você já está trabalhando com uma base de código conhecida, já possui um conjunto de testes de unidade automáticos, testes de integração e testes manuais casos.

Note-se que, independentemente da opção escolhida, você terá que abandonar parte dos recursos do seu aplicativo na versão para Android Go. Isso é normal. O Google faz o mesmo em seus aplicativos Go.

Como resultado, implementando uma arquitetura modular, resolvemos com segurança nossos problemas de memória e começamos a passar em testes mesmo em dispositivos com uma tela pequena, ou seja, reduzimos o consumo de memória para 30 megabytes.



Um pouco sobre o monitoramento de memória, sobre como mantemos o uso da memória sob controle. Antes de tudo, configuramos analisadores estáticos, o mesmo erro no caso de usar enum, criar métodos sintéticos ou usar coleções não otimizadas.

Ainda mais difícil. Configuramos testes de integração automática que executam o Launcher em emuladores e depois de um tempo diminuem o consumo de memória. Se for muito diferente da compilação anterior, avisos e alertas serão acionados. Começamos a investigar o problema e não publicamos alterações que aumentam o uso da memória do Launcher.

Para resumir. Existem várias ferramentas para monitorar a memória, medindo a memória para uma operação rápida e eficiente. É melhor usá-los todos, pois eles têm seus prós e contras.

Soluções radicais com arquitetura modular se mostraram mais confiáveis ​​e eficientes para nós. Lamentamos que não os tenhamos imediatamente. Mas os passos sobre os quais falei no início do relatório não foram em vão. Percebemos que a versão principal do aplicativo começou a otimizar o uso da memória, para trabalhar mais rapidamente. Obrigada

Source: https://habr.com/ru/post/pt448966/


All Articles