Problemas de falta de memória no aplicativo Android - tentei de tudo e ainda não consegui

87

Passei 4 dias inteiros tentando de tudo para descobrir o vazamento de memória em um aplicativo que estou desenvolvendo, mas as coisas deixaram de fazer sentido há muito tempo.

O aplicativo que estou desenvolvendo é de natureza social, então pense no perfil de Atividades (P) e liste Atividades com dados - por exemplo, emblemas (B). Você pode pular de um perfil para uma lista de emblemas, para outros perfis, para outras listas, etc.

Então imagine um fluxo como este P1 -> B1 -> P2 -> B2 -> P3 -> B3, etc. Para consistência, estou carregando perfis e emblemas do mesmo usuário, então cada página P é a mesma e assim é cada página B.

A essência geral do problema é: depois de navegar um pouco, dependendo do tamanho de cada página, recebo uma exceção de falta de memória em locais aleatórios - bitmaps, strings, etc - não parece ser consistente.

Depois de fazer tudo que você pode imaginar para descobrir por que estou ficando sem memória, não encontrei nada. O que não entendo é por que o Android não está matando P1, B1, etc, se ficar sem memória ao carregar e, em vez disso, travar. Eu esperaria que essas atividades anteriores morressem e fossem ressuscitadas se eu voltasse a elas via onCreate () e onRestoreInstanceState ().

Sem falar nisso - mesmo se eu fizer P1 -> B1 -> Voltar -> B1 -> Voltar -> B1, ainda assim recebo um travamento. Isso indica algum tipo de vazamento de memória, mas mesmo depois de despejar hprof e usar MAT e JProfiler, não consigo identificá-lo.

Desativei o carregamento de imagens da web (e aumentei os dados de teste carregados para compensar e tornar o teste justo) e certifiquei-me de que o cache de imagem usa SoftReferences. Na verdade, o Android tenta liberar as poucas SoftReferences que possui, mas logo antes de perder memória.

As páginas do Badge obtêm dados da web, carregam-nos em um array de EntityData de um BaseAdapter e enviam para um ListView (na verdade estou usando o excelente MergeAdapter do CommonsWare , mas nesta atividade do Badge, há realmente apenas 1 adaptador, mas eu queria mencionar esse fato de qualquer maneira).

Eu examinei o código e não consegui encontrar nada que vazasse. Limpei e anulei tudo que pude encontrar e até mesmo System.gc () à esquerda e à direita, mas ainda assim o aplicativo trava.

Ainda não entendo por que as atividades inativas que estão na pilha não são colhidas e eu realmente adoraria descobrir isso.

Neste ponto, estou procurando por dicas, conselhos, soluções ... qualquer coisa que possa ajudar.

Obrigado.

Artem Russakovskii
fonte
Então, se eu abri 15 atividades nos últimos 20 segundos (o usuário está passando muito rápido), poderia ser esse o problema? Que linha de código devo adicionar para limpar a atividade depois que ela for exibida? Recebo um outOfMemoryerro. Obrigado!
Ruchir Baronia

Respostas:

109

Ainda não entendo por que as atividades inativas que estão na pilha não são colhidas e eu realmente adoraria descobrir isso.

Não é assim que as coisas funcionam. O único gerenciamento de memória que impacta o ciclo de vida da atividade é a memória global em todos os processos, já que o Android decide que está com pouca memória e, portanto, precisa eliminar os processos em segundo plano para recuperá-la.

Se o seu aplicativo está em primeiro plano, iniciando mais e mais atividades, ele nunca vai para o segundo plano, portanto, sempre atingirá seu limite de memória de processo local antes que o sistema chegue perto de encerrar seu processo. (E quando ele matar seu processo, ele irá matar o processo que hospeda todas as atividades, incluindo tudo o que está atualmente em primeiro plano.)

Portanto, parece-me que seu problema básico é: você está permitindo que muitas atividades sejam executadas ao mesmo tempo e / ou cada uma dessas atividades está segurando muitos recursos.

Você só precisa redesenhar sua navegação para não depender do empilhamento de um número arbitrário de atividades potencialmente pesadas. A menos que você faça muitas coisas em onStop () (como chamar setContentView () para limpar a hierarquia de visualização da atividade e limpar as variáveis ​​de qualquer outra coisa que ela possa estar segurando), você simplesmente vai ficar sem memória.

Você pode querer considerar o uso das novas APIs de fragmento para substituir essa pilha arbitrária de atividades por uma única atividade que gerencia de forma mais rígida sua memória. Por exemplo, se você usar os recursos de pilha de retorno de fragmentos, quando um fragmento vai para a pilha de retorno e não está mais visível, seu método onDestroyView () é chamado para remover completamente sua hierarquia de visualização, reduzindo bastante sua área de cobertura.

Agora, quanto a você travar no fluxo onde pressiona para trás, vai para uma atividade, pressiona para trás, vai para outra atividade, etc e nunca tem uma pilha profunda, então sim, você só tem um vazamento. Esta postagem do blog descreve como depurar vazamentos: http://android-developers.blogspot.com/2011/03/memory-analysis-for-android.html

hackbod
fonte
47
Em defesa do OP, não é isso que a documentação sugere. Citando developer.android.com/guide/topics/fundamentals/… "(Quando uma atividade é interrompida), ela não é mais visível para o usuário e pode ser eliminada pelo sistema quando a memória é necessária em outro lugar." "Se uma atividade for pausada ou interrompida, o sistema pode eliminá-la da memória ... pedindo que ela termine (chamando seu método finish ())" "(onDestroy () é chamado) porque o sistema está destruindo temporariamente esta instância de a atividade para economizar espaço. "
CommonsWare
42
Na mesma página, "Porém, quando o sistema destrói uma atividade para recuperar a memória". O que você está indicando é que o Android nunca destrói atividades para recuperar memória, mas apenas encerrará o processo para fazer isso. Nesse caso, esta página precisa ser reescrita seriamente, pois sugere repetidamente que o Android destruirá uma atividade para recuperar a memória. Observe também que muitas das passagens citadas também existem nos ActivityJavaDocs.
CommonsWare
6
Pensei ter colocado isso aqui, mas postei uma solicitação para atualizar a documentação para isso: code.google.com/p/android/issues/detail?id=21552
Justin Breitfeller
15
É 2013 e os documentos vieram apenas para apresentar o ponto falso de forma mais clara: developer.android.com/training/basics/activity-lifecycle/… , "Assim que sua atividade for interrompida, o sistema pode destruir a instância se ela precisar se recuperar memória do sistema. Em casos EXTREMOS, o sistema pode simplesmente
encerrar o
2
Bem, merda. Definitivamente, sempre pensei (por causa dos documentos) que o sistema gerenciava as atividades interrompidas em seu processo e as serializaria e desserializaria (destruiria e criaria) conforme necessário.
dcow
21

Algumas dicas:

  1. Certifique-se de não estar no contexto da atividade de vazamento.

  2. Certifique-se de não manter referências em bitmaps. Limpe todas as suas ImageViews em Activity # onStop, algo assim:

    Drawable d = imageView.getDrawable();  
    if (d != null) d.setCallback(null);  
    imageView.setImageDrawable(null);  
    imageView.setBackgroundDrawable(null);
  3. Recicle bitmaps se você não precisar mais deles.

  4. Se você usa cache de memória, como memory-lru, certifique-se de que não está usando muita memória.

  5. Não apenas as imagens ocupam muita memória, mas certifique-se de não manter muitos outros dados na memória. Isso pode acontecer facilmente se você tiver listas infinitas em seu aplicativo. Tente armazenar dados em cache no DataBase.

  6. No Android 4.2, há um bug (stackoverflow # 13754876) com aceleração de hardware, então se você usar hardwareAccelerated=trueem seu manifesto, haverá vazamento de memória. GLES20DisplayList- mantenha as referências retidas, mesmo se você executou a etapa (2) e ninguém mais está fazendo referência a este bitmap. Aqui você precisa:

    a) desative a aceleração de hardware para api 16/17;
    ou
    b) desanexar a vista que contém o bitmap

  7. Para Android 3+, você pode tentar usar android:largeHeap="true"no seu AndroidManifest. Mas não resolverá seus problemas de memória, apenas adie-os.

  8. Se você precisa de navegação infinita, então Fragments - deve ser sua escolha. Portanto, você terá 1 atividade, que apenas alternará entre os fragmentos. Dessa forma, você também resolverá alguns problemas de memória, como o número 4.

  9. Use o Memory Analyzer para descobrir a causa do vazamento de memória.
    Aqui está um vídeo muito bom do Google I / O 2011: Gerenciamento de memória para aplicativos Android
    Se você está lidando com bitmaps, deve ler: Exibindo bitmaps com eficiência

vovkab
fonte
Então, se eu abri 15 atividades nos últimos 20 segundos (o usuário está passando muito rápido), poderia ser esse o problema? Que linha de código devo adicionar para limpar a atividade depois que ela for exibida? Recebo um outOfMemoryerro. Obrigado!
Ruchir Baronia
4

Os bitmaps costumam ser os culpados por erros de memória no Android, então essa seria uma boa área para verificar novamente.

Ed Burnette
fonte
Então, se eu abri 15 atividades nos últimos 20 segundos (o usuário está passando muito rápido), poderia ser esse o problema? Que linha de código devo adicionar para limpar a atividade depois que ela for exibida? Recebo um outOfMemoryerro. Obrigado!
Ruchir Baronia
2

Você está segurando algumas referências para cada atividade? AFAIK esta é uma razão que impede o Android de excluir atividades da pilha.

Você também consegue reproduzir esse erro em outros dispositivos? Eu experimentei um comportamento estranho de alguns dispositivos Android dependendo da ROM e / ou fabricante do hardware.

NewProggie
fonte
Consegui reproduzir isso em um Droid executando CM7 com heap máximo definido como 16 MB, que é o mesmo valor que estou testando no emulador.
Artem Russakovskii
Você pode estar envolvido. Quando a 2ª atividade começar, a 1ª fará onPause-> onStop ou apenas onPause? Porque estou imprimindo todas as chamadas de ciclo de vida do ******* e estou vendo onPause -> onCreate, sem onStop. E um dos despejos de memória realmente disse algo como onPause = true ou onStop = false para 3 atividades que estava eliminando.
Artem Russakovskii
OnStop deve ser chamado quando a atividade sai da tela, mas pode não ser se for recuperada antecipadamente pelo sistema.
dia
Ele não é recuperado porque não vejo onCreate e onRestoreInstanceState chamados se eu clicar de volta.
Artem Russakovskii
de acordo com o ciclo de vida, eles podem nunca ser chamados, verifique minha resposta que tem um link para o blog oficial do desenvolvedor, é mais do que provável a maneira como você está transmitindo seus bitmaps
dia
2

Acho que o problema pode ser uma combinação de muitos fatores declarados aqui nas respostas que está causando problemas. Como @Tim disse, uma referência (estática) a uma atividade ou um elemento nessa atividade pode fazer com que o GC pule a atividade. Aqui está o artigo que discute essa faceta. Eu acho que o problema provável vem de algo que mantém a Activity em um estado de "Processo Visível" ou superior, o que garante que a Activity e seus recursos associados nunca serão recuperados.

Eu passei pelo problema oposto um tempo atrás com um serviço, então foi isso que me fez pensar: há algo mantendo sua atividade no topo da lista de prioridades do processo para que ela não esteja sujeita ao GC do sistema, como uma referência (@Tim) ou um loop (@Alvaro). O loop não precisa ser um item interminável ou de longa duração, apenas algo que funciona muito como um método recursivo ou loop em cascata (ou algo nesse sentido).

EDIT: Pelo que entendi, onPause e onStop são chamados automaticamente pelo Android conforme necessário. Os métodos existem principalmente para você substituir, para que possa cuidar do que precisa antes que o processo de hospedagem seja interrompido (salvar variáveis, salvar manualmente o estado, etc.); mas observe que está claramente declarado que onStop (junto com onDestroy) pode não ser chamado em todos os casos. Além disso, se o processo de hospedagem também estiver hospedando uma Atividade, Serviço, etc. que tenha um status "Forground" ou "Visível", o SO pode nem mesmo olhar para interromper o processo / thread. Por exemplo: uma atividade e um serviço são avaliados no mesmo processo e o serviço retorna START_STICKYdeonStartCommand()o processo assume automaticamente pelo menos um status visível. Essa pode ser a chave aqui, tente declarar um novo proc para a Activity e veja se isso muda alguma coisa. Tente adicionar esta linha à declaração de sua atividade no manifesto como: android:process=":proc2"e, em seguida, execute os testes novamente se sua atividade compartilha um processo com qualquer outra coisa. O pensamento aqui é que se você limpar sua atividade e são bastante certeza de que o problema não é a sua atividade, em seguida, outra coisa é o problema e é hora de caçador para isso.

Além disso, não me lembro onde o vi (se é que o vi nos documentos do Android), mas me lembro de algo sobre fazer PendingIntentreferência a uma Activity pode fazer com que uma Activity se comporte dessa maneira.

Aqui está um link para a onStartCommand()página com alguns insights sobre a frente de não matar do processo.

Kingsolmn
fonte
Com base no link que você forneceu (obrigado), uma atividade pode ser interrompida se seu onStop for chamado, mas no meu caso, acho que algo está impedindo o onStop de funcionar. Definitivamente vou averiguar o porquê. No entanto, também diz que as atividades apenas com onPause também podem ser eliminadas, o que não está acontecendo no meu caso (vejo onPause sendo chamado, mas não onStop).
Artem Russakovskii
1

então, a única coisa que posso realmente pensar é se você tem uma variável estática que faz referência direta ou indiretamente ao contexto. Mesmo algo como uma referência a parte do aplicativo. Tenho certeza que você já tentou, mas vou sugerir apenas no caso, tente apenas anular TODAS as suas variáveis ​​estáticas no onDestroy () apenas para garantir que o coletor de lixo o pegue

Tim Fenton - VenumX
fonte
1

A maior fonte de vazamento de memória que encontrei foi causada por alguma referência global, de alto nível ou de longa data ao contexto. Se você estiver mantendo o "contexto" armazenado em uma variável em qualquer lugar, poderá encontrar vazamentos de memória imprevisíveis.

Dirk Conrad Coetsee
fonte
Sim, eu ouvi e li isso muitas vezes, mas tive problemas para definir. Se eu pudesse descobrir como rastreá-los de forma confiável.
Artem Russakovskii
1
No meu caso, isso se traduziu em qualquer variável no nível da classe que foi definida como igual ao contexto (por exemplo, Class1.variable = getContext ();). Em geral, substituir todo uso de "contexto" em meu aplicativo por uma nova chamada para "getContext" ou similar resolveu meus maiores problemas de memória. Mas os meus eram instáveis ​​e erráticos, não previsíveis como no seu caso, então possivelmente algo diferente.
Dirk Conrad Coetsee
1

Tente passar getApplicationContext () para qualquer coisa que precise de um Context. Você pode ter uma variável global que está mantendo uma referência às suas atividades e evitando que sejam coletadas como lixo.

vee
fonte
1

Uma das coisas que realmente ajudaram no problema de memória no meu caso acabou sendo configurada inPurgeable como true para meus bitmaps. Consulte Por que eu NÃO usaria a opção inPurgeable do BitmapFactory? e a discussão da resposta para mais informações.

A resposta de Dianne Hackborn e nossa discussão subsequente (também obrigada, CommonsWare) ajudaram a esclarecer certas coisas sobre as quais eu estava confuso, então obrigada por isso.

Artem Russakovskii
fonte
Eu sei que este é um tópico antigo, mas vejo que pareço encontrar este tópico em muitas pesquisas. Quero vincular um ótimo tutorial que aprendi muito e que você pode encontrar aqui: developer.android.com/training/displaying-bitmaps/index.html
Codeversed em
0

Eu encontrei o mesmo problema com você. Estava trabalhando em um aplicativo de mensagens instantâneas, para o mesmo contato, é possível iniciar um ProfileActivity em um ChatActivity, e vice-versa. Acabei de adicionar uma string extra na intenção de iniciar outra atividade, ela pega as informações do tipo de classe da atividade inicial e o ID do usuário. Por exemplo, ProfileActivity inicia uma ChatActivity, então em ChatActivity.onCreate, eu marco o tipo de classe de invocador 'ProfileActivity' e a id do usuário, se for para iniciar uma Activity, eu verificaria se é uma 'ProfileActivity' para o usuário ou não . Nesse caso, basta chamar 'finish ()' e voltar para o ProfileActivity anterior em vez de criar um novo. Vazamento de memória é outra coisa.

qiuping345
fonte