Sebbia
Компания
26,26
рейтинг
18 ноября 2014 в 16:10

Разработка → Что такое утечки памяти в android, как проверить программу на их отсутствие и как предотвратить их появление tutorial

В этой статье для начинающих android-разработчиков я постараюсь рассказать о том, что такое «утечки памяти» в android, почему о них стоит думать на современных устройствах, выделяющих по 192МБ на приложение, как быстро найти и устранить эти утечки в малознакомом приложении и на что нужно обращать особое внимание при разработке любого приложения.


Конечная цель этой статьи — ответ на простой вопрос:
Куда нажать, чтобы узнать, какую строчку в приложении поправить?

Что такое «утечка памяти»?


Начнем с того, что же называется «утечкой памяти». В строгом понимании объект можно назвать утечкой памяти, если он продолжает существовать в памяти даже после того, как на него потеряны все ссылки. С этим определением сразу же возникает проблема: память для всех объектов, которые вы создаете, выделяется при участии сборщика мусора, и все созданные объекты сборщик мусора помнит, независимо от того, есть у вас ссылка на объект, или нет.

На самом деле сборщик мусора устроен крайне примитивно (на самом деле нет — но принцип работы действительно простой): есть граф, в котором каждый существующий объект — это вершина, а ссылка от любого объекта на любой другой объект — ребро. Некоторые вершины на этом графе — особые. Это корни сборщика мусора (garbage collection roots) — те сущности, которые созданы системой и продолжают свое существование независимо от того, ссылаются на них другие объекты или нет. Если и только если на графе существует любой путь от данного объекта до любого корня, объект не будет уничтожен сборщиком мусора.

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

Попытки получить в java «чистую» утечку памяти предпринимались неоднократно и, безусловно, продолжают предприниматься, однако ни один из способов не способен заставить сборщика мусора забыть ссылку на объект, не освободив память. Существуют утечки памяти, связанные с выделением памяти нативным кодом (JNI), однако в этой статье мы их не будем рассматривать.

Вывод: вы можете потерять все ссылки на интересующий вас объект, но сборщик мусора помнит.

Итак, определение «утечки памяти» в строгом смысле нам не подходит. Поэтому далее будем понимать утечку памяти как объект, который продолжает существовать после того, как он должен быть уничтожен.

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

Но, прежде чем перейти к описанию этих частых ошибок, нужно ответить на главный вопрос: а нужно ли вообще исправлять эти ошибки? Приложение-то работает...

Почему нужно тратить время на устранение утечек памяти?



Приложения уже давно не падают из-за того, что вы забыли пережать ресурсы в папку drawable-ldpi. Готовясь к написанию этой статьи, я провел простой эксперимент: я взял одно из работающих приложений, и добавил в него утечку памяти таким образом, что ни одно создаваемое activity никогда не выгружалось из памяти (стал добавлять их в статический список). Я открыл приложение и начал прокликивать экраны, ожидая, когда же приложение наконец упадет на моем Nexus 5. Наконец, через 5 минут и 55 экранов, приложение упало. Ирония в том, что, по данным Google Analytics, обычно пользователь за сессию посещает 3 экрана.

Так нужно ли волноваться по поводу утечек памяти, если пользователь может их просто не заметить? Да, и есть три причины почему.

Во-первых, если в вашем приложении работает что-то, что работать не должно, это может привести к очень серьёзным и трудно отлаживаемым проблемам.

Например, вы разработали приложение для социальной сети. В этом приложении можно обмениваться сообщениями между пользователями, где на экране обмена сообщениями есть таймер, который делает запрос на сервер каждые 10 секунд с целью получения новых сообщений, но вы забыли этот таймер выключить при выходе с экрана. К чему это приведет визуально? Да ни к чему. Вы не заметите, что приложение делает что-то не то. Но при этом приложение продолжит каждые 10 секунд посылать запрос на сервер. Даже после того, как вы выйдете из приложения. Даже после того, как вы выключите экран (поведение может варьироваться от телефона). Если пользователь зайдет на экраны общения с тремя разными друзьями, в течение часа вы получите 1000 лишних запросов на сервер и одного пользователя, очень рассерженного на ваше приложение, которое усиленно потребляет батарею. Именно такие результаты я получил с тестовым приложением на телефоне с выключенным экраном.

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

Во-вторых, не все приложения потребляют мало памяти, и не все телефоны выделяют много памяти.

Помните про приложение, которое упало только после 5 минут и 55 не выгруженных экранов? Так вот для этого же приложения мне каждую неделю приходит 1-2 отчета о падении с OutOfMemoryException (в основном с устройств до 4.0; у приложения 50.000 установок). И это при том, что утечек памяти в приложении нет. Поэтому даже сейчас вы можете изрядно подпортить себе карму, выложив приложение с утечками памяти, особенно если ваше приложение потребляет много памяти. Как обычно в мире android, от блестящего будущего нас отделяет суровое настоящее.

В-третьих, мужик должен всё уметь! (я же обещал, что все 3 причины будут серьёзные)

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

Никогда не сохраняйте ссылки на activity (view, fragment, service) в статических переменных


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

Почему же утечка activity — такая большая проблема? Дело в том, что если сборщик мусора не соберет activity, то он не соберет и все view и fragment, а вместе с ними и все прочие объекты, расположенные на activity. В том числе не будут высвобождены картинки. Поэтому утечка любого activity — это, как правило, самая большая утечка памяти, которая может быть в вашем приложении.

Никогда не записывайте ссылки на activity в статические переменные. Используйте передачу объектов через Intent, либо вообще передавайте не объект, а id объекта (если у вас есть база данных, из которой этот id потом можно достать).

Этот пункт также относится к любым объектам, временем жизни которых напрямую или косвенно управляет android. Т.е. к view, fragment, service и т.д..

View и fragment объекты содержат ссылку на activity, в котором они расположены, поэтому, если утечет один единственный view, утечет сразу всё — activity и все view в нём, а, вместе с ними, и все drawable и всё, на что у любого элемента из экрана есть ссылка!

Будьте аккуратны при передаче ссылки на activity (view, fragment, service) в другие объекты


Рассмотрим простой пример: ваше приложение для социальной сети отображает фамилию, имя и рейтинг текущего пользователя на каждом экране приложения. Объект с профилем текущего пользователя существует с момента входа в аккаунт до момента выхода из него, и все экраны вашего приложения обращаются за информацией к одному и тому же объекту. Этот объект также периодически обновляет данные с сервера, так как рейтинг может часто меняться. Необходимо, чтобы объект с профилем уведомлял текущее activity об обновлении рейтинга. Как этого добиться? Очень просто:

@Override
protected void onResume() {
	super.onResume();
	currentUser.addOnUserUpdateListener(this);
}

Как добиться в этой ситуации утечки памяти? Тоже очень несложно! Просто забудьте отписаться от уведомлений в методе onPause:

@Override
protected void onPause() {
	super.onPause();
	/* Забудьте про следующую строчку и вы получите серьёзную утечку памяти */
	currentUser.removeOnUserUpdateListener(this);
}

Из-за такой утечки памяти activity будет продолжать обновлять интерфейс каждый раз, когда профиль будет обновляться даже после того, как экран перестанет быть видим пользователю. Хуже того, таким образом экран может подписать 2, 3 или больше раза на одно и то же уведомление. Это может привести к видимым тормозам интерфейса в момент обновления профиля — и не только на этом экране.

Что делать, чтобы избежать этой ошибки?

Во-первых, конечно нужно всегда внимательно следить за тем, что вы отписались от всех уведомлений в момент ухода activity в фон.

Во-вторых, вы должны периодически проверять своё приложение на наличие утечек памяти.

В-третьих, есть и альтернативный подход к проблеме: вы можете сохранять не ссылки на объекты, а слабые ссылки. Это особенно полезно для наследников класса View — ведь у них нет метода onPause и не совсем понятно, в какой момент они должны отписываться от уведомления. Слабые ссылки не считаются сборщиком мусора как связи между объектами, поэтому объект, на который существуют только слабые ссылки, будет уничтожен, а ссылка перестанет ссылаться на объект и примет значение null. Чтобы не возиться каждый раз с не очень удобными в использовании слабыми ссылками, вы можете воспользоваться примерно следующим шаблонным классом:

public class Observer<I> {
	
	private ArrayList<I> strongListeners = new ArrayList<I>();
	private ArrayList<WeakReference<I>> weakListeners = new ArrayList<WeakReference<I>>();
	
	public void addStrongListener(I listener) {
		strongListeners.add(listener);
	}

	public void addWeakListener(I listener) {
		weakListeners.add(new WeakReference<I>(listener));
	}
	
	public void removeListener(I listener) {
		strongListeners.remove(listener);
		for (int i = 0; i < weakListeners.size(); ++i) {
			WeakReference<I> ref = weakListeners.get(i);
			if (ref.get() == null || ref.get() == listener) {
				weakListeners.remove(i--);
			}
		}
	}
	
	public List<I> getListeners() {
		ArrayList<I> activeListeners = new ArrayList<I>();
		activeListeners.addAll(strongListeners);
		for (int i = 0; i < weakListeners.size(); ++i) {
			WeakReference<I> ref = weakListeners.get(i);
			I listener = ref.get();
			if (listener == null) {
				weakListeners.remove(i--);
				continue;
			}
			
			activeListeners.add(listener);
		}
		return activeListeners;
	}
	
}

Который будет работать примерно вот так:

public class User {
	
	...
	
	public interface OnUserUpdateListener {
		public void onUserUpdate();
	}
	
	private Observer<OnUserUpdateListener> updateObserver = new Observer<OnUserUpdateListener>();
	
	public Observer<OnUserUpdateListener> getUpdateObserver() {
		return updateObserver;
	}
	
}

...

@Override
protected void onFinishInflate() {
	super.onFinishInflate();
	/* Мы подписываемся на уведомления при создании объекта */
	currentUser.getUpdateObserver().addWeakListener(this);
}

/* ... и никогда от этих уведомлений не отписываемся */
...


Да, вы можете получить лишние обновления этого view. Но часто это — меньшее из зол. И, при любом раскладе, утечку памяти вы уже не получите.

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

/* Не делайте так! */
currentUser.getUpdateObserver().addWeakListener(new OnUserUpdateListener() {
	@Override
	public void onUserUpdate() {
		/* Этот код не будет вызван */
	}
});

Таймеры и потоки, которые не отменяются при выходе с экрана


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

public class HandlerActivity extends Activity {

	private Handler mainLoopHandler = new Handler(Looper.getMainLooper());
	private Runnable queryServerRunnable = new Runnable() {
		@Override
		public void run() {
			new QueryServerTask().execute();
			mainLoopHandler.postDelayed(queryServerRunnable, 10000);
		}
	};
	
	@Override
	protected void onResume() {
		super.onResume();
		mainLoopHandler.post(queryServerRunnable);
	}
	
	@Override
	protected void onPause() {
		super.onPause();
		/* Вы забыли написать строчку ниже и в вашем приложении появилась утечка памяти */
		/* mainLoopHandler.removeCallbacks(queryServerRunnable); */
	}
	
	...
	
}

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

Никогда не сохраняйте ссылки на fragment в activity или другом fragment


Я очень много раз видел эту ошибку. Activity хранит ссылки на 5-6 запущенных фрагментов даже не смотря на то, что на экране всегда виден только 1. Один фрагмент хранит ссылку на другой фрагмент. Фрагменты, видимые на экране в разное время, общаются друг с другом по прямым закешированным ссылкам. FragmentManager в таких приложениях выполняет чаще всего рудиментарную роль — в нужный момент он подменяет содержимое контейнера нужным фрагментом, а сами фрагменты в back stack не добавляются (добавление фрагмента, на который у вас есть прямая ссылка, в back stack рано или поздно приведет к тому, что фрагмент будет выгружен из памяти; после возврата к этому фрагменту будет создан новый, а ваша ссылка продолжит ссылаться на существующий, но невидимый пользователю фрагмент).

Это очень плохой подход по целому ряду причин.

Во-первых, если вы храните в activity прямые ссылки на 5-6 фрагментов, то это тоже самое, как если бы вы хранили ссылки на 5-6 activity. Весь интерфейс, все картинки и вся логика 5 неиспользуемых фрагментов не могут быть выгружены из памяти, пока запущено activity.

Во-вторых, эти фрагменты становится крайне сложно переиспользовать. Попробуйте перенести фрагмент в другое место программы при условии, что он должен быть обязательно запущен в одном activity с фрагментами, x, y и z, которые переносить не надо.

Относитесь к фрагментам как к activity. Делайте их максимально модульными, общайтесь между фрагментами только через activity и fragmentManager. Это может казаться излишне сложной системой: зачем так стараться, когда можно просто передать ссылку? Но, на самом деле, такой подход сделает вашу программу лучше и проще.

По этой теме есть отличная официальная статья от Google: «Communicating with Other Fragments». Перечитайте эту статью и никогда больше не сохраняйте указатели на фрагменты.

Обобщённое правило


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

Все утечки памяти появляются тогда и только тогда, когда вы сохраняете ссылку на объект с коротким жизненным циклом (short-lived object) в объекте с длинным жизненным циклом (long-lived object).

Помните об этом и всегда внимательно относитесь к таким ситуациям.

У этого правила нет красивого короткого названия, такого как KISS, YAGNI или RTFM, но оно применимо ко всем языкам со сборщиком мусора и ко всем объектам, а не только к activity в android.

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

Куда нажать, чтобы узнать, какую строчку в приложении поправить?


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

Для того, чтобы определить наличие и источник утечек памяти в приложении вам потребуется немного времени и MAT. Если вы никогда раньше не пользовались MAT, установите его как plugin к eclipse, откройте DDMS perspective и найдите кнопку «Dump HPROF file». Нажатие на эту кнопку откроет дамп памяти выбранного приложения. Если вы используете Android Studio, то процесс будет немного сложнее, так как на данный момент MAT все ещё не существует как плагин к Android Studio. Поставьте MAT как отдельную программу и воспользуйтесь инструкцией со stackoverflow.

Выполните следующие шаги:

  1. Установите приложение на устройство, подключенное к компьютеру и попользуйтесь им таким образом, чтобы оказаться на каждом экране как минимум однажды. Если один экран может быть открыт с разными параметрами, постарайтесь открыть его со всеми возможными комбинациями параметров. Вообщем — пройдитесь по всему приложению, как если бы вы проверяли его перед релизом. После того как вы прошли все экраны, нажимайте кнопку «назад» до тех пор, пока не выйдите из приложения. Не нажимайте кнопку home — ваша задача завершить все запущенные activity, а не просто скрыть их.
  2. Нажмите на кнопку Cause GC несколько раз. Если вы этого не сделаете, в дампе будут видны объекты, которые подлежат уничтожению сборщиком мусора, но ещё не были уничтожены.
  3. Сделайте дамп памяти приложения нажав на кнопку «Dump HPROF file».
  4. В открывшемся окне сделайте OQL запрос: «SELECT * FROM instanceof android.app.Activity»


    Список результатов должен быть пустым. Если в списке есть хотя бы один элемент, значит этот элемент — это и есть ваша утечка памяти. На скриншоте вы видите именно такой элемент — HandlerActivity: это и есть утечка памяти. Выполните пункты 8-10 для каждого элемента из списка.
  5. Выполните аналогичные запросы для наследников Fragment: «SELECT * FROM instanceof android.app.Fragment». Как и в предыдущем случае, все, что попало в список результатов — это утечки памяти. Выполните для каждой из них пункты 8-10.
  6. Откройте histogram. Результаты, отображаемые в histogram, отличаются от результатов, отображаемых в OQL тем, что в histogram отображаются классы, а не объекты. В поле фильтра введите используемый для ваших классов package name (на скриншоте это com.examples.typicalleaks) и отсортируйте результаты по колонке objects (сколько объектов данного класса сейчас существует в системе). Обратите внимание, что в результатах отображаются в том числе и классы, 0 экземпляров которых существовало на момент получения дампа. Эти классы нас не интересуют. Если объектов действительно много — выделите всю таблицу, нажмите правой кнопкой и выберите пункт Calculate Precise Retained Size. Отсортируйте таблицу по полю Retained Heap и рассматривайте только объекты с большими значениями Retained Heap, например больше 10000.


    На этот раз далеко не все объекты классов, которые вы видите в списке, являются утечками памяти. Однако все эти классы — это классы вашего приложения, и вы должны примерно понимать, сколько объектов каждого из этих классов должно существовать в данный момент. Например, на скриншоте мы видим 6 объектов класса Example и один массив Example[]. Это нормально — класс Example это enum, его объекты были созданы при первом обращении и будут существовать пока существует приложение. А вот HandlerActivity и HandlerActivity$1 (первый анонимный класс, объявленный внутри файла HandlerActivity.java) — это уже знакомые нам утечки памяти. Нажимаем правой кнопкой на подозрительный класс, выбираем пункт list objects, выполняем пункты 8-10 для одного из объектов из полученного списка.
  7. Если к этому шагу у вас не набралось ни одного подозрительного объекта — поздравляю! В вашем приложении нет значимых утечек памяти.
  8. Нажмите правой кнопкой на подозрительный объект и выберите пункт Merge Shortest Paths to GC Roots — exclude all phantom/weak/soft etc. references.
  9. Раскройте дерево. У вас должна получится примерно следующая картина:


    В самом низу вы должны увидеть ваш подозрительный объект. В самом верху — корень сборщика мусора. Все, что посередине — это объекты, соединяющие ваш подозрительный объект с корнем сборщика мусора. Именно эта цепочка и не позволяет сборщику мусора уничтожить подозрительный объект. Читать эту цепочку следует следующим образом: жирным написана переменная объекта выше по списку, в которой содержится ссылка на объект справа от названия переменной. Т.е. на скриншоте мы видим, что переменная mMessages объекта MessageQueue содержит ссылку на объект Message, который содержит переменную callback, ссылающуюся на объект HandlerActivity$1, который содержит ссылку на объект HandlerActivity в переменной this$0. Иными словами, наш подозрительный объект HandlerActivity удерживает первый Runnable, объявленный в файле HandlerActivity.java, так как он добавлен в Handler с помощью метода post или postDelayed. Найдите последний снизу списка класс, который являются частью вашего приложения, нажмите на него правой кнопкой и выберите пункт Open Source File.
  10. Исправьте код приложения таким образом, чтобы разрушить цепочку между подозрительным объектом и корнем сборщика мусора в тот момент, когда подозрительный объект перестанет быть нужен. В нашем примере нам достаточно вызвать метод Handler.removeCallbacks(Runnable r) в методе onPause HandlerActivity.
  11. После того, как вы разобрались со всеми подозрительными объектами, повторите алгоритм с шага 1, чтобы проверить, что теперь все работает нормально.


Заключение


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

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

Автор: @Grebenets
Sebbia
рейтинг 26,26
Компания прекратила активность на сайте

Комментарии (36)

  • +2
    Шикарная статья. Спасибо!
  • +2
    Действительно шикарная, и комикс порадовал.
  • –2
    Статья про утечки памяти и ни слова про valgrind. Хм…
    • +2
      Да, в статье рассказывается только про поиск «утечек» java-объектов.
      В начале статьи я написал про это, но, возможно, недостаточно четко.
  • 0
    Для людей делающих все из консоли или для любителей автоматизировать (требуется root доступ):
    Можно получить *.hprof дамп сделав «adb shell kill -10 » для вашего dalvikvm-процесса. Стянув дамп из /data/misc к себе на компьютер с помощью adb pull его можно поизучать даже с помощью стандарной jvisualvm (standalone-приложение, кажется идет вместе с JDK, не требудет установки эклипса).
  • –6
    Статья, бесспорно, полезная для новичков.

    Но я бы половину текста заменил 3-мя пунктами:
    1. Придерживайтесь принципов ООП.
    2. Используйте инструменты (классы, менеджеры, механизмы), предоставленные Android SDK.
    3. Прочтите какую-нибудь книгу про «чистый код».

    Например:
    — Абстракция и инкапсуляция в кУпе с различными менеджерами SDK не дадут образоваться большей части описанных утечек.
    — Использование локальных переменных вместо полей класса значительно облегчит работу сборщика мусора.
    — …
    И таких моментов много, которые следуют из основ. Читайте книги, они очень полезны.

    • +4
      И еще «4. питайтесь правильно».

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

      Говоря про механизмы Android SDK тоже надо понимать, некоторые механизмы оказываются практически бесполезными для задач для которых они предназначались (у меня в голове пример с AsyncTask), так что для упрощения некоторых задач приходится использовать сторонние механизмы.
      • –3
        ООП бесполезно? Нуну. А как же классика в лице утечки через Context? Стоит ли мне рассказывать как много утечек связано с незнанием принципа наследования и использования в дальнейшем контекста активити в получении чего-либо?

        инструменты SDK не гарантируют вам отсутствие утечек памяти

        Вам вообще ничего не может гарантировать их отсутствие, как и это статья. Так стоит ли об этом говорить вообще?
        • –4
          Минусующие, вы не на GeekTimes, умейте аргументировать свои доводы, особенно в такой важной теме. А коль не знаете что сказать, то лучше и мышку не клацать, ибо ваше анонимное мнение может быть ошибочно воспринято другими.
        • +2
          Вам вообще ничего не может гарантировать их отсутствие, как и это статья. Так стоит ли об этом говорить вообще?

          Жизнь вообще бессмысленная штука :)

          Но вы правы — ничто не может гарантировать отсутствие ошибок. Даже если вы прекрасно понимаете всю теорию, вы все равно будете иногда делать опечатки или просто забывать про какие-то моменты.

          Именно поэтому я рекомендую перед релизом приложения один раз пройтись по всему приложению и отловить все такие ошибки с помощью алгоритма, приведенного в конце статьи. Это занимает достаточно много времени, но это полезная практика. А теория — она больше для того, чтобы понимать что нужно искать.
          • 0
            MAT безусловно лучший инструмент которым можно отлавливать утечки и я трёмя руками за него, потому как и сам его активно использую. Правда я бы порекомендовал перед этим пройтись статическим анализатором кода, например Lint или FindBugs. Они порой указывают на места в коде с проблемами, при этом приводят текст проблемы. Бывает очень полезно.
        • +1
          Вы наверное как-то шире чем я трактуете слово ООП. Лично я не увидел какого-то нарушения ООП в примерах с утечками памяти. Речь идет о сохранении ссылок о которых разработчик может забывать или даже не подозревать, а не о таких вещах как инкапсуляция или полиморфизм.

          Вам вообще ничего не может гарантировать их отсутствие, как и это статья. Так стоит ли об этом говорить вообще?

          Ну в этой статье есть во первых рекомендации по работе с инструментами по поиску утечек. Во-вторых более-менее типичные проблемы с утечками в Android UI программировании. Наверное классика утечек через Context тоже могла бы быть здесь.
          • 0
            Никто и не говорил о нарушениях ООП. Говорили о
            Но я бы половину текста заменил 3-мя пунктами:
            1. Придерживайтесь принципов ООП.


            [quote]Речь идет о сохранении ссылок о которых разработчик может забывать или даже не подозревать[/quote]
            О чем речь идет в принципе понятно. Просто это база для людей. Да, материал неплохой, но он содержит рекомендации для новичков, при этом говорит о MAT, который в свою очередь анализирует хип памяти, а эта тема уже далеко не для новичков.

            И да, я не придираюсь, я просто заметил что ООП тут «причем». Инкапсуляции и полиморфизма тут нет, но вот наследование есть. Народ привык передавать контекст в методы и их не парит что переданная Activity или контекст активности = сама Activity. Нуачо, работает же.
      • 0
        Я не писал про гарантии и панацею. И я ни в коем случае не отрицал полезность тулзов для мониторинга памяти и статьи в целом.
        Но проблемы с лишними ссылками на объекты (коим посвящено >50% статьи) возникают именно по причинам не следования принципам ООП.
        Конечно же есть много других причин утечек памяти. Но именно описанные в статье проблемы решаются продуманной архитектурой и знанием инструментов SDK.

  • –1
    Никогда не сохраняйте ссылки на activity (view, fragment, service) в статических переменных

    Думаю что нужно просто очищать (=null) эту ссылку в onDestroy(). После вызова onDestroy() activity становится не валидной (т.е. не живой), поэтому ссылку в любом случае правильно было бы инвалидировать чтобы никто не воспользовался.
    • 0
      не рекомендую полагаться на это
      • +1
        Если пройдете дальше по вашим ссылкам то увидете что полагаться вполне можно. onDestroy не вызовется если Android убьет процесс который хостит Activity — в этом случае беспокоится об утечках памяти не стоит. onDestory не рекомендуют для сохранения данных.

        Впрочем я не настаиваю на onDestroy(). Если ссылка на activity нужна только когда activity видима можно управлять ею в onResume/OnPause, onStart/onStop. Однако пока выглядит так что обнуление ссылкив onDestroy() решит проблему утечек памяти.
  • 0
    Приличных размеров статья о том, как избегать проблем с менеджментом памяти в языке, который создан чтобы упростить менеджмент памяти.
    Все таки ИМХО куда проще писать на языке без GC, где всегда четко видно время жизни объекта и для избегания утечек достаточно придерживаться только одного правила: объект должен быть удален там же, где был создан.
    • 0
      > Приличных размеров статья о том, как избегать проблем с менеджментом памяти в языке, который создан чтобы упростить менеджмент памяти.

      Упростить != сделать неважным.
      Представьте сколько способов накосячить в плюсах…
      • 0
        Но при этом все возможные способы покрываются одним коротким правилом, которые я выше написал. :)
        А о факте утечек можно узнать просто подключив профайлер, который при закрытии приложения укажет на все участки не освобожденной памяти.
  • 0
    Пожалуй первый за долгое время блог компании, стартовавший отличным постом. Спасибо и так держать.
  • 0
    Один из первых вопросов, с которым сталкивается каждый начинающий разработчик, это как передать объект из одного activity в следующий.
    Используйте передачу объектов через Intent, либо вообще передавайте не объект, а id объекта (если у вас есть база данных, из которой этот id потом можно достать)

    Каким же образом поступать, если надо передать сложный несериализуемый объект? Имхо, из-за невозможности сделать это простым, очевидным способом и растут кучи костылей.
    • 0
      если у вас сложный несериализуемый объект, то как поведёт себя приложение при принудительной его выгрузке и перезапуске потом?
      • 0
        Создается заново при каждом открытии приложения. Пример такого объекта менеджер закачек, кэширования и прочего. Служит для того чтобы не запрашивать много кратно одни и те-же данные, но так же не держать их устаревшими.
        • 0
          зачем передавать объект? хороший пример тут — наличие менеджера, где хранить эти сложные объекты. а между Activity передавать ID объектов.
          • 0
            Менеджер тоже объект и его тоже надо передавать между Activity.
            • 0
              Этот манагер как раз может быть статическим с виклинками.
          • 0
            Сразу вспоминается фабрика абстрактных фабрик.
  • 0
    Благодаря вашей статье увидел, что реклама от Гугла очень «течет» у меня в приложении.
    Что было

    image


    Оказалось достаточно было в onStop удалять adView
    Вот так
    AdView adView = (AdView) findViewById(R.id.adView);
    adView.destroy();
    adView.removeView(adView);


    И после повторных тестов утечек уже не было!

    Я бы, даже не подумал туда посмотреть, если бы не вы.
    Спасибо за отличный пост!
  • 0
    который продолжает существовать после того, как он должен быть уничтожен
    после того, как он перестал быть нужным.
  • 0
    А для Android Studio (IDEA) есть тулза, аналогичная MAT?
    Цель — получить список ссылок на тот или иной объект.
    • 0
      На данный момент плагинов для Android Studio, решающих эту задачу, нет.
      Поставьте MAT как отдельную программу и воспользуйтесь инструкцией со stackoverflow.

      Либо поставьте Eclipse с ADT и MAT и воспользуйтесь им. Вам даже проект импортировать в eclipse не потребуется.
  • 0
    Я открыл приложение и начал прокликивать экраны, ожидая, когда же приложение наконец упадет на моем Nexus 5. Наконец, через 5 минут и 55 экранов, приложение упало.

    Вот поэтому я и проверяю плавность работы анимаций, скорость перехода между экраанами и прочие визуальные штуки (включая и OutOfMemoryError, причины которых я нет-нет, и допускаю в коде) в приложениях, которые я пишу, не на Nexus 5, а на довольно хилом HTC Desire X.
  • 0
    Не только потому, что он моментально приводит к утечке памяти (статическая переменная продолжит существовать пока существует приложение, и activity, на который она ссылается, никогда не будет выгружен). Этот подход также может привести к ситуации, когда вы будете обмениваться информацией не с тем экраном, ведь экран, невидимый пользователю, может в любой момент быть уничтожен и пересоздан лишь когда пользователь к нему вернется.

    Вижу противоречие. Либо не будет выгружен, либо все-таки будет в любой момент уничтожен. Собственно, ссылки на GUI элементы и держат, когда нужно чтобы их не убило, и не надо было заново создавать и грузить экран при каждом переключении.
    • +1
      Я неправильно выразился. Он не может быть уничтожен, он может быть «забыт» системой.

      Я поясню на примере:

      Представьте, что ваше приложение состоит из двух Activity: ListActivity и DetailsActivity.
      Когда пользователь запускает приложение, он попадает на ListActivtiy. Когда он выбирает элемент в ListActivity, открывается DetailsActivity.

      Пользователь запускает приложение и система автоматически создает экземпляр класса ListActivity; назовем его объект 1. Вы сохраняете ссылку на этот объект в статической переменной. После этого пользователь переходит на экран DetailsActivity. В этот момент система создает экземпляр класса DetailsActivity; назовем его объект 2. После некоторого времени пользователь нажимает кнопку back и возвращается на экран ListActivity. Вот тут и начинаются проблемы.

      В любой момент между тем, как пользователь ушел с экрана ListActivity и вернулся к нему обратно, система может «забыть» про объект 1. Это не значит, что объект будет обязательно уничтожен сборщиком мусора (вы же держите на него ссылку). Это значит, что в момент, когда пользователь вернется на экран ListActivity вместо того, чтобы использовать старый объект 1, система создаст новый экземпляр класса ListActivity, объект 3. У вас в системе будет одновременно два экземпляра класса ListActivity: объект 1, хранящийся в вашей статической переменной и объект 3, отображаемый пользователю.

      Про объект 1 будете знать только вы — он уже никогда не будет показан пользователю. Вы можете изменять его поля, вызывать его методы, но визуально это не будет приводить ни к чему — ведь пользователь видит на экране объект 3, а не объект 1.

      Вы можете легко добиться описанного эффекта с двумя экземплярами ListActivity включив опцию Settings>Developer options>Don't keep activities.

      Поэтому ваше утверждение о том, что
      Собственно, ссылки на GUI элементы и держат, когда нужно чтобы их не убило, и не надо было заново создавать и грузить экран при каждом переключении.

      в корне неверно относительно Activity (с fragments или views так получится — их созданием управляете вы сами, а не система).
      • 0
        Это значит, что в момент, когда пользователь вернется на экран ListActivity вместо того, чтобы использовать старый объект 1, система создаст новый экземпляр класса ListActivity, объект 3. У вас в системе будет одновременно два экземпляра класса ListActivity: объект 1, хранящийся в вашей статической переменной и объект 3, отображаемый пользователю.


        А, понятно, специфическое поведение активити.

Только зарегистрированные пользователи могут оставлять комментарии. Войдите, пожалуйста.

Самое читаемое Разработка