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

Я читал несколько статей об утечках памяти в 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();
    }
};

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

Is it?

Допустим, я только что изменил ориентацию устройства (что является наиболее частой причиной утечек). Когда super.onCreate(savedInstanceState) будет вызываться в моем onCreate(), восстановит ли это значения полей (как они были до изменения ориентации)? Будет ли это также восстанавливать состояния внутренних классов?

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


person Sébastien    schedule 02.06.2012    source источник


Ответы (3)


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

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

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

  • Static Nested Classes:
    • Are considered "top-level".
    • Не требует создания экземпляра содержащего класса.
    • Не может ссылаться на содержащиеся члены класса без явной ссылки.
    • Имейте свою жизнь.
  • Inner Nested Classes:
    • Always require an instance of the containing class to be constructed.
    • Автоматически иметь неявную ссылку на содержащий экземпляр.
    • Может получить доступ к членам класса контейнера без ссылки.
    • Срок службы предполагается не должен превышать срок службы контейнера.

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

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

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

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

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

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

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

Действия и просмотры: введение

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

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

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

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

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

Просочившиеся действия, просмотры и контексты действий

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

Решения: действия и просмотры

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

Runnables: введение

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

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

Выполняемые файлы и действия / просмотры

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

Решения: исполняемые файлы

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

Ответ на последний вопрос. Теперь ответим на вопросы, которые не были напрямую рассмотрены в других разделах этого сообщения. Вы спросили: «Когда объект внутреннего класса может прожить дольше, чем его внешний класс?» Прежде чем мы перейдем к этому, позвольте мне еще раз подчеркнуть: хотя вы правы, что беспокоитесь об этом в действиях, это может вызвать утечку где угодно. Я приведу простой пример (без использования 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...
    }
}

Теперь у нас есть утечки, но нет Factory. Несмотря на то, что мы выпустили Factory, она останется в памяти, потому что каждая утечка имеет на нее ссылку. Даже неважно, что у внешнего класса нет данных. Это происходит гораздо чаще, чем можно подумать. Нам не нужен создатель, нам нужны только его творения. Итак, мы создаем его временно, но используем творения бесконечно.

Представьте, что произойдет, если мы немного изменим конструктор.

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

Заключение

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

person Community    schedule 10.06.2012
comment
Большое спасибо за четкий и подробный ответ. Я просто не понимаю, что вы имеете в виду, говоря, что многие разработчики используют замыкания для определения своих Runnables. - person Sébastien; 10.06.2012
comment
Замыкания в Java - это анонимные внутренние классы, подобные описываемому вами Runnable. Это способ использовать класс (почти расширить его) без написания определенного класса, расширяющего Runnable. Это называется закрытием, потому что это определение закрытого класса в том смысле, что у него есть собственное закрытое пространство памяти внутри фактического содержащего объекта. - person Fuzzical Logic; 11.06.2012
comment
Отредактированный ответ, чтобы удалить ссылку на закрытие для ясности. - person Fuzzical Logic; 11.06.2012
comment
Поучительная статья! Одно замечание относительно терминологии: в Java нет такой вещи, как статический внутренний класс. (Документы ). Вложенный класс может быть статическим или внутренним, но не может быть и тем, и другим одновременно. - person jenzz; 16.06.2013
comment
Хотя это технически правильно, Java позволяет вам определять статические классы внутри статических классов. Терминология предназначена не для меня, а для тех, кто не понимает технической семантики. Поэтому в первую очередь упоминается, что они на высшем уровне. В документации для разработчиков Android также используется эта терминология, и она предназначена для людей, интересующихся разработкой Android, поэтому я подумал, что лучше сохранить последовательность. - person Fuzzical Logic; 20.06.2013
comment
Отличный пост, один из лучших на StackOverflow, особенно для Android. - person StackOverflowed; 22.07.2013
comment
Классный пост. Не могли бы вы объяснить, что Многие Runnables с таким же успехом могли быть AsyncTasks. Рассмотрите возможность использования AsyncTask, поскольку по умолчанию они управляются виртуальными машинами.? Почему AsyncTask может решить проблему? Есть пример. Спасибо - person jordan; 05.09.2013
comment
AsyncTask - это асинхронная операция (обычно недолговечная). Это выходит за рамки этого вопроса, поэтому было бы лучше, если бы вы провели некоторое исследование AsyncTask и того, как его использовать. - person Fuzzical Logic; 12.10.2013
comment
Замечательный ответ, приятель! Эти строчки открыли мне глаза;) - person Diego Palomar; 14.01.2015
comment
Спасибо за этот ответ. Очень хорошо объяснено! - person Darshan Dorai; 04.07.2015
comment
Я не согласен с одним моментом, если создается поток, который выполняет только некоторую фоновую задачу, он никогда не будет пропускать ссылку на активность, он будет утечкой только своей ссылки, поскольку он не обновляет какой-либо пользовательский интерфейс и никоим образом не удерживает Looper - person user1530779; 07.07.2015
comment
Почему asynctask вызывает утечку памяти, потому что в ней есть ссылка на Looper - person user1530779; 07.07.2015
comment
Если фоновая задача запрограммирована правильно, вы правы. Если это не так, то, безусловно, может произойти утечка ссылок на пользовательский интерфейс, даже если он не обновляет пользовательский интерфейс. Этот конкретный момент прямо рассматривается даже в документе, предоставленном ОП. - person Fuzzical Logic; 07.07.2015
comment
Что вы имеете в виду, у AsyncTask есть ссылка на Looper? AsyncTasks не хранит такой ссылки. Их можно запускать только один раз для каждой задачи. Впоследствии их нужно выбросить. Фактически, это предпочтительный метод из многих источников в течение многих лет для выполнения очень краткосрочных задач. - person Fuzzical Logic; 07.07.2015
comment
@FuzzicalLogic, я предполагаю, что утечка также произойдет, если внешний класс содержит ссылки на экземпляры своего внутреннего класса? потребуется выполнить некоторую очистку, прежде чем внешний класс потеряет все ссылки на него, как я предполагаю. Например, внутренний класс адаптера списка и внешний класс, содержащий ссылку на экземпляр адаптера (в этом случае происходит утечка как внешнего, так и внутреннего классов из-за цикла ссылок) - person Fonix; 16.12.2015
comment
@Fonix Это зависит от того, говорите ли вы о самом внешнем классе или об экземпляре внешнего класса. В первом случае это потенциально может привести к утечке, но большинство сборщиков мусора решат проблему после завершения работы приложения. В последнем случае утечки не будут создаваться, если только во внутреннем классе не было нарушений. Учитывая конкретный вопрос, я не уверен, стоит ли рассматривать его в этом посте. - person Fuzzical Logic; 27.12.2015
comment
Многие Runnables с такой же легкостью могли бы быть AsyncTasks. Рассмотрите возможность использования AsyncTask, поскольку по умолчанию они управляются виртуальными машинами. Но если у нас есть внутренний нестатический AsyncTask, возникнут те же проблемы, что и Runnable. Я прав? - person q126y; 24.07.2016
comment
В то время, когда это было написано, AsyncTask управлялся самостоятельно (т. Е. GC'd). Таким образом, он избежал большинства проблем, связанных с неправильным использованием Runnable. AFAIK, это не изменилось. - person Fuzzical Logic; 24.07.2016
comment
Отличный ответ .. !! - person Ritesh; 27.11.2017
comment
Лучший ответ на свете: D - person nhp; 31.01.2018
comment
«Статический внутренний» - это противоречие в терминах, поэтому «нестатическое внутреннее» является тавтологией. Терминология здесь совершенно неверная. Пожалуйста, исправьте. - person user207421; 05.03.2018
comment
@FuzzicalLogic, спасибо за сообщение, можно вопрос? Я думаю, что когда нет ссылки на экземпляр SwissCheese после того, как он выполнил свою работу, Factory сможет быть собран, и утечка памяти больше не будет проблемой, я прав? - person LiuWenbin_NO.; 30.03.2018
comment
Автор перепутал термины вложенный, внутренний и статический. Я обновил текст, чтобы использовать термины в соответствии с Руководство по Java и другие ответы на Stack Overflow. Я думаю, что оставлять их такими, какими они были, вызывает большую путаницу в сложной теме, а автор, кажется, оставил Stack Overflow. - person Lii; 30.09.2018
comment
Спасибо всем за приятные комментарии, а также за долгосрочную поддержку и очистку ответа. @Lii: Спасибо за исправление условий, так как документы по Android исправили их немного назад, и они больше не нужны специально для новичков в Android. Вероятно, сейчас это должна быть вики, поэтому я собираюсь переместить ее туда. - person Fuzzical Logic; 02.02.2019
comment
@FuzzicalLogic, Java не имеет закрытий. У него есть лямбды, большая разница. И имя не происходит от определения закрытого класса, поскольку у него есть собственное закрытое пространство памяти внутри фактического содержащего объекта. В отличие от лямбда-выражений, замыкания могут изменять окружающие переменные. Например. как в Groovy или Scala. Извините за придирки: D - person Angel O'Sphere; 04.10.2019
comment
@ AngelO'Sphere Это вики уже несколько месяцев. Как первоначально указывалось, терминология, использованная в ответе, была использована на основе неточного использования в исходных документах и ​​руководствах по Android, поскольку это ответ, ориентированный на Android. Поскольку ответ был изначально написан, Google обновил свои документы / руководства, чтобы они больше соответствовали определениям Java. И поэтому многие другие люди соответствующим образом редактировали и исправляли терминологию. Они будут продолжать это делать, поскольку я уже давно перешел от этого ответа. Имейте в виду, что этому ответу более 7 лет, IIRC - person Fuzzical Logic; 06.10.2019
comment
Да, я понимаю, поэтому я извиняюсь за свои придирки, хе-хе. Ваш пример, кстати. довольно забавно: D - person Angel O'Sphere; 07.10.2019
comment
Спасибо за подробный ответ. Зная, что действия можно убивать и воссоздавать в случайном порядке, это предупреждение, наконец, имеет для меня смысл. - person Romuald Brunet; 24.01.2020
comment
Спасибо. Не могли бы вы добавить пример, в котором внутренний вложенный класс будет работать без утечки памяти? Прочитав этот ответ, я боюсь использовать какой-либо внутренний вложенный класс. - person vainquit; 05.02.2021

У вас 2 вопроса в 1 посте:

  1. Небезопасно использовать внутренний класс, не объявив его как static. Это не ограничивается только Android, но применимо ко всему миру Java.

Более подробное объяснение здесь

Примеры общих внутренних классов для проверки того, используете ли вы static class InnerAdapter или просто class InnerAdapter, - это списки (ListView или RecyclerView, вкладка + макет страницы (ViewPager), раскрывающийся список и подклассы AsyncTask

  1. Не имеет значения, используете ли вы Handler + Runnable, AsyncTask, RxJava или что-то еще, если операция завершится после уничтожения Activity / Fragment / View, вы создадите ссылку на объект Activity / Fragment / View (которые являются огромный), которые не могут быть собраны мусором (слоты памяти, которые нельзя освободить)

Поэтому не забудьте отменить эти длительные задачи в onDestroy() или ранее, и утечки памяти не будет.

person ericn    schedule 01.09.2020

Если вы знаете, что ваши внутренние (анонимные) классы имеют более короткий или точно такой же жизненный цикл, что и внешний класс, вы можете безопасно использовать их.

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

введите описание изображения здесь

Другой пример, связанный с утечкой памяти, - это Android LocationCallback в качестве примера взрыва.

public class MainActivity extends AppCompatActivity {

  @Override
  protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.activity_main);
    initLocationLibraries();
  }

  private void initLocationLibraries() {
    mFusedLocationClient = LocationServices.getFusedLocationProviderClient(this);
    mSettingsClient = LocationServices.getSettingsClient(this);

    mLocationCallback = new LocationCallback() {
        @Override
        public void onLocationResult(LocationResult locationResult) {
            super.onLocationResult(locationResult);
            // location is received
            mCurrentLocation = locationResult.getLastLocation();
            updateLocationUI();
        }
    };

    mRequestingLocationUpdates = false;

    mLocationRequest = new LocationRequest();
    mLocationRequest.setInterval(UPDATE_INTERVAL_IN_MILLISECONDS);
    mLocationRequest.setFastestInterval(FASTEST_UPDATE_INTERVAL_IN_MILLISECONDS);
    mLocationRequest.setPriority(LocationRequest.PRIORITY_HIGH_ACCURACY);

    LocationSettingsRequest.Builder builder = new LocationSettingsRequest.Builder();
    builder.addLocationRequest(mLocationRequest);
    mLocationSettingsRequest = builder.build();
  }
}

Теперь не только Activity имеет ссылку на LocationCallback, она также есть в службе Android GMS. Сервис GMS имеет гораздо более длительный жизненный цикл, чем Activity. Это вызовет утечку памяти для активности. введите описание изображения здесь

Более подробная информация представлена ​​здесь .

person Weidian Huang    schedule 21.09.2020