actionscript 3.0: Работа с памятью

Автор: Павел Наказенко (DTF.RU)

Студент Сибирского государственного Аэрокосмического Университета им. академика М. Ф. Решетнева, факультет информатики и систем управления. Лауреат премии талантливой молодежи, призер всероссийского конкурса по компьютерной графике. Занимался фрилансом (программирование), работал над рядом игровых проектов, в том числе над игрой «Воплощение». В настоящий момент — программист компании «Стратегикон».

0. Введение или «зачем все это?»

В настоящее время сфера online-игр изобилует продуктами, использующими технологию Flash: будь то различные мини-игры или крупные online-проекты (например, браузерные MMORPG). Причем, стоит обратить внимание на тот факт, что Flash приложения авторы стараются использовать по минимуму в своих игровых разработках, отводя им чисто иллюстративную роль. Работая над «Воплощением» мне показался странным тот факт, что несмотря на возможности доступных сред и SDK (Flex, Flash CS, Flash Developer, IDEA), можно было сосчитать по пальцам крупные flash-based бизнес приложения и игры. Как оказалось позже, на то были объективные причины.

Во-первых, приложения базирующиеся на Flex SDK имеют очень низкую производительность в плане визуализации, однако предоставляют довольно удобный набор компонент для быстрого создания бизнес-приложений. В свою очередь, Flash CS SDK обладает высокой производительностью в плане визуализации, однако предоставляет довольно бедный набор визуальных компонент. Существуют различные хитрости и методы оптимизации процесса визуализации Flex SDK приложений, однако их описания не будет в рамках данной статьи.

Во-вторых, сопроводительная документация к любому SDK от Adobe оставляет желать лучшего: часть важных и полезных функций не документирована, практически отсутствуют рекомендации по оптимизации приложений и описание работы базовых механизмов Flash Player (вроде Renderer’а и GC), низка активность представителей Adobe на issue-related форумах. Большую часть информации приходилось добывать из различных блогов и на независимых web-ресурсах. И это не говоря уже о большом количестве ошибок в SDK, которые иногда получается устранить с помощью создания своего исправленного класса — наследника родителя с ошибкой.

В-третьих, темпы обновлений от Adobe сложно предугадать, равно как и масштабы изменений с каждой новой субверсией. Опять же, порой с новой субверсией возникали различные конфликты: начиная от несовместимости Flash Player с семейством ОС, заканчивая отсутствием обратной совместимости с предыдущими субверсиями (спасает только принудительное требование последней версии при открытии страницы с приложением).

В-четвертых, было потрачено довольно много времени на поиск Flash Player для *bsd и *nix семейств, но найти версию, корректно работающую с кириллицей (проблема отображения ввода с клавиатуры), так и не удалось. Не помогло даже использование Wine, как и различные хитрые манипуляции с локалями и переменными окружения целевой ОС.

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

Дело в том, что после старта клиента игры «Воплощение» на машинах пользователей наблюдался сильный перерасход памяти, а, следовательно, снижение производительности системы в целом, не говоря уже о зависаниях браузера. Что интересно, график расхода памяти Windows показывал резкие скачки, что не двусмысленно намекало на «не обычную» работу GC Flash Player.

Данная статья является своеобразным сводом правил по работе с памятью в приложениях на основе ActionScript 3. Прежде всего, хотелось бы уточнить, что данные, описанные в статье, собирались по крупицам из Интернета, некоторые из них являются следствием горького опыта. Полноценной документации по работе с памятью от производителя нет. По причине отсутствия целостного описания простейших правил работы с памятью даже опытные разработчики часто сталкиваются с проблемой утечки памяти или ее необоснованного перерасхода. К сожалению, количество «подводных камней» велико, и на поздней стадии разработки может потребоваться довольно много ресурсов для внесения необходимых исправлений.

Предполагается, что читатель владеет базовыми навыками программирования на языке ActionScript 3.

1. Термины и определения

Прежде чем начать описание базовых правил работы с памятью, условимся, что:

Garbage Collector (GC) — это механизм, который в определенные моменты времени пытается освободить память, использованную объектами, которые уже не будут востребованы приложением (то есть производит сборку мусора).

Strong reference — обычный тип ссылки на объект, наличие которого сигнализирует GC об актуальности и необходимости объекта.

Weak reference — обособленный, «не строгий» тип ссылки на объект, наличие которого не влияет на работу GC.

Chunk (чанк) — в нашем случае некоторое количество байт, выделенное менеджером памяти для объекта по требованию Flash Player.

Pool (пул) — в нашем случае совокупность определенного количества чанков.

2. Принципы работы Garbage Collector

Память, которой оперирует GC, представлена в виде пулов, которые, в свою очередь, представлены набором чанков по несколько байт (обычно 512). Такие крупные объекты как картинки и файлы в пул не заносятся.

По запросу на выделение памяти резервируются некоторое количество чанков. Как только свободных чанков становится недостаточно, выделяется новый пул. По мере того, как объект удаляется или явно теряет свою актуальность, соответствующий ему chunk памяти помечается как «unused», но фактического освобождения памяти не происходит. При этом GC совершает итерацию по текущему пулу памяти. В том случае, если памяти становится мало, GC пытается освободить память из-под «unused» чанков, но делает это только в пределах текущего пула. В том случае, если этого оказывается не достаточно, совершается дополнительная итерация по дефрагментации пулов, то есть упорядочиванию «unused» чанков среди остальных пулов для последующего освобождения памяти.

Однако не существует гарантии, что за одну итерацию будут освобождены все неиспользованные чанки. Объекты, которые имеют счетчик строгих ссылок больше нуля, не удаляются GC. Поэтому зачастую GC выделяет пул, когда есть возможность использования «unused» чанков. Из-за этого возникает сильная фрагментация пулов (необоснованный перерасход памяти) и утечки памяти.

Специфической чертой работы GC в AS3 является то, что не производится периодической проверки на наличие «мусора». Это означает, что выделение памяти будет оставаться на том же уровне, несмотря на многочасовой idle приложения или при микрооперациях с памятью, когда для GC не возникает необходимости очищать пул или создавать новый (совершать итерацию).

3. Weak References, Strong References

Как уже было сказано ранее, все ссылки в Action Script 3 делятся на два вида: weak references и strong references. Единственное различие между ними — Garbage Collector при удалении объекта не обращает внимания на weak references, в то время как, если счетчик strong references > 0, то память не освобождается.

4. Правила работы со ссылками

4.1. Все strong-ссылки на объекты в ActionScript должны удаляться, в противном случае GC удалением подобных объектов не занимается.

Пример:

// Создаем объект, помещаем референс на него в first
var first:Object = {dtf:"rulez"}
// помещаем референс в second
var second:Object = first;
// удаляем объект first
delete(first);
// смотрим, сохранился ли объект в памяти
trace(second.dtf);
// выведет "rulez", значит память все еще не освобождена

Варианты решения:

  1. Слежение за strong reference вручную, своевременное их удаление. Требует больших трудозатрат.

  2. Использование WeakReference (см. ниже). Сравнительно небольшие трудозатраты в использовании.

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

Пример (стараться избегать):

var first:Object = {}
// создаем объект со ссылкой на первый
var second:Object = {link: first};
// у первого объекта делаем ссылку на второй
first.link = second;
// пытаемся удалить оба референса
delete(first);
delete(second);
// при этом доступ к объектам first и second мы теряем, однако счетчик
референсов у каждого из них все еще равен 1

Примечание: Специфично для различных версий Flash Player 8.x, 9.x. В некоторых Mark Sweeping алгоритм справляется, в некоторых нет.

Варианты решения: Использовать WeakReference (см. ниже).

4.3. События, назначаемые объекту, обязательно должны иметь флаг weakReference.

Пример:

Неправильно:

object.addEventListener(Event.CLICK,handleClick);

Правильно:

object.addEventListener(Event.CLICK,handleClick false,0,true);

Такая необходимость обусловлена тем, что хэндлеры по умолчанию считаются как strong reference для объекта, даже если он уже удален.

Примечание: Рекомендуется так же контролировать удаление назначенных Listener’ов с помощью парной функции removeEventListener();

4.4. При использовании словарей (Dictionary) необходимо использовать опцию weakKeys в конструкторе, где это уместно.

Пример:

var dict:Dictionary = new Dictionary(true);
dict[firstObj] = secondObj;
// при этом, на firstObj у словаря weak reference, а на secondObj – strong reference

4.5. Предпочтительно использование класса WeakReference при работе со сложными, большими объектами.

На основе возможностей использования weakKeys у словаря (пункт 2.4), можно написать класс, обеспечивающий «не строгие» ссылки на объекты.

Пример:

import utils.WeakReference;
function getWR(data):WeakReference {
return new WeakReference(data);
}
...
function doSome(object:BigComplexObject):void
{
// получаем weak ссылку на объект
var weakBigObject:BigComplexObject = getWR(object).get() as
BigComplexObject;
// работаем как с обычным объектом
weakBigObject.Run();
}

Примечание: WeakReference не является стандартным модулем. Скачать WeakReference.as (610 байт).

5. Memory Controller

Ввиду особенностей работы GarbageCollector в ActionScript 3 рекомендуется использовать класс MemoryController для регулярного совершения GС итераций и освобождения памяти. Не рекомендуется использовать одновременно более одной копии MemoryController в приложении.

Пример использования:

import utils.MemoryController
...
private var mMemoryController:MemoryController;
...
mMemoryController = new MemoryController(<интервал обновления>, <лимит расхода памяти
в байтах перед принудительным совершением итерации GC>, <лимит расхода памяти перед
критичной ситуацией>, <мин. интервал между принудительными итерациями>, <количество
принудительных итераций за раз>, <хэндлер, вызываемый при прохождении критического
предела>, <хэндлер, вызываемый при прохождении опасного предела>);

Данный класс в соответствии с указанным интервалом обновления производит выделения/освобождения памяти различного размера, дабы задействовать максимально возможное количество «unused» чанков в пределах текущего пула или спровоцировать спонтанную итерацию по всем пулам. Как только приложение пересекает «опасный» лимит памяти, MemoryController начинает форсировать заданное количество итераций GC с минимальным интервалом между попытками (указано внутри конструктора класса). Так же вызывается соответствующий хэндлер, если это указано (может быть использовано для отладки или дополнительной информации). По мере того, как приложение достигает «критического» уровня выделения памяти, вызывается соответствующий хэндлер, попытки форсированных итераций продолжаются.

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

Не рекомендуется выставлять короткий слишком интервал между попытками форсирования итерации GC или выставлять большое количество этих самых итераций, так как это может серьезно снизить производительность приложения.

Примечание: MemoryController не является стандартным модулем. Скачать MemoryController.as (3278 байт).

6. Заключение

Применив в коде «Воплощения» несколько правил из раздела 4, а так же MemoryController, получилось существенно снизить объемы утечки памяти: с пика перерасхода в более чем 220 Мбайт, до 85–95 Мбайт.

Применение всех выше перечисленных базовых правил и вспомогательных средств на начальном этапе разработки позволяет повысить производительность приложения в целом и избежать сильного перерасхода памяти. Описанные методики могут быть использованы так же и на поздней стадии разработки проекта, однако исправление кода в этом случае может потребовать некоторых ресурсов. Ввиду разнообразия SDK для разработки flash-приложений, в данной статье приведены лишь общие правила и не рассмотрено поведение отдельных компонентов Flex SDK и Flash CS SDK, многие из которых изначально содержат ошибки, ведущие к утечкам памяти.

Комментарии

Аватар пользователя Aloran

в пункте 4.3 в строке

object.addEventListener(Event.CLICK,handleClick false,0,true);

добавте запятую. Должно получиться так

object.addEventListener(Event.CLICK,handleClick, false

,

0,true);

Аватар пользователя as3coder

Спасибо! Давай еще :)

as3coder.blogspot.com

Аватар пользователя moisha

Частично знал. Однакаоч хорошая инфа о работе Сборщика мусора. 

good