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

324

Я читал некоторые статьи об утечках памяти в Android и смотрел это интересное видео из Google I / O на эту тему .

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

Вот что я понял:

Утечка памяти произойдет, если экземпляр внутреннего класса выживет дольше, чем его внешний класс (Activity). -> В каких ситуациях это может произойти?

В этом примере я предполагаю, что нет риска утечки, потому что нет никакого способа, которым расширение анонимного класса 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)будет вызвано в my onCreate(), это восстановит значения полей (как они были до изменения ориентации)? Восстановит ли это также состояния внутренних классов?

Я понимаю, что мой вопрос не очень точный, но я очень ценю любое объяснение, которое могло бы прояснить ситуацию.

Себастьен
источник
14
Этот пост и этот пост содержат полезную информацию об утечках памяти и внутренних классах. :)
Алекс Локвуд

Ответы:

651

То, что вы спрашиваете, довольно сложный вопрос. Хотя вы можете думать, что это всего лишь один вопрос, вы на самом деле задаете несколько вопросов одновременно. Я приложу все усилия, зная, что я должен покрыть это, и, надеюсь, некоторые другие присоединятся, чтобы покрыть то, что я могу пропустить.

Вложенные классы: введение

Поскольку я не уверен, насколько вам удобно с ООП в Java, это затронет несколько основ. Вложенный класс - это когда определение класса содержится в другом классе. Существует два основных типа: статические вложенные классы и внутренние классы. Реальная разница между ними:

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

Сборка мусора и внутренние классы

Сборка мусора происходит автоматически, но пытается удалить объекты в зависимости от того, считает ли она, что они используются. Сборщик мусора довольно умен, но не безупречен. Он может только определить, используется ли что-то, существует ли активная ссылка на объект.

Реальная проблема здесь в том, что внутренний класс остается живым дольше, чем его контейнер. Это из-за неявной ссылки на содержащий класс. Единственный способ, которым это может произойти, - это если объект за пределами содержащего класса сохраняет ссылку на внутренний объект, безотносительно к содержащемуся объекту.

Это может привести к ситуации, когда внутренний объект жив (посредством ссылки), но ссылки на содержащий объект уже удалены из всех других объектов. Внутренний объект, следовательно, поддерживает вмещающий объект, потому что он всегда будет иметь ссылку на него. Проблема заключается в том, что, если он не запрограммирован, невозможно вернуться к содержащемуся объекту, чтобы проверить, жив ли он.

Наиболее важным аспектом этой реализации является то, что не имеет значения, находится ли она в Деятельности или является доступной для рисования. Вам всегда нужно быть методичным при использовании внутренних классов и следить за тем, чтобы они никогда не переживали объекты контейнера. К счастью, если это не основной объект вашего кода, утечки могут быть небольшими по сравнению. К сожалению, это некоторые из самых трудных утечек, которые можно найти, потому что они, вероятно, останутся незамеченными, пока многие из них не протекут.

Решения: внутренние классы

  • Получить временные ссылки из содержащего объекта.
  • Разрешить содержащему объекту быть единственным, чтобы хранить долгоживущие ссылки на внутренние объекты.
  • Используйте установленные шаблоны, такие как Фабрика.
  • Если внутренний класс не требует доступа к содержащим его членам класса, рассмотрите возможность превращения его в статический класс.
  • Используйте с осторожностью, независимо от того, находится ли он в действии или нет.

Деятельность и взгляды: Введение

Действия содержат много информации, чтобы иметь возможность запускать и отображать. Действия определяются характеристикой, которая должна иметь представление. У них также есть определенные автоматические обработчики. Независимо от того, указали вы это или нет, у Activity есть неявная ссылка на содержащееся в ней представление.

Чтобы создать представление, оно должно знать, где его создать и есть ли у него дочерние элементы, чтобы его можно было отобразить. Это означает, что каждое представление имеет ссылку на действие (через getContext()). Более того, каждый View хранит ссылки на своих потомков (то есть getChildAt()). Наконец, каждое представление хранит ссылку на отображаемое растровое изображение, которое представляет его отображение.

Всякий раз, когда у вас есть ссылка на действие (или контекст действия), это означает, что вы можете следовать всей цепочке вниз по иерархии макета. Вот почему утечки памяти в отношении действий или представлений так важны. Это может быть тонна утечки памяти за один раз.

Деятельность, представления и внутренние классы

Учитывая приведенную выше информацию о внутренних классах, это наиболее распространенные утечки памяти, но их также чаще всего избегают. Хотя желательно, чтобы внутренний класс имел прямой доступ к членам класса «Деятельности», многие хотят просто сделать их статичными, чтобы избежать потенциальных проблем. Проблема с деятельностью и представлениями гораздо глубже.

Утечка активности, просмотров и контекстов активности

Все сводится к контексту и жизненному циклу. Существуют определенные события (например, ориентация), которые убивают контекст действия. Так как для многих классов и методов требуется контекст, разработчики иногда пытаются сохранить некоторый код, захватывая ссылку на контекст и удерживая его. Просто так получилось, что многие объекты, которые мы должны создать, чтобы запустить наше действие, должны существовать вне жизненного цикла действия, чтобы позволить действию делать то, что ему нужно. Если какой-либо из ваших объектов, когда он уничтожен, имеет ссылку на Activity, ее Context или какой-либо из его Views, вы только что слили эту Activity и все дерево View.

Решения: деятельность и взгляды

  • Любой ценой избегайте статических ссылок на представление или действие.
  • Все ссылки на контексты деятельности должны быть кратковременными (продолжительность функции)
  • Если вам нужен долгоживущий контекст, используйте контекст приложения ( getBaseContext()или getApplicationContext()). Они не хранят ссылки неявно.
  • Кроме того, вы можете ограничить уничтожение Действия, переопределив Изменения конфигурации. Однако это не мешает другим потенциальным событиям разрушить Деятельность. Хотя вы можете сделать это, вы все равно можете обратиться к вышеупомянутым практикам.

Runnables: Введение

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

Из-за простоты, удобочитаемости и логического выполнения программы многие разработчики используют анонимные внутренние классы для определения своих Runnables, как, например, в примере, который вы создали выше. Это приводит к примеру, подобному тому, который вы напечатали выше. Анонимный внутренний класс - это в основном дискретный внутренний класс. Вам просто не нужно создавать совершенно новое определение и просто переопределять соответствующие методы. Во всех других отношениях это внутренний класс, что означает, что он хранит неявную ссылку на свой контейнер.

Runnables и Действия / Представления

Ура! Этот раздел может быть коротким! Из-за того, что Runnables работают вне текущего потока, опасность с ними связана с длительными асинхронными операциями. Если выполняемый объект определен в Деятельности или Представлении как Анонимный Внутренний Класс ИЛИ вложенный Внутренний Класс, существуют некоторые очень серьезные опасности. Это потому, что, как уже говорилось, он должен знать, кто его контейнер. Введите изменение ориентации (или уничтожение системы). Теперь просто вернитесь к предыдущим разделам, чтобы понять, что только что произошло. Да, ваш пример довольно опасен.

Решения: Runnables

  • Попробуйте расширить Runnable, если он не нарушает логику вашего кода.
  • Сделайте все возможное, чтобы сделать расширенные Runnables статичными, если они должны быть вложенными классами.
  • Если вам необходимо использовать Anonymous Runnables, избегайте их создания в любом объекте, который имеет долговременную ссылку на используемое действие или представление.
  • Многие Runnables также легко могли быть AsyncTasks. Подумайте об использовании AsyncTask, поскольку по умолчанию это VM Managed.

Ответ на последний вопрос Теперь ответим на вопросы, которые не были напрямую рассмотрены в других разделах этого поста. Вы спросили: «Когда объект внутреннего класса может выжить дольше, чем его внешний класс?» Прежде чем мы вернемся к этому, позвольте мне еще раз подчеркнуть: хотя вы и вправе беспокоиться об этом в разделе «Действия», это может привести к утечке в любом месте. Я приведу простой пример (без использования Activity) просто для демонстрации.

Ниже приведен типичный пример базовой фабрики (отсутствует код).

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 только что просочился. Что вы думаете об этом? Это два очень распространенных примера того, как внутренний класс может пережить внешний класс любого типа. Если бы этот внешний класс был Деятельностью, представьте, насколько хуже было бы.

Вывод

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

Fuzzical Logic
источник
3
Большое спасибо за этот четкий и подробный ответ. Я просто не понимаю, что вы имеете в виду, когда «многие разработчики используют замыкания для определения своих Runnables»
Себастьян
1
Замыкания в Java - это анонимные внутренние классы, такие как Runnable, который вы описываете. Это способ использовать класс (почти расширить его) без написания определенного класса, расширяющего Runnable. Это называется закрытием, потому что это «определение закрытого класса» в том смысле, что оно имеет свое собственное закрытое пространство памяти внутри фактического содержащего объекта.
Fuzzical Logic
26
Восхитительная рецензия! Одно замечание относительно терминологии: в Java нет такого понятия, как статический внутренний класс . ( Документы ). Вложенный класс является статическим или внутренним , но не может быть одновременно одновременно.
Дженс
2
Хотя это технически правильно, Java позволяет вам определять статические классы внутри статических классов. Терминология не в мою пользу, а в пользу тех, кто не понимает техническую семантику. Вот почему впервые упоминается, что они «верхнего уровня». Документы для разработчиков Android также используют эту терминологию, и она предназначена для людей, которые смотрят на разработку Android, поэтому я подумал, что лучше придерживаться последовательности.
Нечеткая логика
13
Отличный пост, один из лучших в StackOverflow, особенно для Android.
Переполнение