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