Quando è esattamente sicuro usare classi interne (anonime)?


324

Ho letto alcuni articoli sulle perdite di memoria in Android e ho visto questo interessante video dell'I / O di Google sull'argomento .

Tuttavia, non capisco appieno il concetto, specialmente quando è sicuro o pericoloso per le classi interne degli utenti all'interno di un'attività .

Questo è quello che ho capito:

Si verificherà una perdita di memoria se un'istanza di una classe interna sopravvive più a lungo della sua classe esterna (un'attività). -> In quali situazioni può succedere?

In questo esempio, suppongo che non vi sia alcun rischio di perdita, perché non esiste alcun modo in cui l'estensione della classe anonima OnClickListenervivrà più a lungo dell'attività, giusto?

    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();

Ora, questo esempio è pericoloso e perché?

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

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

Ho dei dubbi sul fatto che la comprensione di questo argomento abbia a che fare con la comprensione dettagliata di ciò che viene mantenuto quando un'attività viene distrutta e ricreata.

È?

Diciamo che ho appena cambiato l'orientamento del dispositivo (che è la causa più comune di perdite). Quando super.onCreate(savedInstanceState)verrà chiamato nel mio onCreate(), questo ripristinerà i valori dei campi (come prima della modifica dell'orientamento)? Ciò ripristinerà anche gli stati delle classi interiori?

Mi rendo conto che la mia domanda non è molto precisa, ma apprezzerei davvero qualsiasi spiegazione che possa chiarire le cose.


14
Questo post sul blog e questo post sul blog contengono alcune buone informazioni su perdite di memoria e classi interne. :)
Alex Lockwood,

Risposte:


651

Quello che stai ponendo è una domanda piuttosto difficile. Mentre potresti pensare che sia solo una domanda, in realtà stai ponendo più domande contemporaneamente. Farò del mio meglio con la consapevolezza che devo coprirlo e, si spera, alcuni altri si uniranno per coprire ciò che potrei perdere.

Classi nidificate: introduzione

Dato che non sono sicuro di quanto tu sia a tuo agio con OOP in Java, questo colpirà un paio di basi. Una classe nidificata è quando una definizione di classe è contenuta in un'altra classe. Esistono sostanzialmente due tipi: Classi nidificate statiche e Classi interne. La vera differenza tra questi sono:

  • Classi nidificate statiche:
    • Sono considerati "di alto livello".
    • Non richiede la creazione di un'istanza della classe contenente.
    • Non può fare riferimento ai membri della classe contenente senza un riferimento esplicito.
    • Hanno la loro vita.
  • Classi annidate interne:
    • Richiede sempre la creazione di un'istanza della classe contenitore.
    • Avere automaticamente un riferimento implicito all'istanza contenente.
    • Può accedere ai membri della classe del contenitore senza il riferimento.
    • La vita non dovrebbe essere più lunga di quella del contenitore.

Garbage Collection e classi interne

Garbage Collection è automatico ma cerca di rimuovere gli oggetti in base al fatto che pensi che vengano utilizzati. Il Garbage Collector è piuttosto intelligente, ma non perfetto. Può solo determinare se qualcosa viene utilizzato se esiste o meno un riferimento attivo all'oggetto.

Il vero problema qui è quando una classe interiore è stata mantenuta in vita più a lungo del suo contenitore. Ciò è dovuto al riferimento implicito alla classe contenente. L'unico modo in cui ciò può accadere è se un oggetto esterno alla classe contenente mantiene un riferimento all'oggetto interno, indipendentemente dall'oggetto contenitore.

Ciò può portare a una situazione in cui l'oggetto interno è vivo (tramite riferimento) ma i riferimenti all'oggetto contenente sono già stati rimossi da tutti gli altri oggetti. L'oggetto interno, quindi, mantiene vivo l'oggetto contenitore perché avrà sempre un riferimento ad esso. Il problema è che, a meno che non sia programmato, non c'è modo di tornare all'oggetto contenitore per verificare se è ancora vivo.

L'aspetto più importante di questa realizzazione è che non fa alcuna differenza se si trova in un'attività o è un disegno. Dovrai sempre essere metodico quando usi le classi interne e assicurarti che non sopravvivano mai agli oggetti del contenitore. Fortunatamente, se non è un oggetto principale del codice, le perdite potrebbero essere piccole in confronto. Sfortunatamente, queste sono alcune delle perdite più difficili da trovare, perché è probabile che passino inosservate fino a quando molte di esse non sono trapelate.

Soluzioni: classi interne

  • Ottieni riferimenti temporanei dall'oggetto contenitore.
  • Consentire all'oggetto di contenimento di essere l'unico a mantenere riferimenti di lunga durata agli oggetti interni.
  • Usa modelli consolidati come Factory.
  • Se la classe interna non richiede l'accesso ai membri della classe contenente, prendere in considerazione la possibilità di trasformarla in una classe statica.
  • Usare con cautela, indipendentemente dal fatto che si trovi in ​​un'attività o meno.

Attività e viste: Introduzione

Le attività contengono molte informazioni per poter essere eseguite e visualizzate. Le attività sono definite dalla caratteristica che devono avere una vista. Hanno anche alcuni gestori automatici. Indipendentemente dal fatto che tu lo specifichi o meno, l'attività ha un riferimento implicito alla vista che contiene.

Per poter creare una vista, deve sapere dove crearla e se ha figli in modo che possa essere visualizzata. Ciò significa che ogni vista ha un riferimento all'attività (via getContext()). Inoltre, ogni vista mantiene riferimenti ai propri figli (es getChildAt().). Infine, ogni vista mantiene un riferimento alla bitmap renderizzata che rappresenta la sua visualizzazione.

Ogni volta che hai un riferimento a un'attività (o contesto di attività), ciò significa che puoi seguire TUTTA la catena nella gerarchia del layout. Questo è il motivo per cui le perdite di memoria relative alle attività o alle visualizzazioni sono un grosso problema. Può essere una tonnellata di memoria che fuoriesce tutto in una volta.

Attività, viste e classi interne

Date le informazioni di cui sopra sulle classi interne, queste sono le perdite di memoria più comuni, ma anche le più comunemente evitate. Mentre è auspicabile che una classe interna abbia accesso diretto ai membri di una classe Attività, molti sono disposti a renderli statici per evitare potenziali problemi. Il problema con Attività e Viste è molto più profondo di così.

Attività trapelate, visualizzazioni e contesti di attività

Tutto dipende dal contesto e dal ciclo di vita. Ci sono alcuni eventi (come l'orientamento) che uccideranno un contesto di attività. Poiché così tante classi e metodi richiedono un contesto, gli sviluppatori a volte cercheranno di salvare un po 'di codice afferrando un riferimento a un contesto e trattenendolo. Accade semplicemente che molti degli oggetti che dobbiamo creare per eseguire la nostra attività debbano esistere al di fuori del ciclo di vita delle attività, al fine di consentire all'attività di fare ciò che deve fare. Se uno qualsiasi dei tuoi oggetti ha un riferimento a un'attività, al suo contesto o a una qualsiasi delle sue viste quando viene distrutta, hai appena fatto trapelare quell'attività e l'intero albero della vista.

Soluzioni: attività e viste

  • Evitare, a tutti i costi, di fare un riferimento statico a una vista o attività.
  • Tutti i riferimenti ai contesti di attività devono essere di breve durata (la durata della funzione)
  • Se è necessario un contesto di lunga durata, utilizzare il contesto dell'applicazione ( getBaseContext()o getApplicationContext()). Questi non mantengono i riferimenti implicitamente.
  • In alternativa, puoi limitare la distruzione di un'attività sostituendo le modifiche alla configurazione. Tuttavia, ciò non impedisce ad altri potenziali eventi di distruggere l'attività. Mentre puoi farlo, potresti comunque voler fare riferimento alle pratiche di cui sopra.

Runnables: Introduzione

I runner non sono poi così male. Voglio dire, potrebbero esserlo, ma in realtà abbiamo già colpito la maggior parte delle zone di pericolo. Un Runnable è un'operazione asincrona che esegue un'attività indipendentemente dal thread sul quale è stata creata. La maggior parte dei runnable sono istanziati dal thread dell'interfaccia utente. In sostanza, l'utilizzo di Runnable sta creando un altro thread, leggermente più gestito. Se classifichi un Runnable come una classe standard e segui le linee guida sopra, dovresti riscontrare alcuni problemi. La realtà è che molti sviluppatori non lo fanno.

Per facilità, leggibilità e flusso logico del programma, molti sviluppatori utilizzano le classi interne anonime per definire i loro Runnable, come nell'esempio sopra creato. Ciò si traduce in un esempio come quello che hai digitato sopra. Una classe interna anonima è sostanzialmente una classe interna discreta. Non devi creare una definizione completamente nuova e semplicemente sostituire i metodi appropriati. Sotto tutti gli altri aspetti è una classe interna, il che significa che mantiene un riferimento implicito al suo contenitore.

Runnables e attività / viste

Sìì! Questa sezione può essere breve! A causa del fatto che i Runnable funzionano al di fuori del thread corrente, il pericolo con questi è rappresentato dalle operazioni asincrone a esecuzione prolungata. Se il runnable è definito in un'attività o vista come una classe interna anonima o una classe interna nidificata, ci sono alcuni pericoli molto gravi. Questo perché, come precedentemente affermato, deve sapere chi è il suo contenitore. Immettere la modifica dell'orientamento (o kill del sistema). Ora fai riferimento alle sezioni precedenti per capire cosa è appena successo. Sì, il tuo esempio è abbastanza pericoloso.

Soluzioni: Runnables

  • Prova ad estendere Runnable, se non rompe la logica del tuo codice.
  • Fai del tuo meglio per rendere statici Runnables estesi, se devono essere classi nidificate.
  • Se è necessario utilizzare Runnable anonimi, evitare di crearli in qualsiasi oggetto che abbia un riferimento di lunga durata a un'attività o vista in uso.
  • Molti Runnable avrebbero potuto essere altrettanto facilmente AsyncTasks. Prendi in considerazione l'utilizzo di AsyncTask poiché quelli sono gestiti da VM per impostazione predefinita.

Rispondere alla domanda finale ora per rispondere alle domande che non sono state affrontate direttamente dalle altre sezioni di questo post. Hai chiesto "Quando un oggetto di una classe interna può sopravvivere più a lungo della sua classe esterna?" Prima di arrivare a questo, lasciatemi riproporre: anche se hai ragione a preoccuparti di ciò in Attività, può causare una perdita ovunque. Fornirò un semplice esempio (senza usare un'attività) solo per dimostrare.

Di seguito è riportato un esempio comune di una fabbrica di base (manca il codice).

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;
    }
}

Questo non è un esempio comune, ma abbastanza semplice da dimostrare. La chiave qui è il costruttore ...

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...
    }
}

Ora abbiamo perdite, ma nessuna fabbrica. Anche se abbiamo rilasciato la Factory, rimarrà nella memoria perché ogni singola perdita ha un riferimento ad essa. Non importa nemmeno che la classe esterna non abbia dati. Questo accade molto più spesso di quanto si possa pensare. Non abbiamo bisogno del creatore, solo delle sue creazioni. Quindi ne creiamo uno temporaneamente, ma utilizziamo le creazioni indefinitamente.

Immagina cosa succede quando cambiamo leggermente il costruttore.

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();
        }
    }
}

Ora, ognuna di quelle nuove LeakFactories è appena trapelata. Cosa ne pensi? Questi sono due esempi molto comuni di come una classe interna può sopravvivere a una classe esterna di qualsiasi tipo. Se quella classe esterna fosse stata un'attività, immagina quanto sarebbe peggiorato.

Conclusione

Questi elencano i pericoli principalmente noti dell'utilizzo inappropriato di questi oggetti. In generale, questo post avrebbe dovuto coprire la maggior parte delle tue domande, ma capisco che era un post molto lungo, quindi se hai bisogno di chiarimenti, fammelo sapere. Finché seguirai le pratiche di cui sopra, avrai pochissima preoccupazione di perdite.


3
Grazie mille per questa risposta chiara e dettagliata. Semplicemente non capisco cosa intendi per "molti sviluppatori utilizzano chiusure per definire i loro Runnables"
Sébastien,

1
Le chiusure in Java sono Classi interne anonime, come il Runnable che descrivi. È un modo per utilizzare una classe (quasi estenderla) senza scrivere una Classe definita che estende Runnable. Si chiama chiusura perché è "una definizione di classe chiusa" in quanto ha il suo spazio di memoria chiuso all'interno dell'oggetto contenitore effettivo.
Fuzzical Logic,

26
Scrittura illuminante! Un'osservazione relativa alla terminologia: non esiste una classe interna statica in Java. ( Documenti ). Una classe nidificata è statica o interna , ma non può essere entrambe contemporaneamente.
jenzz,

2
Sebbene ciò sia tecnicamente corretto, Java consente di definire classi statiche all'interno di classi statiche. La terminologia non è a mio vantaggio, ma a beneficio di altri che non comprendono la semantica tecnica. Questo è il motivo per cui si dice per la prima volta che sono "di alto livello". Anche i documenti per sviluppatori Android usano questa terminologia, e questo è per le persone che guardano allo sviluppo di Android, quindi ho pensato che fosse meglio mantenere la coerenza.
Fuzzical Logic,

13
Ottimo post, uno dei migliori su StackOverflow, esp per Android.
StackOverflow del
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.