среда, 24 декабря 2014 г.

Ищем утечки памяти в игре на AndEngine

Привет.

Один из пользователей (привет, NEBR), который следит за процессом создания моей игры Save New Year, попросил поделиться опытом устранения утечек оперативной памяти.


Что ж, постараюсь упорядочить свои мысли и действия, написав данную статью.

Напомню, что я пишу игры для платформы Android в IDE IntelliJ Idea 14. Возможно некоторых инструментов, о которых я сегодня расскажу, нет в вашей IDE либо они имеют отличное от моих название. В там случае придется искать альтернативы либо менять 

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

Этап первый - контроль за выделением памяти

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

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

Первым подсказать Вам о том, что что-то идет не так может Memory Monitior. Не знаю на счет других IDE, Но у меня он находится вот тут:


На это вкладке в реалтайм рисуется график, по которому просто определяется если ли у Вас "лишние" выделения памяти. Если график растет с постоянной скоростью вверх, или после определенной последовательности действий. Это первый звоночек, что нужно искать причину! Для большей уверенности можно принудительно вызвать GC и если образуется резкий обрыв графика - это точный знак того, что в программе есть что оптимизировать!


Конечно же просмотреть вдоль и поперек свой код в поисках new можно и даже нужно, но мне кажется это не очень то эффективно. К счастью Idea предлагает инструмент, который позволяет зафиксировать момент создания новый объектов (т.е. выделение памяти).

Allocation Tracking для фиксации выделения памяти


Для этого нужно подключить девайс к компьютеру, запустить приложение. Далее на вкладке "Android DDMS" выбрать нужный процесс с Вашим приложением и затем нажать кнопку "Start Allocation Tracking"

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

После чего следует нажать снова но кнопку "Stop Allocation Tracking".  Через некоторые время (от нескольких секунд до минуты, в зависимости от продолжительности "прохода") на экране появится список, в котором будут все места где выделялась память.


Коротко о полученной информации: 
Allocated Class - собственно класс, для объекта которого была выделена память
Allocated Size - размер выделенной памяти.
Allocated Site - место в программе, где было выделена память.
Если нажать на любую строку, то в нижней части окна будет Stack Trace вызовов. Со ссылкой на код.

В этих списках как Ваши классы так и системные. Первым делам я сортирую Allocated Site по алфавиту, и смотрю где в моих классах выделяется память. Сделает отметить, что не все чего много то плохо! Например у меня в игре летят снежинки, хоть я и использую для них пул, но первоначально, чтобы его заполнить нужно их создать!

Таким образом, вот такое:

Для моей программы вполне нормально, так как все эти вызовы ведут в пул:


Тем не менее, таким образом я нашел несколько мест для оптимизации, Одними из их является конкатенация строк, например, "123"+"123123" Для каждой такое операции создается новый объект String. Если таких операций много, и они находятся в игровом цикле, который вызывает повторяет из со скоростью FPS...  Поэтому лучше для данной операции использовать StingBuilder.

У меня такие нарушения были и  в обновлении счетчика времени.

После исправления этим мелочей, рост графика памяти не прекращался, поэтому воспользуемся следующим приемом.

Локализация проблемных зон

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

Так я нашел еще одно не приятное место. Я ограничил количество снежинок до 250. Но даже при не большом снегопаде создавались все 250! Разгадка таилась в том, что я ограничил время жизни каждой отдельной снежинки до 10-и секунд. За это время конечно же успевало создаться 250 снежинок. Сейчас переделал таким образом, что как только снежинка исчезает с экрана, она возвращается в пул.

После таких манипуляций, мой график почти не растет!

Обнаружение объектов, которые не могут быть очищены сборщиком мусора.

Об утечках я задумался после статьи на хабре. Собственно и данный метод поиска и устранения я почерпнул от туда.

Чтобы я не повторялся, просто прочтите данную статью) 

А я просто опишу то, что мне удалась найти в своем приложении. Может быть пригодится тем, кто также работает с AndEngine.

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

clearTouchAreas() // очищает ссылки на слушатели касаний
clearEntityModifiers(); // очищает ссылки на модификаторы
clearUpdateHandlers(); // очищает ссылки на слушатели обновлений внутреннего таймера
clearChildScene(); // очищает ссылки на элементы которые отображаются на сцене

Следующим важным моментом являются поля Scene которые вы передали в дочерние классы, которые в свою очередь сохраняют эти ссылки во внутренних полях.

Такие поля лучше обнулять самому.

Вот как-то так я и борюсь с утечками памяти в своих играх. 

P.s. Перечитал пост. Сплошная вода и рассуждения.. Так и не научился я писать достойные посты за все время ведения блога..














2 комментария: