Когда именно безопасно для утечки использовать (анонимные) внутренние классы?
Я прочитал несколько статей об утечках памяти в Android и посмотрел это интересное видео от Google I / O на эту тему.
Тем не менее, я не до конца понимаю концепцию, и особенно когда это безопасно или опасно для пользователя внутренние классы внутри Activity.
Это то, что я понял:
Утечка памяти произойдет, если экземпляр внутреннего класса просуществует дольше, чем его внешний класс (Activity). -> В каких ситуациях это может произойти?
В этом примере, я полагаю, нет риска утечки, потому что расширение анонимного класса никоим образом не OnClickListener
будет жить дольше, чем activity, верно?
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();
}
};
У меня есть сомнения относительно того факта, что понимание этой темы связано с подробным пониманием того, что сохраняется при уничтожении и воссоздании activity.
Так ли это?
Допустим, я просто изменил ориентацию устройства (что является наиболее распространенной причиной утечек). Когда super.onCreate(savedInstanceState)
будет вызван в my onCreate()
, восстановит ли это значения полей (такими, какими они были до изменения ориентации)? Восстановит ли это также состояния внутренних классов?
Я понимаю, что мой вопрос не очень точный, но я был бы очень признателен за любое объяснение, которое могло бы прояснить ситуацию.
Переведено автоматически
Ответ 1
Вы задаете довольно сложный вопрос. Хотя вы можете подумать, что это всего лишь один вопрос, на самом деле вы задаете сразу несколько вопросов. Я сделаю все возможное, зная, что должен рассказать об этом, и, надеюсь, некоторые другие присоединятся, чтобы рассказать о том, что я могу пропустить.
Вложенные классы: введение
Поскольку я не уверен, насколько вам комфортно с ООП в Java, рассмотрим пару основ. Вложенный класс - это когда определение класса содержится внутри другого класса. В основном существует два типа: статические вложенные классы и внутренние классы. Реальная разница между ними заключается в:
- Статические вложенные классы:
- Считаются "высокоуровневыми".
- Не требуется создавать экземпляр содержащего класса.
- Может не ссылаться на содержащие члены класса без явной ссылки.
- Имеют свой собственный срок службы.
- Внутренние вложенные классы:
- Всегда требуется создание экземпляра содержащего класса.
- Автоматически создается неявная ссылка на содержащий экземпляр.
- Может обращаться к членам класса контейнера без ссылки.
- Время жизни должно быть не больше, чем у контейнера.
Сборка мусора и внутренние классы
Сборка мусора выполняется автоматически, но пытается удалить объекты в зависимости от того, считает ли он, что они используются. Сборщик мусора довольно умен, но не безупречен. Он может определить, используется ли что-либо, только по тому, есть ли активная ссылка на объект.
Реальная проблема здесь заключается в том, что внутренний класс поддерживается в рабочем состоянии дольше, чем его контейнер. Это происходит из-за неявной ссылки на содержащий класс. Это может произойти только в том случае, если объект вне содержащего класса сохраняет ссылку на внутренний объект, независимо от содержащего объекта.
Это может привести к ситуации, когда внутренний объект активен (посредством ссылки), но ссылки на содержащий объект уже удалены из всех других объектов. Следовательно, внутренний объект поддерживает работу содержащего объекта, потому что у него всегда будет ссылка на него. Проблема с этим заключается в том, что, если это не запрограммировано, нет способа вернуться к содержащему объекту, чтобы проверить, является ли он вообще живым.
Наиболее важным аспектом этой реализации является то, что не имеет значения, находится ли это в Activity или может быть нарисовано. Вы всегда должны быть методичными при использовании внутренних классов и следить за тем, чтобы они никогда не переживали объекты контейнера. К счастью, если это не основной объект вашего кода, утечки могут быть небольшими по сравнению с этим. К сожалению, это одни из самых сложных утечек, которые можно обнаружить, потому что они, вероятно, останутся незамеченными до тех пор, пока не произойдет утечка многих из них.
Решения: внутренние классы
- Получение временных ссылок из содержащего объекта.
- Allow the containing object to be the only one to keep long-lived references to the inner objects.
- Use established patterns such as the Factory.
- If the inner class does not require access to the containing class members, consider turning it into a static class.
- Use with caution, regardless of whether it is in an Activity or not.
Activities and Views: Introduction
Activities contain a lot of information to be able to run and display. Activities are defined by the characteristic that they must have a View. They also have certain automatic handlers. Whether you specify it or not, the Activity has an implicit reference to the View it contains.
In order for a View to be created, it must know where to create it and whether it has any children so that it can display. This means that every View has an reference to the Activity (via getContext()
). Moreover, every View keeps references to its children (i.e. getChildAt()
). Finally, each View keeps a reference to the rendered Bitmap that represents its display.
Whenever you have a reference to an Activity (or Activity Context), this means that you can follow the ENTIRE chain down the layout hierarchy. This is why memory leaks regarding Activities or Views are such a huge deal. It can be a ton of memory being leaked all at once.
Activities, Views and Inner Classes
Given the information above about Inner Classes, these are the most common memory leaks, but also the most commonly avoided. While it is desirable to have an inner class have direct access to an Activities class members, many are willing to just make them static to avoid potential issues. The problem with Activities and Views goes much deeper than that.
Leaked Activities, Views and Activity Contexts
Все сводится к контексту и жизненному циклу. Существуют определенные события (например, ориентация), которые прерывают контекст действия. Поскольку для очень многих классов и методов требуется контекст, разработчики иногда пытаются сохранить часть кода, беря ссылку на контекст и удерживая ее. Так уж получилось, что многие объекты, которые мы должны создать для выполнения нашего Activity, должны существовать вне жизненного цикла Activity, чтобы позволить Activity выполнять то, что ей нужно. Если какой-либо из ваших объектов случайно содержит ссылку на действие, его контекст или любое из его представлений при его уничтожении, вы только что слили это действие и все его дерево представлений.
Решения: действия и представления
- Любой ценой избегайте создания статической ссылки на представление или действие.
- Все ссылки на контексты активности должны быть недолговечными (продолжительность функции)
- Если вам нужен долговечный контекст, используйте контекст приложения (
getBaseContext()
илиgetApplicationContext()
). Они не сохраняют ссылки неявно. - В качестве альтернативы вы можете ограничить уничтожение действия, переопределив изменения конфигурации. Однако это не останавливает другие потенциальные события от уничтожения действия. Хотя вы можете это сделать, вы все равно можете захотеть обратиться к приведенным выше методам.
Runnables: Введение
Runnables на самом деле не так уж плохи. Я имею в виду, что они могли бы быть такими, но на самом деле мы уже попали в большинство опасных зон. Runnable - это асинхронная операция, которая выполняет задачу независимо от потока, в котором она была создана. Большинство runnables создаются из потока пользовательского интерфейса. По сути, использование Runnable создает другой поток, только немного более управляемый. Если вы классифицируете Runnable как стандартный класс и следуете приведенным выше рекомендациям, вы должны столкнуться с несколькими проблемами. Реальность такова, что многие разработчики этого не делают.
Из-за простоты, удобочитаемости и логичности работы программы многие разработчики используют анонимные внутренние классы для определения своих исполняемых файлов, таких как приведенный выше пример. В результате получается пример, подобный тому, который вы ввели выше. Анонимный внутренний класс - это, по сути, отдельный внутренний класс. Вам просто не нужно создавать совершенно новое определение и просто переопределять соответствующие методы. Во всех других отношениях это внутренний класс, что означает, что он сохраняет неявную ссылку на свой контейнер.
Выполняемые файлы и действия / представления
Ура! Этот раздел может быть коротким! Из-за того, что Runnables выполняются вне текущего потока, опасность с ними связана с длительными асинхронными операциями. Если runnable определен в Activity или представлении как анонимный внутренний класс ИЛИ вложенный внутренний класс, возникает ряд очень серьезных опасностей. Это потому, что, как указывалось ранее, он должен знать, кто является его контейнером. Введите изменение ориентации (или уничтожение системы). Теперь просто вернитесь к предыдущим разделам, чтобы понять, что только что произошло. Да, ваш пример довольно опасен.
Решения: Runnables
- Попробуйте расширить Runnable, если это не нарушает логику вашего кода.
- Сделайте все возможное, чтобы расширенные Runnables были статическими, если они должны быть вложенными классами.
- Если вам необходимо использовать анонимные исполняемые файлы, избегайте их создания в любом объекте, который имеет долговременную ссылку на действие или представление, которое используется.
- Многие исполняемые файлы с таким же успехом могли быть асинхронными задачами. Рассмотрите возможность использования AsyncTask, поскольку по умолчанию они управляются виртуальной машиной.
Отвечаю на последний вопрос Теперь, чтобы ответить на вопросы, которые не были непосредственно затронуты в других разделах этого поста. Вы спросили: "Когда объект внутреннего класса может просуществовать дольше, чем его внешний класс?" Прежде чем мы перейдем к этому, позвольте мне еще раз подчеркнуть: хотя вы правы, беспокоясь об этом в Activities, это может вызвать утечку где угодно. Я приведу простой пример (без использования 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 только что были обнародованы. Что вы об этом думаете? Это два очень распространенных примера того, как внутренний класс может пережить внешний класс любого типа. Если бы этот внешний класс был Activity, представьте, насколько хуже это было бы.
Заключение
Здесь перечислены основные известные опасности ненадлежащего использования этих объектов. В общем, этот пост должен был охватить большинство ваших вопросов, но я понимаю, что это был длинный пост, поэтому, если вам нужны разъяснения, просто дайте мне знать. Пока вы будете следовать описанным выше методам, вы будете очень мало беспокоиться об утечке.
Ответ 2
У вас 2 вопроса в 1 сообщении:
- Никогда не бывает безопасно использовать внутренний класс, не объявив его как
static
. Это не ограничивается только Android, но применимо ко всему миру Java.
Более подробное объяснение здесь
Примерами распространенных внутренних классов для проверки, используете ли вы static class InnerAdapter
или только class InnerAdapter
, являются списки (ListView
или RecyclerView
, tab + page layout (ViewPager
), раскрывающийся список и подклассы AsyncTask
- Не имеет значения, используете ли вы Handler + Runnable, AsyncTask, RxJava или что-либо еще, если операция завершится после уничтожения Activity / Фрагмента / представления, вы создадите новую ссылку на объект Activity / Fragment / View (которые огромны), который не может собирать мусор (слоты памяти, которые нельзя освободить).
Поэтому обязательно отмените эти длительные задачи в onDestroy()
или более ранней версии, и утечки памяти не произойдет
Ответ 3
Пока вы знаете, что ваши внутренние (анонимные) классы имеют более короткий или точно такой же жизненный цикл, как у внешнего класса, вы можете безопасно их использовать.
Например, вы используете setOnClickListener()
для кнопок Android, большую часть времени вы используете анонимный класс, потому что нет другого объекта, содержащего ссылку на него, и вы не будете выполнять какой-то длительный процесс внутри прослушивателя. Как только внешний класс уничтожен, внутренний класс также может быть уничтожен.
Another example has memory leak issue is Android LocationCallback
as blow example.
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();
}
}
Now not only Activity holds the reference of LocationCallback, Android GMS service also holds it. GMS service has much longer lifecycle than Activity. It will cause memory leak to the activity.
More details are explained here.