Quando exatamente é seguro vazar usar classes internas (anônimas)?

324

Eu tenho lido alguns artigos sobre vazamentos de memória no Android e assisti a este interessante vídeo do Google I / O sobre o assunto .

Ainda assim, não entendo completamente o conceito, e especialmente quando é seguro ou perigoso para o usuário classes internas dentro de uma Activity .

Isto é o que eu entendi:

Um vazamento de memória ocorrerá se uma instância de uma classe interna sobreviver mais do que sua classe externa (uma Atividade). -> Em que situações isso pode acontecer?

Neste exemplo, suponho que não há risco de vazamento, porque não há como a classe anônima estender OnClickListenera vida mais do que a atividade, certo?

    final Dialog dialog = new Dialog(this);
    dialog.setContentView(R.layout.dialog_generic);
    Button okButton = (Button) dialog.findViewById(R.id.dialog_button_ok);
    TextView titleTv = (TextView) dialog.findViewById(R.id.dialog_generic_title);

    // *** Handle button click
    okButton.setOnClickListener(new OnClickListener() {
        public void onClick(View v) {
            dialog.dismiss();
        }
    });

    titleTv.setText("dialog title");
    dialog.show();

Agora, este exemplo é perigoso e por quê?

// We are still inside an Activity
_handlerToDelayDroidMove = new Handler();
_handlerToDelayDroidMove.postDelayed(_droidPlayRunnable, 10000);

private Runnable _droidPlayRunnable = new Runnable() { 
    public void run() {
        _someFieldOfTheActivity.performLongCalculation();
    }
};

Tenho uma dúvida quanto ao fato de que entender esse tópico tem a ver com entender detalhadamente o que é mantido quando uma atividade é destruída e recriada.

É isso?

Digamos que eu mudei a orientação do dispositivo (que é a causa mais comum de vazamento). Quando super.onCreate(savedInstanceState)será chamado no meu onCreate(), isso restaurará os valores dos campos (como eram antes da mudança de orientação)? Isso também restaurará os estados das classes internas?

Sei que minha pergunta não é muito precisa, mas eu realmente aprecio qualquer explicação que possa tornar as coisas mais claras.

Sébastien
fonte
14
Este post e este post tem algumas boas informações sobre vazamentos de memória e classes internas. :)
Alex Lockwood

Respostas:

651

O que você está perguntando é uma pergunta bastante difícil. Embora você pense que é apenas uma pergunta, na verdade você está fazendo várias perguntas ao mesmo tempo. Farei o meu melhor com o conhecimento que tenho para cobri-lo e, espero, alguns outros se juntarão para cobrir o que posso sentir falta.

Classes aninhadas: Introdução

Como não tenho certeza de como você se sente confortável com o OOP em Java, isso será fundamental. Uma classe aninhada é quando uma definição de classe está contida em outra classe. Existem basicamente dois tipos: Classes aninhadas estáticas e Classes internas. A diferença real entre estes são:

  • Classes aninhadas estáticas:
    • São considerados "de nível superior".
    • Não exija que uma instância da classe que contém seja construída.
    • Não pode fazer referência aos membros da classe sem uma referência explícita.
    • Ter sua própria vida.
  • Classes internas aninhadas:
    • Sempre exija que uma instância da classe que contém seja construída.
    • Ter automaticamente uma referência implícita à instância que contém.
    • Pode acessar os membros da classe do contêiner sem a referência.
    • A vida útil não deve exceder a do contêiner.

Coleta de Lixo e Classes Internas

A coleta de lixo é automática, mas tenta remover objetos com base no fato de achar que eles estão sendo usados. O Garbage Collector é bastante inteligente, mas não é perfeito. Ele só pode determinar se algo está sendo usado se existe ou não uma referência ativa ao objeto.

O verdadeiro problema aqui é quando uma classe interna é mantida viva por mais tempo que seu contêiner. Isso ocorre devido à referência implícita à classe que o contém. A única maneira de isso ocorrer é se um objeto fora da classe que contém uma referência ao objeto interno, sem levar em conta o objeto que o contém.

Isso pode levar a uma situação em que o objeto interno está ativo (via referência), mas as referências ao objeto que contém já foram removidas de todos os outros objetos. O objeto interno é, portanto, manter vivo o objeto que o contém, pois sempre terá uma referência a ele. O problema é que, a menos que esteja programado, não há como voltar ao objeto que o contém para verificar se ele está vivo.

O aspecto mais importante para essa realização é que não faz diferença se está em uma atividade ou é um drawable. Você sempre precisará ser metódico ao usar classes internas e garantir que elas nunca superem os objetos do contêiner. Felizmente, se não for um objeto principal do seu código, os vazamentos podem ser pequenos em comparação. Infelizmente, esses são alguns dos vazamentos mais difíceis de encontrar, porque provavelmente passarão despercebidos até que muitos deles vazem.

Soluções: Classes internas

  • Obtenha referências temporárias do objeto que contém.
  • Permita que o objeto contido seja o único a manter referências de longa duração aos objetos internos.
  • Use padrões estabelecidos, como a Fábrica.
  • Se a classe interna não exigir acesso aos membros da classe que contém, considere transformá-la em uma classe estática.
  • Use com cuidado, independentemente de estar ou não em uma atividade.

Atividades e Visões: Introdução

As atividades contêm muitas informações para poder executar e exibir. As atividades são definidas pela característica de que elas devem ter uma Visualização. Eles também têm certos manipuladores automáticos. Se você especificar ou não, a Atividade terá uma referência implícita à Visualização que ela contém.

Para que uma Visualização seja criada, ele deve saber onde criá-la e se possui filhos para que possa ser exibida. Isso significa que toda visualização tem uma referência à atividade (via getContext()). Além disso, todo View mantém referências a seus filhos (ie getChildAt()). Por fim, cada Visualização mantém uma referência ao Bitmap renderizado que representa sua exibição.

Sempre que você tem uma referência a uma atividade (ou contexto de atividade), isso significa que você pode seguir a cadeia INTEIRA na hierarquia de layout. É por isso que vazamentos de memória em relação a Atividades ou exibições são um grande negócio. Pode haver uma tonelada de memória vazando ao mesmo tempo.

Atividades, vistas e classes internas

Dadas as informações acima sobre Classes internas, esses são os vazamentos de memória mais comuns, mas também os mais evitados. Embora seja desejável que uma classe interna tenha acesso direto aos membros de uma classe de Atividades, muitos desejam apenas torná-los estáticos para evitar possíveis problemas. O problema com atividades e visualizações é muito mais profundo que isso.

Atividades vazadas, exibições e contextos de atividades

Tudo se resume ao contexto e ao ciclo de vida. Existem certos eventos (como orientação) que matam um contexto de atividade. Como muitas classes e métodos requerem um Contexto, os desenvolvedores às vezes tentam salvar algum código, pegando uma referência a um Contexto e mantendo-o. Acontece que muitos dos objetos que temos que criar para executar nossa Atividade precisam existir fora do Ciclo de Vida da Atividade, a fim de permitir que a Atividade faça o que precisa. Se algum dos seus objetos tiver uma referência a uma Atividade, seu Contexto ou qualquer uma de suas Visualizações quando ela for destruída, você acabou de vazar essa Atividade e toda a sua árvore Ver.

Soluções: Atividades e Visões

  • Evite, a todo custo, fazer uma referência estática a uma exibição ou atividade.
  • Todas as referências aos contextos de atividade devem ter vida curta (a duração da função)
  • Se você precisar de um contexto de longa duração, use o contexto de aplicativo ( getBaseContext()ou getApplicationContext()). Estes não mantêm referências implicitamente.
  • Como alternativa, você pode limitar a destruição de uma Atividade, substituindo as Alterações na Configuração. No entanto, isso não impede que outros eventos em potencial destruam a Atividade. Enquanto você pode fazer isso, você ainda pode consultar as práticas acima.

Runnables: Introdução

Runnables não são tão ruins assim. Quero dizer, eles poderiam ser, mas realmente já atingimos a maioria das zonas de perigo. Um Runnable é uma operação assíncrona que executa uma tarefa independente do encadeamento em que foi criado. A maioria das executáveis ​​é instanciada a partir do thread da interface do usuário. Em essência, o uso de um Runnable está criando outro encadeamento, apenas um pouco mais gerenciado. Se você classifica um Runnable como uma classe padrão e segue as diretrizes acima, você deve ter poucos problemas. A realidade é que muitos desenvolvedores não fazem isso.

Por facilidade, legibilidade e fluxo lógico do programa, muitos desenvolvedores utilizam Classes Internas Anônimas para definir suas Runnables, como o exemplo que você criou acima. Isso resulta em um exemplo como o que você digitou acima. Uma classe interna anônima é basicamente uma classe interna discreta. Você simplesmente não precisa criar uma definição totalmente nova e simplesmente substituir os métodos apropriados. Em todos os outros aspectos, é uma classe interna, o que significa que mantém uma referência implícita ao seu contêiner.

Runnables e Atividades / Views

Yay! Esta seção pode ser curta! Devido ao fato de os Runnables serem executados fora do encadeamento atual, o perigo com eles ocorre em operações assíncronas de longa execução. Se o executável for definido em uma Atividade ou Exibição como uma Classe Interna Anônima OU Classe Interna aninhada, existem alguns perigos muito sérios. Isso ocorre porque, como afirmado anteriormente, ele precisa saber quem é seu contêiner. Digite a mudança de orientação (ou interrupção do sistema). Agora basta voltar às seções anteriores para entender o que aconteceu. Sim, seu exemplo é bastante perigoso.

Soluções: Runnables

  • Tente estender o Runnable, se não quebrar a lógica do seu código.
  • Faça o seu melhor para tornar estáveis ​​as Runnables estendidas, se elas precisarem ser classes aninhadas.
  • Se você deve usar Runnables Anônimos, evite criá-los em qualquer objeto que tenha uma referência de longa duração a uma Atividade ou Exibição que esteja em uso.
  • Muitos Runnables poderiam facilmente ter sido AsyncTasks. Considere usar o AsyncTask, pois esses são gerenciados por VM por padrão.

Respondendo à pergunta final Agora, para responder às perguntas que não foram abordadas diretamente pelas outras seções desta postagem. Você perguntou "Quando um objeto de uma classe interna pode sobreviver mais tempo do que sua classe externa?" Antes de chegarmos a isso, deixe-me enfatizar: embora você tenha razão em se preocupar com isso em Atividades, ele pode causar um vazamento em qualquer lugar. Fornecerei um exemplo simples (sem usar uma Atividade) apenas para demonstrar.

Abaixo está um exemplo comum de uma fábrica básica (sem o código).

public class LeakFactory
{//Just so that we have some data to leak
    int myID = 0;
// Necessary because our Leak class is an Inner class
    public Leak createLeak()
    {
        return new Leak();
    }

// Mass Manufactured Leak class
    public class Leak
    {//Again for a little data.
       int size = 1;
    }
}

Este não é um exemplo tão comum, mas simples o suficiente para demonstrar. A chave aqui é o construtor ...

public class SwissCheese
{//Can't have swiss cheese without some holes
    public Leak[] myHoles;

    public SwissCheese()
    {//Gotta have a Factory to make my holes
        LeakFactory _holeDriller = new LeakFactory()
    // Now, let's get the holes and store them.
        myHoles = new Leak[1000];

        for (int i = 0; i++; i<1000)
        {//Store them in the class member
            myHoles[i] = _holeDriller.createLeak();
        }

    // Yay! We're done! 

    // Buh-bye LeakFactory. I don't need you anymore...
    }
}

Agora, temos vazamentos, mas nenhuma fábrica. Mesmo que tenhamos lançado a Fábrica, ela permanecerá na memória, pois cada Leak tem uma referência a ela. Nem importa que a classe externa não tenha dados. Isso acontece com muito mais frequência do que se imagina. Não precisamos do criador, apenas de suas criações. Então criamos uma temporariamente, mas usamos as criações indefinidamente.

Imagine o que acontece quando mudamos um pouco o construtor.

public class SwissCheese
{//Can't have swiss cheese without some holes
    public Leak[] myHoles;

    public SwissCheese()
    {//Now, let's get the holes and store them.
        myHoles = new Leak[1000];

        for (int i = 0; i++; i<1000)
        {//WOW! I don't even have to create a Factory... 
        // This is SOOOO much prettier....
            myHoles[i] = new LeakFactory().createLeak();
        }
    }
}

Agora, cada um desses novos LeakFactories acaba de vazar. O que você acha daquilo? Esses são dois exemplos muito comuns de como uma classe interna pode sobreviver a uma classe externa de qualquer tipo. Se essa classe externa tivesse sido uma Atividade, imagine quanto pior teria sido.

Conclusão

Eles listam os perigos principalmente conhecidos do uso inadequado desses objetos. Em geral, este post deve ter abordado a maioria das suas perguntas, mas entendo que foi um post muito longo; portanto, se você precisar de esclarecimentos, entre em contato. Contanto que você siga as práticas acima, você terá muito pouca preocupação com vazamentos.

Fuzzical Logic
fonte
3
Muito obrigado por esta resposta clara e detalhada. Eu simplesmente não entendo o que você quer dizer com "muitos desenvolvedores utilizam tampas para definir suas Runnables"
Sébastien
1
Os fechamentos em Java são classes internas anônimas, como o executável que você descreve. É uma maneira de utilizar uma classe (quase a estende) sem escrever uma classe definida que estenda o Runnable. É chamado de fechamento porque é "uma definição de classe fechada", pois possui seu próprio espaço de memória fechado dentro do objeto real.
Fuzzical Logic
26
Artigo esclarecedor! Uma observação sobre a terminologia: não existe classe interna estática em Java. ( Docs ). Uma classe aninhada é estática ou interna , mas não pode ser as duas ao mesmo tempo.
jenzz
2
Enquanto isso é tecnicamente correto, Java permite definir classes estáticas dentro de classes estáticas. A terminologia não é para meu benefício, mas para o benefício de outras pessoas que não entendem a semântica técnica. É por isso que é mencionado pela primeira vez que eles são "de nível superior". Os documentos do desenvolvedor do Android também usam essa terminologia, e isso é para pessoas que estão olhando para o desenvolvimento do Android, então achei melhor manter a consistência.
Fuzzical Logic
13
Ótimo post, um dos melhores no StackOverflow, esp para Android.
StackOverflowed