Коли саме це безпечно для використання (анонімних) внутрішніх класів?


324

Я читав деякі статті про витоки пам’яті в Android і переглядав це цікаве відео з вводу / виводу Google на цю тему .

Тим не менш, я не повністю розумію цю концепцію, особливо коли це безпечно або небезпечно для внутрішніх класів користувача всередині діяльності .

Це те, що я зрозумів:

Витік пам'яті відбудеться, якщо екземпляр внутрішнього класу зберігається довше, ніж його зовнішній клас (активність). -> У яких ситуаціях це може статися?

У цьому прикладі я припускаю, що немає ніякого ризику витоку, оскільки немає можливості анонімний клас, який розширюватиметься OnClickListener, житиме довше, ніж активність, правда?

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

Тепер цей приклад небезпечний, і чому?

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

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

У мене виникають сумніви щодо того, що розуміння цієї теми пов'язане з детальним розумінням того, що зберігається, коли діяльність знищується та відтворюється заново.

Є це?

Скажімо, я просто змінив орієнтацію пристрою (що є найпоширенішою причиною протікання). Коли super.onCreate(savedInstanceState)буде викликано мій onCreate(), чи відновить це значення полів (як вони були до того, як орієнтація змінилася)? Чи це також відновить стану внутрішніх класів?

Я усвідомлюю, що моє запитання не дуже точне, але я дуже вдячний за будь-яке пояснення, яке могло б зробити речі яснішими.


14
У цьому блозі та в цьому дописі є хороша інформація про витоки пам'яті та внутрішні класи. :)
Алекс Локвуд

Відповіді:


651

Те, що ви задаєте, - досить складне питання. Хоча ви можете подумати, що це лише одне запитання, ви насправді задаєте відразу кілька питань. Я зроблю все можливе з того, що я маю це висвітлити, і, сподіваюся, деякі інші приєднаються, щоб висвітлити те, що я можу пропустити.

Вкладені класи: Вступ

Оскільки я не впевнений, наскільки вам комфортно з OOP на Java, це вплине на пару основ. Вкладений клас - це коли визначення класу міститься в іншому класі. В основному існує два типи: Статичні вкладені класи та Внутрішні класи. Справжня різниця між ними:

  • Статичні вкладені класи:
    • Вважаються "найвищим рівнем".
    • Не потрібно будувати екземпляр класу, що містить.
    • Не може посилатися на членів класу без явного посилання.
    • Мають своє життя.
  • Внутрішні вкладені класи:
    • Завжди потрібно побудувати екземпляр класу, що містить.
    • Автоматично мати неявну посилання на містить екземпляр.
    • Можливий доступ до членів класу контейнера без посилання.
    • Термін експлуатації повинен бути не довший, ніж у контейнера.

Збір сміття та внутрішні класи

Збір сміття є автоматичним, але намагається видалити об'єкти залежно від того, чи вважає вони, що вони використовуються. Колекціонер сміття досить розумний, але не бездоганний. Він може лише визначити, чи використовується щось за допомогою активного посилання на об'єкт чи ні.

Справжня проблема тут полягає в тому, коли внутрішній клас зберігається живим довше, ніж його контейнер. Це пояснюється неявним посиланням на клас, що містить. Єдиний спосіб цього може відбутися, якщо об’єкт поза класом, що містить, зберігає посилання на внутрішній об'єкт, не враховуючи об'єкт, що містить.

Це може призвести до ситуації, коли внутрішній об’єкт живий (за допомогою посилання), але посилання на об'єкт, що міститься, вже видалено з усіх інших об'єктів. Отже, внутрішній об'єкт підтримує живий об'єкт живим, оскільки він завжди матиме посилання на нього. Проблема в цьому полягає в тому, що, якщо він не запрограмований, немає способу повернутися до об'єкта, який міститься, щоб перевірити, чи він ще живий.

Найважливіший аспект цієї реалізації полягає в тому, що це не має значення, чи є воно у діяльності, чи є простим. Ви завжди повинні бути методичними при використанні внутрішніх класів і стежте, щоб вони ніколи не переживали об'єкти контейнера. На щастя, якщо він не є основним об'єктом вашого коду, витоки можуть бути невеликими порівняно. На жаль, це деякі найскладніші витоки, які можна знайти, оскільки вони, ймовірно, залишаться непоміченими, поки багато з них не просочуються.

Рішення: Внутрішні класи

  • Отримайте тимчасові посилання від містить об'єкта.
  • Дозвольте що містить об'єкт єдиним, щоб зберігати довговічні посилання на внутрішні об'єкти.
  • Використовуйте встановлені схеми, такі як Фабрика.
  • Якщо внутрішній клас не потребує доступу до членів класу, що містить, розгляньте його перетворення в статичний клас.
  • Використовуйте обережно, незалежно від того, входить він у діяльність чи ні.

Діяльність та погляди: Вступ

Діяльність містить багато інформації, яку можна запускати та показувати. Діяльність визначається характеристикою того, що вони повинні мати Вигляд. Вони також мають певні автоматичні обробники. Незалежно від того, вказали ви це чи ні, у "Активісті" є неявна посилання на перегляд, який він містить.

Для того, щоб створити вигляд, він повинен знати, де його створити та чи є у нього діти, щоб він міг відображатись. Це означає, що кожен перегляд має посилання на активність (через getContext()). Більше того, кожен погляд зберігає посилання на своїх дітей (тобто getChildAt()). Нарешті, кожен перегляд зберігає посилання на відтворену растрову карту, яка представляє його відображення.

Щоразу, коли у вас є посилання на активність (або контекст діяльності), це означає, що ви можете слідувати ланцюжку ENTIRE вниз по ієрархії макета. Ось чому витоки пам’яті щодо «Діянь» або «Переглядів» - це така велика справа. Це може випустити тону пам’яті всі відразу.

Заходи, види та внутрішні класи

З огляду на вищенаведену інформацію про Внутрішні класи, це найчастіші витоки пам'яті, але також найбільш часто їх уникати. Хоча внутрішній клас бажано мати прямий доступ до членів класу Діяльність, багато хто готовий просто зробити їх статичними, щоб уникнути можливих проблем. Проблема з видами діяльності та переглядами йде набагато глибше.

Витік діяльності, погляди та контекст діяльності

Все зводиться до контексту та життєвого циклу. Існують певні події (наприклад, орієнтація), які вбивають контекст діяльності. Оскільки так багато класів і методів вимагає Контексту, розробники іноді намагатимуться зберегти якийсь код, захопивши посилання на контекст і тримаючись за нього. Так буває, що багато об'єктів, які ми повинні створити для запуску нашої діяльності, повинні існувати поза життєвим циклом діяльності, щоб дозволити діяльності робити те, що їй потрібно робити. Якщо будь-який із ваших об’єктів має посилання на активність, її контекст або будь-який із його переглядів, коли він знищений, ви просто просочили цю активність та все дерево перегляду.

Рішення: Діяльність та погляди

  • За будь-яку ціну уникайте статичних посилань на вигляд або діяльність.
  • Усі посилання на контексти діяльності повинні бути короткочасними (тривалість функції)
  • Якщо вам потрібен довговічний контекст, використовуйте контекст програми ( getBaseContext()або getApplicationContext()). Вони не містять посилань неявно.
  • Крім того, ви можете обмежити знищення діяльності, змінивши конфігураційні зміни. Однак це не заважає іншим потенційним подіям руйнувати Діяльність. Хоча ви можете це зробити, можливо, ви все ще захочете посилатися на вищезазначені практики.

Випускні: Вступ

Бігові майданчики насправді не такі вже й погані. Я маю на увазі, вони могли бути, але насправді ми вже потрапили в більшість небезпечних зон. Runnable - це асинхронна операція, яка виконує завдання, незалежне від потоку, на якому було створено. Більшість застосунків використовуються з потоку інтерфейсу користувача. По суті, використання Runnable - це створення ще однієї нитки, просто трохи більш керованої. Якщо ви класифікуєте Runnable, як стандартний клас, і дотримуєтесь наведених вище вказівок, у вас виникне проблема. Реальність така, що багато розробників цього не роблять.

З-за простоти, читабельності та логічного потоку програми багато розробників використовують Anonymous Inner Clas для визначення своїх Runnables, наприклад, наведеного вище прикладу. Це призводить до такого прикладу, як той, який ви ввели вище. Анонімний внутрішній клас - це в основному дискретний внутрішній клас. Вам просто не потрібно створювати абсолютно нове визначення і просто переосмислювати відповідні методи. В усіх інших відношеннях це Внутрішній клас, що означає, що він зберігає неявну посилання на свій контейнер.

Випускні та заняття / Перегляди

Так! Цей розділ може бути коротким! Через те, що Runnables працює за межами поточної нитки, небезпека при цьому виникає при тривалих асинхронних операціях. Якщо функція, яку можна виконати, визначена в розділі Діяльність або Перегляд як анонімний внутрішній клас АБО вкладений Внутрішній клас, існують деякі дуже серйозні небезпеки. Це відбувається тому , що, як було сказано раніше, він повинен знати , хто його контейнер. Введіть зміну орієнтації (або вбивство системи). Тепер просто зверніться до попередніх розділів, щоб зрозуміти, що саме сталося. Так, ваш приклад досить небезпечний.

Рішення: Виконання

  • Спробуйте розширити Runnable, якщо це не порушує логіку вашого коду.
  • Зробіть все можливе, щоб зробити розширені Runnables статичними, якщо вони повинні бути вкладеними класами.
  • Якщо вам потрібно використовувати Anonymous Runnables, уникайте їх створення в будь-якому об'єкті, який має довговічне посилання на активність або перегляд, який використовується.
  • Багато Runnables так само легко могли бути AsyncTasks. Подумайте про використання AsyncTask, оскільки вони за замовчуванням управляються VM.

Відповідь на остаточне запитання Тепер, щоб відповісти на запитання, на які безпосередньо не зверталися інші розділи цієї публікації. Ви запитали "Коли об'єкт внутрішнього класу може вижити довше, ніж його зовнішній клас?" Перш ніж ми дістанемося до цього, дозвольте мені переосмислити: хоча ви правильно турбуєтесь про це в діяльності, це може спричинити витік в будь-якому місці Я наведіть простий приклад (без використання діяльності) просто для демонстрації.

Нижче наведено загальний приклад базової фабрики (відсутній код).

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

Це не настільки поширений приклад, але досить простий для демонстрації. Ключовим тут є конструктор ...

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

Тепер у нас є витоки, але немає заводу. Незважаючи на те, що ми випустили Фабрику, вона залишиться в пам’яті, оскільки кожен витік має посилання на неї. Навіть не важливо, що зовнішній клас не має даних. Це трапляється набагато частіше, ніж можна було б подумати. Нам не потрібен творець, лише його творіння. Тому ми створюємо його тимчасово, але використовуємо твори нескінченно.

Уявіть, що відбувається, коли ми дещо змінимо конструктор.

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

Тепер кожен з цих нових LeakFactories тільки що просочився. Що ти думаєш про це? Це два дуже поширені приклади того, як внутрішній клас може переживати зовнішній клас будь-якого типу. Якби цей зовнішній клас був Діяльністю, уявіть, наскільки це було б гірше.

Висновок

У них перераховані основні небезпеки небезпечного використання цих об'єктів. Взагалі ця публікація повинна була охоплювати більшість ваших питань, але я розумію, що це публікація в лунгу, тому якщо вам потрібно уточнення, просто дайте мені знати. Поки ви будете дотримуватися вищезазначених практик, ви будете дуже мало турбуватися про витік.


3
Велике спасибі за цю чітку і детальну відповідь. Я просто не розумію, що ви маєте на увазі під "багатьма розробниками, які використовують закриття, щоб визначити свій Runnables"
Sébastien

1
Закриття в Java - це анонімні внутрішні класи, як і описаний вами Runnable. Це спосіб використовувати клас (майже розширити його) без написання визначеного класу, який розширює Runnable. Це називається закриттям, оскільки це "визначення закритого класу" тим, що він має власний закритий простір пам'яті всередині фактично містить об'єкта.
Fuzzical Logic

26
Освічуюча підписка! Одне зауваження щодо термінології: у Java немає такого поняття, як статичний внутрішній клас . ( Документи ). Вкладений клас є або статичним, або внутрішнім , але не може бути обом одночасно.
jenzz

2
Хоча це технічно правильно, Java дозволяє визначати статичні класи всередині статичних класів. Термінологія не на мою користь, а на користь інших, хто не розуміє технічної семантики. Ось чому вперше згадується, що вони "найвищого рівня". Документи розробників Android також використовують цю термінологію, і це для людей, які дивляться на розробку Android, тому я вважав, що краще зберегти послідовність.
Fuzzical Logic

13
Чудовий пост, один з найкращих у StackOverflow, esp для Android.
StackOverflowed
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.