Kiedy dokładnie można bezpiecznie używać (anonimowych) klas wewnętrznych?


324

Czytałem kilka artykułów na temat wycieków pamięci w Androidzie i oglądałem ten interesujący film od Google I / O na ten temat .

Mimo to nie do końca rozumiem tę koncepcję, zwłaszcza gdy jest ona bezpieczna lub niebezpieczna dla użytkowników wewnętrznych klas wewnątrz działania .

Oto co zrozumiałem:

Wyciek pamięci nastąpi, jeśli instancja klasy wewnętrznej przetrwa dłużej niż jej klasa zewnętrzna (działanie). -> W jakich sytuacjach może się to zdarzyć?

W tym przykładzie przypuszczam, że nie ma ryzyka wycieku, ponieważ nie ma możliwości, aby rozszerzenie klasy anonimowej trwało OnClickListenerdłużej niż aktywność, prawda?

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

Czy ten przykład jest niebezpieczny i dlaczego?

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

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

Mam wątpliwości co do tego, że zrozumienie tego tematu wiąże się ze szczegółowym zrozumieniem tego, co zachowuje się, gdy działanie zostanie zniszczone i odtworzone.

Czy to jest

Powiedzmy, że właśnie zmieniłem orientację urządzenia (co jest najczęstszą przyczyną wycieków). Kiedy super.onCreate(savedInstanceState)zostanie wywołane u mnie onCreate(), czy przywróci to wartości pól (tak jak przed zmianą orientacji)? Czy to również przywróci stany klas wewnętrznych?

Zdaję sobie sprawę, że moje pytanie nie jest zbyt precyzyjne, ale naprawdę doceniłbym każde wyjaśnienie, które mogłoby wyjaśnić sprawę.


14
Ten post na blogu i ten post na blogu zawierają dobre informacje na temat wycieków pamięci i klas wewnętrznych. :)
Alex Lockwood

Odpowiedzi:


651

To, co zadajesz, jest dość trudnym pytaniem. Chociaż możesz myśleć, że to tylko jedno pytanie, w rzeczywistości zadajesz kilka pytań naraz. Zrobię co w mojej mocy, wiedząc, że muszę to opisać i, mam nadzieję, niektórzy dołączą się, aby opisać to, co mogę przegapić.

Klasy zagnieżdżone: Wprowadzenie

Ponieważ nie jestem pewien, jak dobrze czujesz się w OOP w Javie, omówię kilka podstawowych rzeczy. Zagnieżdżona klasa ma miejsce, gdy definicja klasy jest zawarta w innej klasie. Istnieją zasadniczo dwa typy: klasyczne zagnieżdżone klasy i klasy wewnętrzne. Prawdziwa różnica między nimi to:

  • Klasy zagnieżdżone statycznie:
    • Są uważane za „najwyższego poziomu”.
    • Nie wymaga budowania instancji klasy zawierającej.
    • Nie mogą odwoływać się do zawierających członków klasy bez wyraźnego odwołania.
    • Mają własne życie.
  • Wewnętrzne klasy zagnieżdżone:
    • Zawsze należy zbudować instancję klasy zawierającej.
    • Automatycznie mają niejawne odniesienie do zawierającej instancji.
    • Może uzyskać dostęp do członków klasy kontenera bez odniesienia.
    • Żywotność powinna być nie dłuższa niż żywotność pojemnika.

Odśmiecanie i klasy wewnętrzne

Odśmiecanie jest automatyczne, ale próbuje usunąć obiekty na podstawie tego, czy sądzi, że są używane. Garbage Collector jest całkiem sprytny, ale nie bezbłędny. Może jedynie określić, czy coś jest używane przez to, czy istnieje aktywne odwołanie do obiektu.

Prawdziwy problem polega na tym, że klasa wewnętrzna była utrzymywana przy życiu dłużej niż jej pojemnik. Wynika to z niejawnego odwołania do zawierającej klasy. Jedynym sposobem może się to stać, jeśli obiekt poza klasą zawierającą zachowa odniesienie do obiektu wewnętrznego, bez względu na obiekt zawierający.

Może to prowadzić do sytuacji, w której wewnętrzny obiekt żyje (poprzez referencję), ale odniesienia do obiektu zawierającego zostały już usunięte ze wszystkich innych obiektów. Dlatego wewnętrzny obiekt utrzymuje żywy obiekt zawierający, ponieważ zawsze będzie miał do niego odniesienie. Problem polega na tym, że jeśli nie jest zaprogramowany, nie ma możliwości powrotu do zawierającego obiekt, aby sprawdzić, czy w ogóle żyje.

Najważniejszym aspektem tej realizacji jest to, że nie ma znaczenia, czy jest ona w działaniu, czy jest dająca się wyciągnąć. Będziesz zawsze trzeba być metodyczny przy użyciu klas wewnętrzne i upewnij się, że nie przeżyje obiekty pojemnika. Na szczęście, jeśli nie jest to podstawowy obiekt twojego kodu, wycieki mogą być niewielkie w porównaniu. Niestety są to jedne z najtrudniejszych wycieków, ponieważ mogą pozostać niezauważone, dopóki wiele z nich nie wycieknie.

Rozwiązania: klasy wewnętrzne

  • Uzyskaj tymczasowe odwołania z obiektu zawierającego.
  • Pozwól, aby zawierający obiekt był jedynym, który zachowuje długotrwałe odniesienia do wewnętrznych obiektów.
  • Użyj ustalonych wzorów, takich jak Fabryka.
  • Jeśli klasa wewnętrzna nie wymaga dostępu do elementów klasy zawierającej, rozważ przekształcenie jej w klasę statyczną.
  • Używaj ostrożnie, bez względu na to, czy jest w działaniu, czy nie.

Działania i opinie: Wprowadzenie

Działania zawierają wiele informacji, które można uruchomić i wyświetlić. Działania są zdefiniowane przez cechę, że muszą mieć widok. Mają także pewne automatyczne programy obsługi. Niezależnie od tego, czy ją podasz, czy nie, działanie zawiera niejawne odniesienie do widoku, który zawiera.

Aby widok mógł zostać utworzony, musi wiedzieć, gdzie go utworzyć i czy ma jakieś elementy potomne, aby mógł zostać wyświetlony. Oznacza to, że każdy widok ma odniesienie do działania (via getContext()). Co więcej, każdy widok zawiera odniesienia do swoich dzieci (tj getChildAt().). Wreszcie każdy widok zawiera odniesienie do renderowanej mapy bitowej reprezentującej jej wyświetlanie.

Ilekroć masz odniesienie do działania (lub kontekstu działania), oznacza to, że możesz śledzić cały łańcuch w dół hierarchii układu. Dlatego przecieki pamięci dotyczące działań lub widoków są tak ogromne. To może być mnóstwo pamięci wycieku wszystko naraz.

Zajęcia, widoki i klasy wewnętrzne

Biorąc pod uwagę powyższe informacje o klasach wewnętrznych, są to najczęstsze wycieki pamięci, ale także najczęściej unikane. Chociaż pożądane jest, aby klasa wewnętrzna miała bezpośredni dostęp do członków klasy Działania, wielu chce po prostu ustawić ich statycznie, aby uniknąć potencjalnych problemów. Problem z działaniami i widokami jest znacznie głębszy.

Wyciekłe działania, widoki i konteksty działań

Wszystko sprowadza się do kontekstu i cyklu życia. Istnieją pewne zdarzenia (takie jak orientacja), które zabijają kontekst działania. Ponieważ tak wiele klas i metod wymaga kontekstu, programiści czasami próbują zapisać jakiś kod, chwytając odwołanie do kontekstu i trzymając się go. Zdarza się, że wiele obiektów, które musimy stworzyć, aby uruchomić naszą Aktywność, musi istnieć poza cyklem życia Aktywności, aby Aktywność mogła wykonywać to, co powinna. Jeśli któryś z twoich obiektów ma odniesienie do działania, jego kontekstu lub któregokolwiek z jego widoków, gdy jest zniszczony, właśnie wyciekłeś z tego działania i całego jego drzewa.

Rozwiązania: Działania i widoki

  • Za wszelką cenę unikaj statycznego odniesienia do Widoku lub Działania.
  • Wszystkie odniesienia do kontekstów działań powinny być krótkotrwałe (czas trwania funkcji)
  • Jeśli potrzebujesz długowiecznego kontekstu, użyj kontekstu aplikacji ( getBaseContext()lub getApplicationContext()). Nie przechowują referencji domyślnie.
  • Alternatywnie możesz ograniczyć zniszczenie działania, zastępując zmiany konfiguracji. Nie powstrzymuje to jednak innych potencjalnych zdarzeń przed zniszczeniem działania. Chociaż możesz to zrobić, nadal możesz odwołać się do powyższych praktyk.

Runnables: Wprowadzenie

Runnable nie są wcale takie złe. To znaczy, mogą być, ale tak naprawdę trafiliśmy już w większość niebezpiecznych stref. Runnable to operacja asynchroniczna, która wykonuje zadanie niezależne od wątku, w którym zostało utworzone. Większość plików wykonywalnych jest tworzona z wątku interfejsu użytkownika. Zasadniczo użycie Runnable tworzy kolejny wątek, nieco bardziej zarządzany. Jeśli klasyfikujesz Runnable jak klasę standardową i postępujesz zgodnie z powyższymi wytycznymi, powinieneś napotkać kilka problemów. W rzeczywistości wielu programistów tego nie robi.

Ze względu na łatwość, czytelność i logiczny przebieg programu wielu programistów używa Anonimowych klas wewnętrznych do definiowania swoich Runnables, takich jak powyższy przykład. To daje przykład jak ten, który wpisałeś powyżej. Anonimowa klasa wewnętrzna jest w zasadzie dyskretną klasą wewnętrzną. Po prostu nie musisz tworzyć zupełnie nowej definicji i po prostu zastępować odpowiednie metody. Pod wszystkimi innymi względami jest to klasa wewnętrzna, co oznacza, że ​​zachowuje niejawne odniesienie do swojego kontenera.

Runnable i działania / widoki

Tak! Ta sekcja może być krótka! Ze względu na fakt, że Runnables działają poza bieżącym wątkiem, niebezpieczeństwo z nimi związane wiąże się z długotrwałymi operacjami asynchronicznymi. Jeśli element runnable jest zdefiniowany w działaniu lub widoku jako anonimowa klasa wewnętrzna LUB zagnieżdżona klasa wewnętrzna, istnieje kilka bardzo poważnych zagrożeń. Jest tak, ponieważ, jak wcześniej wspomniano, musi wiedzieć, kto jest jego pojemnikiem. Wprowadź zmianę orientacji (lub zabijanie systemu). Teraz wystarczy wrócić do poprzednich sekcji, aby zrozumieć, co się właśnie wydarzyło. Tak, twój przykład jest dość niebezpieczny.

Rozwiązania: Runnables

  • Spróbuj rozszerzyć Runnable, jeśli nie złamie to logiki twojego kodu.
  • Staraj się, aby rozszerzone Runnables były statyczne, jeśli muszą to być klasy zagnieżdżone.
  • Jeśli musisz użyć Anonimowych Runnables, unikaj ich tworzenia w dowolnym obiekcie, który ma długotrwałe odwołanie do używanego działania lub widoku.
  • Wiele Runnables mogło równie łatwo być AsyncTasks. Rozważ użycie AsyncTask, ponieważ są one domyślnie zarządzane przez VM.

Odpowiedź na ostatnie pytanie Teraz, aby odpowiedzieć na pytania, które nie zostały bezpośrednio poruszone w innych sekcjach tego postu. Zapytałeś: „Kiedy obiekt klasy wewnętrznej może przetrwać dłużej niż klasa zewnętrzna?” Zanim do tego przejdziemy, jeszcze raz podkreślę: chociaż masz rację martwić się tym w Działaniach, może to spowodować wyciek w dowolnym miejscu. Podam prosty przykład (bez użycia działania) tylko w celu zademonstrowania.

Poniżej znajduje się typowy przykład podstawowej fabryki (brak kodu).

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

To nie jest tak powszechny przykład, ale wystarczająco prosty do zademonstrowania. Kluczem tutaj jest 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...
    }
}

Teraz mamy przecieki, ale nie ma fabryki. Mimo że wydaliśmy fabrykę, pozostanie w pamięci, ponieważ każdy wyciek ma odniesienie do niej. Nie ma nawet znaczenia, że ​​klasa zewnętrzna nie ma danych. Zdarza się to znacznie częściej, niż mogłoby się wydawać. Nie potrzebujemy twórcy, tylko jego dzieła. Tworzymy więc tymczasowo, ale używamy kreacji w nieskończoność.

Wyobraź sobie, co dzieje się, gdy nieznacznie zmieniamy konstruktor.

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

Teraz każdy z tych nowych LeakFactories właśnie wyciekł. Co o tym myślisz? Są to dwa bardzo częste przykłady tego, jak klasa wewnętrzna może przeżyć klasę zewnętrzną dowolnego typu. Gdyby ta klasa zewnętrzna była działaniem, wyobraź sobie, o ile gorsze byłoby.

Wniosek

Wymieniają one przede wszystkim znane niebezpieczeństwa związane z niewłaściwym użyciem tych obiektów. Ogólnie rzecz biorąc, ten post powinien obejmować większość pytań, ale rozumiem, że był to długi post, więc jeśli potrzebujesz wyjaśnienia, daj mi znać. Tak długo, jak postępujesz zgodnie z powyższymi praktykami, nie będziesz się martwić o wyciek.


3
Wielkie dzięki za tę jasną i szczegółową odpowiedź. Po prostu nie rozumiem, co masz na myśli, mówiąc, że „wielu programistów używa zamknięć do zdefiniowania swoich Runnables”
Sébastien

1
Zamknięcia w Javie są anonimowymi klasami wewnętrznymi, takimi jak opisywalny Runnable. Jest to sposób na wykorzystanie klasy (prawie ją rozszerzyć) bez pisania zdefiniowanej klasy rozszerzającej Runnable. Nazywa się to zamknięciem, ponieważ jest „definicją klasy zamkniętej”, ponieważ ma własną zamkniętą przestrzeń pamięci w obrębie rzeczywistego obiektu zawierającego.
Fuzzical Logic

26
Oświecający napis! Jedna uwaga dotycząca terminologii: nie ma czegoś takiego jak statyczna klasa wewnętrzna w Javie. ( Dokumenty ). Zagnieżdżona klasa jest statyczna lub wewnętrzna , ale nie może być jednocześnie naraz.
jenzz

2
Chociaż jest to technicznie poprawne, Java pozwala definiować klasy statyczne wewnątrz klas statycznych. Terminologia nie jest dla mojej korzyści, ale dla korzyści innych, którzy nie rozumieją semantyki technicznej. Dlatego po raz pierwszy wspomniano, że są „na najwyższym poziomie”. Dokumenty dla programistów Androida również używają tej terminologii, i to jest dla osób, które patrzą na rozwój Androida, więc pomyślałem, że lepiej zachować spójność.
Fuzzical Logic

13
Świetny post, jeden z najlepszych w StackOverflow, szczególnie na Androida.
StackOverflow Flow
Korzystając z naszej strony potwierdzasz, że przeczytałeś(-aś) i rozumiesz nasze zasady używania plików cookie i zasady ochrony prywatności.
Licensed under cc by-sa 3.0 with attribution required.