Wann genau ist es lecksicher, (anonyme) innere Klassen zu verwenden?

324

Ich habe einige Artikel über Speicherlecks in Android gelesen und mir dieses interessante Video von Google I / O zu diesem Thema angesehen .

Trotzdem verstehe ich das Konzept nicht vollständig, insbesondere wenn es für Benutzerinnenklassen innerhalb einer Aktivität sicher oder gefährlich ist .

Das habe ich verstanden:

Ein Speicherverlust tritt auf, wenn eine Instanz einer inneren Klasse länger überlebt als ihre äußere Klasse (eine Aktivität). -> In welchen Situationen kann das passieren?

In diesem Beispiel besteht vermutlich kein Risiko eines Lecks, da die anonyme Klasse, die erweitert OnClickListenerwird, auf keinen Fall länger als die Aktivität lebt, oder?

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

Ist dieses Beispiel gefährlich und warum?

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

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

Ich habe Zweifel an der Tatsache, dass das Verständnis dieses Themas damit zu tun hat, im Detail zu verstehen, was aufbewahrt wird, wenn eine Aktivität zerstört und neu erstellt wird.

Ist es?

Angenommen, ich habe gerade die Ausrichtung des Geräts geändert (was die häufigste Ursache für Undichtigkeiten ist). Wann super.onCreate(savedInstanceState)wird in my aufgerufen onCreate(), werden dadurch die Werte der Felder wiederhergestellt (wie vor der Änderung der Ausrichtung)? Wird dies auch die Zustände der inneren Klassen wiederherstellen?

Mir ist klar, dass meine Frage nicht sehr präzise ist, aber ich würde mich über jede Erklärung freuen, die die Dinge klarer machen könnte.

Sébastien
quelle
14
Dieser Blog-Beitrag und dieser Blog-Beitrag enthalten einige gute Informationen zu Speicherlecks und inneren Klassen. :)
Alex Lockwood

Antworten:

651

Was Sie fragen, ist eine ziemlich schwierige Frage. Während Sie vielleicht denken, dass es nur eine Frage ist, stellen Sie tatsächlich mehrere Fragen gleichzeitig. Ich werde mein Bestes geben mit dem Wissen, dass ich darüber berichten muss, und hoffentlich werden einige andere mitmachen, um zu besprechen, was ich vermissen könnte.

Verschachtelte Klassen: Einführung

Da ich nicht sicher bin, wie gut Sie mit OOP in Java vertraut sind, wird dies einige Grundlagen treffen. Eine verschachtelte Klasse liegt vor, wenn eine Klassendefinition in einer anderen Klasse enthalten ist. Grundsätzlich gibt es zwei Typen: Statische verschachtelte Klassen und innere Klassen. Der wirkliche Unterschied zwischen diesen ist:

  • Statisch verschachtelte Klassen:
    • Werden als "Top-Level" betrachtet.
    • Es ist nicht erforderlich, dass eine Instanz der enthaltenden Klasse erstellt wird.
    • Darf nicht ohne expliziten Verweis auf die enthaltenen Klassenmitglieder verweisen.
    • Habe ihr eigenes Leben.
  • Inner verschachtelte Klassen:
    • Es muss immer eine Instanz der enthaltenden Klasse erstellt werden.
    • Verfügen Sie automatisch über einen impliziten Verweis auf die enthaltene Instanz.
    • Kann ohne Referenz auf die Klassenmitglieder des Containers zugreifen.
    • Die Lebensdauer soll nicht länger sein als die des Containers.

Müllabfuhr und innere Klassen

Die Speicherbereinigung erfolgt automatisch, versucht jedoch, Objekte zu entfernen, je nachdem, ob sie verwendet werden. Der Garbage Collector ist ziemlich schlau, aber nicht fehlerfrei. Es kann nur feststellen, ob etwas verwendet wird, indem festgestellt wird, ob ein aktiver Verweis auf das Objekt vorhanden ist oder nicht.

Das eigentliche Problem hierbei ist, wenn eine innere Klasse länger als ihr Container am Leben gehalten wurde. Dies liegt an der impliziten Bezugnahme auf die enthaltende Klasse. Dies kann nur passieren, wenn ein Objekt außerhalb der enthaltenden Klasse einen Verweis auf das innere Objekt behält, ohne Rücksicht auf das enthaltende Objekt.

Dies kann zu einer Situation führen, in der das innere Objekt lebt (über Referenz), die Verweise auf das enthaltende Objekt jedoch bereits von allen anderen Objekten entfernt wurden. Das innere Objekt hält daher das enthaltende Objekt am Leben, da es immer einen Verweis darauf hat. Das Problem dabei ist, dass es keine Möglichkeit gibt, zum enthaltenen Objekt zurückzukehren, um zu überprüfen, ob es überhaupt lebt, es sei denn, es ist programmiert.

Der wichtigste Aspekt dieser Erkenntnis ist, dass es keinen Unterschied macht, ob es sich um eine Aktivität handelt oder ob es sich um eine Zeichnungsaktivität handelt. Sie müssen immer methodisch vorgehen, wenn Sie innere Klassen verwenden, und sicherstellen, dass sie niemals Objekte des Containers überleben. Glücklicherweise können die Lecks im Vergleich gering sein, wenn es sich nicht um ein Kernobjekt Ihres Codes handelt. Leider sind dies einige der am schwersten zu findenden Lecks, da sie wahrscheinlich unbemerkt bleiben, bis viele von ihnen durchgesickert sind.

Lösungen: Innere Klassen

  • Erhalten Sie temporäre Referenzen vom enthaltenden Objekt.
  • Lassen Sie zu, dass das enthaltende Objekt das einzige ist, das langlebige Verweise auf die inneren Objekte behält.
  • Verwenden Sie etablierte Muster wie die Factory.
  • Wenn für die innere Klasse kein Zugriff auf die enthaltenen Klassenmitglieder erforderlich ist, sollten Sie sie in eine statische Klasse umwandeln.
  • Verwenden Sie es mit Vorsicht, unabhängig davon, ob es sich um eine Aktivität handelt oder nicht.

Aktivitäten und Ansichten: Einführung

Aktivitäten enthalten viele Informationen, die ausgeführt und angezeigt werden können. Aktivitäten werden durch das Merkmal definiert, dass sie eine Ansicht haben müssen. Sie haben auch bestimmte automatische Handler. Unabhängig davon, ob Sie es angeben oder nicht, enthält die Aktivität einen impliziten Verweis auf die darin enthaltene Ansicht.

Damit eine Ansicht erstellt werden kann, muss sie wissen, wo sie erstellt werden muss und ob untergeordnete Elemente vorhanden sind, damit sie angezeigt werden kann. Dies bedeutet, dass jede Ansicht einen Verweis auf die Aktivität (via getContext()) enthält. Darüber hinaus enthält jede Ansicht Verweise auf ihre untergeordneten Elemente (dh getChildAt()). Schließlich enthält jede Ansicht einen Verweis auf die gerenderte Bitmap, die ihre Anzeige darstellt.

Wenn Sie einen Verweis auf eine Aktivität (oder einen Aktivitätskontext) haben, bedeutet dies, dass Sie der GESAMTEN Kette in der Layouthierarchie folgen können. Aus diesem Grund sind Speicherverluste in Bezug auf Aktivitäten oder Ansichten eine große Sache. Es kann eine Menge Speicher sein, der auf einmal verloren geht.

Aktivitäten, Ansichten und innere Klassen

Angesichts der obigen Informationen zu inneren Klassen sind dies die häufigsten Speicherlecks, aber auch die am häufigsten vermiedenen. Während es wünschenswert ist, dass eine innere Klasse direkten Zugriff auf die Mitglieder einer Aktivitätsklasse hat, sind viele bereit, sie nur statisch zu machen, um potenzielle Probleme zu vermeiden. Das Problem mit Aktivitäten und Ansichten geht viel tiefer.

Durchgesickerte Aktivitäten, Ansichten und Aktivitätskontexte

Alles hängt vom Kontext und dem Lebenszyklus ab. Es gibt bestimmte Ereignisse (z. B. Orientierung), die einen Aktivitätskontext beenden. Da für so viele Klassen und Methoden ein Kontext erforderlich ist, versuchen Entwickler manchmal, Code zu speichern, indem sie einen Verweis auf einen Kontext abrufen und daran festhalten. Es kommt einfach so vor, dass viele der Objekte, die wir zum Ausführen unserer Aktivität erstellen müssen, außerhalb des Aktivitätslebenszyklus existieren müssen, damit die Aktivität das tun kann, was sie tun muss. Wenn eines Ihrer Objekte zufällig einen Verweis auf eine Aktivität, ihren Kontext oder eine ihrer Ansichten enthält, wenn es zerstört wird, haben Sie gerade diese Aktivität und ihren gesamten Ansichtsbaum durchgesickert.

Lösungen: Aktivitäten und Ansichten

  • Vermeiden Sie unter allen Umständen einen statischen Verweis auf eine Ansicht oder Aktivität.
  • Alle Verweise auf Aktivitätskontexte sollten von kurzer Dauer sein (Dauer der Funktion).
  • Wenn Sie einen langlebigen Kontext benötigen, verwenden Sie den Anwendungskontext ( getBaseContext()oder getApplicationContext()). Diese enthalten keine impliziten Referenzen.
  • Alternativ können Sie die Zerstörung einer Aktivität einschränken, indem Sie Konfigurationsänderungen überschreiben. Dies hindert jedoch andere potenzielle Ereignisse nicht daran, die Aktivität zu zerstören. Während Sie dies tun können, möchten Sie möglicherweise dennoch auf die oben genannten Vorgehensweisen verweisen.

Runnables: Einführung

Runnables sind eigentlich gar nicht so schlecht. Ich meine, sie könnten es sein, aber wir haben wirklich schon die meisten Gefahrenzonen getroffen. Eine ausführbare Datei ist eine asynchrone Operation, die eine Aufgabe unabhängig von dem Thread ausführt, für den sie erstellt wurde. Die meisten ausführbaren Dateien werden über den UI-Thread instanziiert. Im Wesentlichen wird durch die Verwendung eines Runnable ein weiterer Thread erstellt, der nur geringfügig besser verwaltet wird. Wenn Sie ein Runnable wie eine Standardklasse klassifizieren und die obigen Richtlinien befolgen, sollten Sie auf wenige Probleme stoßen. Die Realität ist, dass viele Entwickler dies nicht tun.

Aus Gründen der Einfachheit, Lesbarkeit und des logischen Programmflusses verwenden viele Entwickler anonyme innere Klassen, um ihre Runnables zu definieren, wie im obigen Beispiel. Dies führt zu einem Beispiel wie dem oben eingegebenen. Eine anonyme innere Klasse ist im Grunde eine diskrete innere Klasse. Sie müssen nur keine ganz neue Definition erstellen und einfach die entsprechenden Methoden überschreiben. Im Übrigen handelt es sich um eine innere Klasse, was bedeutet, dass ein impliziter Verweis auf ihren Container beibehalten wird.

Runnables und Aktivitäten / Ansichten

Yay! Dieser Abschnitt kann kurz sein! Aufgrund der Tatsache, dass Runnables außerhalb des aktuellen Threads ausgeführt werden, besteht bei diesen die Gefahr von lang laufenden asynchronen Vorgängen. Wenn die ausführbare Datei in einer Aktivität oder Ansicht als anonyme innere Klasse ODER verschachtelte innere Klasse definiert ist, bestehen einige sehr schwerwiegende Gefahren. Dies liegt daran, wie bereits erwähnt, es hat zu wissen , wer sein Behälter ist. Geben Sie die Orientierungsänderung (oder den Systemabbruch) ein. Lesen Sie jetzt einfach die vorherigen Abschnitte, um zu verstehen, was gerade passiert ist. Ja, Ihr Beispiel ist ziemlich gefährlich.

Lösungen: Runnables

  • Versuchen Sie, Runnable zu erweitern, wenn dies nicht die Logik Ihres Codes verletzt.
  • Geben Sie Ihr Bestes, um erweiterte Runnables statisch zu machen, wenn es sich um verschachtelte Klassen handeln muss.
  • Wenn Sie Anonymous Runnables verwenden müssen, vermeiden sie bei der Schaffung von jedem Objekt , das einen langlebigen Bezug auf eine Aktivität oder View hat , das in Gebrauch ist.
  • Viele Runnables könnten genauso gut AsyncTasks gewesen sein. Erwägen Sie die Verwendung von AsyncTask, da diese standardmäßig von VM verwaltet werden.

Beantwortung der letzten Frage Beantworten Sie nun die Fragen, die in den anderen Abschnitten dieses Beitrags nicht direkt behandelt wurden. Sie fragten: "Wann kann ein Objekt einer inneren Klasse länger überleben als seine äußere Klasse?" Bevor wir dazu kommen, möchte ich noch einmal betonen: Obwohl Sie sich zu Recht in Aktivitäten darüber Sorgen machen, kann dies überall zu Undichtigkeiten führen. Ich werde ein einfaches Beispiel (ohne Verwendung einer Aktivität) geben, um dies zu demonstrieren.

Unten finden Sie ein allgemeines Beispiel für eine Basisfabrik (ohne Code).

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

Dies ist ein nicht so verbreitetes Beispiel, aber einfach genug, um es zu demonstrieren. Der Schlüssel hier ist der Konstruktor ...

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

Jetzt haben wir Lecks, aber keine Fabrik. Obwohl wir die Factory veröffentlicht haben, bleibt sie im Speicher, da jedes einzelne Leck einen Verweis darauf hat. Es ist nicht einmal wichtig, dass die äußere Klasse keine Daten hat. Dies passiert weitaus häufiger als man denkt. Wir brauchen den Schöpfer nicht, nur seine Kreationen. Wir erstellen also vorübergehend eine, verwenden die Kreationen jedoch auf unbestimmte Zeit.

Stellen Sie sich vor, was passiert, wenn wir den Konstruktor nur geringfügig ändern.

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

Jetzt ist jede einzelne dieser neuen LeakFactories durchgesickert. Was halten Sie davon? Dies sind zwei sehr häufige Beispiele dafür, wie eine innere Klasse eine äußere Klasse eines beliebigen Typs überleben kann. Wenn diese äußere Klasse eine Aktivität gewesen wäre, stellen Sie sich vor, wie viel schlimmer es gewesen wäre.

Fazit

Diese listen die vorwiegend bekannten Gefahren einer unsachgemäßen Verwendung dieser Objekte auf. Im Allgemeinen hätte dieser Beitrag die meisten Ihrer Fragen abdecken sollen, aber ich verstehe, dass es ein langer Beitrag war. Wenn Sie also eine Klärung benötigen, lassen Sie es mich einfach wissen. Solange Sie die oben genannten Praktiken befolgen, müssen Sie sich kaum Sorgen über Leckagen machen.

Fuzzical Logic
quelle
3
Vielen Dank für diese klare und detaillierte Antwort. Ich verstehe einfach nicht, was Sie unter "viele Entwickler verwenden Verschlüsse, um ihre Runnables zu definieren" verstehen
Sébastien
1
Verschlüsse in Java sind anonyme innere Klassen, wie das von Ihnen beschriebene Runnable. Es ist eine Möglichkeit, eine Klasse zu verwenden (fast zu erweitern), ohne eine definierte Klasse zu schreiben, die Runnable erweitert. Es wird als Abschluss bezeichnet, da es sich um eine "geschlossene Klassendefinition" handelt, da es einen eigenen geschlossenen Speicherbereich innerhalb des tatsächlich enthaltenen Objekts hat.
Fuzzical Logic
26
Erleuchtende Zusammenfassung! Eine Bemerkung zur Terminologie: In Java gibt es keine statische innere Klasse . ( Docs ). Eine verschachtelte Klasse ist entweder statisch oder innerlich , kann jedoch nicht beide gleichzeitig sein.
Jenzz
2
Während dies technisch korrekt ist, können Sie mit Java statische Klassen innerhalb statischer Klassen definieren. Die Terminologie ist nicht zu meinem Vorteil, sondern zum Nutzen anderer, die die technische Semantik nicht verstehen. Aus diesem Grund wird erstmals erwähnt, dass es sich um "Top-Level" handelt. Die Android-Entwicklerdokumente verwenden ebenfalls diese Terminologie, und dies ist für Leute gedacht, die sich mit Android-Entwicklung befassen. Daher hielt ich es für besser, die Konsistenz aufrechtzuerhalten.
Fuzzical Logic
13
Toller Beitrag, einer der besten bei StackOverflow, besonders für Android.
StackOverflowed