JavaScript

Управление памятью в приложениях Windows Store

Дэвид Теппер

Продукты и технологии:

Windows 8, JavaScript

В статье рассматриваются:

  • различные виды рабочей памяти;
  • обнаружение утечек памяти;
  • распространенные источники утечек памяти;
  • проектирование архитектуры приложений Windows Store, позволяющей избежать утечек памяти при использовании JavaScript.

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

Как свидетельствует опыт группы Microsoft Windows Application Experience, некоторые приложения Windows Store при продолжительном использовании начинают сталкиваться с проблемой нехватки ресурсов. Ошибки управления памятью в приложениях могут со временем усугубляться, вызывая использование лишней памяти и отрицательно влияя на общую производительность компьютера. Предпринимая попытки вычистить эти ошибки в собственных продуктах, мы идентифицировали ряд повторяющихся шаблонов проблем, а также нашли общие исправления и методики, позволяющие избегать подобных ошибок. В этой статье мы рассмотрим, как правильно управлять памятью в приложениях Windows Store, а также обсудим способы выявления потенциально возможных утечек памяти. Я также представлю некоторые решения в коде, устраняющие частые проблемы, которые наблюдала наша группа.

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

Любая ситуация в приложении, ведущая к тому, что ресурсы нельзя ни вернуть системе, ни использовать, рассматривается как утечка памяти (memory leak). Иначе говоря, если приложение удерживает блок памяти, который остальная часть системы никогда не сможет задействовать до завершения этого приложения, и если приложение само не пользуется этой памятью, проблема налицо. Это более широкое определение утечки памяти по сравнению с типичным — «динамически распределяемая память, которая становится недоступной в коде», — но оно и полезнее, поскольку охватывает другие, схожие проблемы использования ресурсов, ведь они тоже могут отрицательно сказаться как на производительности системы, так и на эффективности труда пользователя. Например, если какое-то приложение хранит данные, доступные изо всех частей кода, но эти данные используются лишь раз и впоследствии никогда не освобождаются, это тоже утечка памяти согласно данному определению.

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

Каково влияние утечки памяти?

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

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

  • часто выполняемых задачах (например, при декодировании очередного кадра видеоролика);
  • задачах, для инициации выполнения которых не требуется взаимодействия с пользователем (скажем, периодическое автосохранение документа);
  • длительно выполняемых операциях (таких как фоновые задачи).

Утечки в этих ситуациях (и в целом) могут драматически увеличить объем занимаемой вашим приложением памяти. Это не только ведет к кризису с ресурсами во всей системе, но и резко увеличивает вероятность принудительного завершения вашего приложения вместо приостановки в период простоя. Принудительно завершенные приложения требуют больше времени на повторную активацию, чем приостановленные, что создает отрицательное впечатление у пользователей. Детальное описание того, как Windows использует диспетчер сроков жизни процессов для изъятия памяти у неиспользуемых приложений, см. в блоге Building Windows 8 по ссылке bit.ly/JAqexg.

Итак, утечки памяти — штука плохая, но как находить их? В следующих разделах мы займемся этим вопросом, а затем рассмотрим, почему они происходят и что с ними можно сделать.

Разные виды памяти

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

Закрытый рабочий набор (private working set) Набор страниц, занимаемых в данный момент вашим приложением для хранения собственных уникальных данных. Когда говорят об использовании памяти приложением, чаще всего подразумевают именно этот набор.

Разделяемый рабочий набор (shared working set) Набор страниц, используемых вашим процессом, но не принадлежащих ему. Если ваше приложение использует общую исполняемую среду или инфраструктуру, общие DLL или другие ресурсы, предназначенные для многих процессов, эти ресурсы занимают некий объем памяти. Разделяемый рабочий набор является мерой таких общих ресурсов.

Общий рабочий набор (total working set, TWS) Иногда называют просто рабочим набором; это сумма закрытого и разделяемого рабочих наборов.

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

Обнаружение утечек памяти

Самый простой способ выяснить, сколько памяти занимает ваше приложение в каждой категории, — использовать встроенный Windows Task Manager (диспетчер задач).

  1. Запустите Task Manager нажатием Ctrl+Shift+Esc, а затем щелкните More Details в нижней части.
  2. Откройте элемент меню Options и убедитесь, что строка «Always on top» помечена галочкой. Это предотвратит переход вашего приложения в фоновый режим и его приостановку, пока вы смотрите в Task Manager.
  3. Запустите свое приложение. Как только оно появится в Task Manager, щелкните его правой кнопкой мыши и выберите Go to details.
  4. В верхней части щелкните правой кнопкой мыши любой столбец и выберите Select Columns.
  5. Здесь вы заметите параметры для отображения размеров закрытого и разделяемого рабочих наборов (помимо других), но на данный момент достаточно выбрать Working set (memory) и щелкнуть OK (рис. 1).
  6. Значение, которое вы увидите, соответствует TWS вашего приложения.

Проверка общего рабочего набора в Windows Task Manager
Рис. 1. Проверка общего рабочего набора в Windows Task Manager

Чтобы быстро выявить потенциальные утечки памяти, оставьте свое приложение и Task Manager открытыми и запишите значение TWS. Теперь выберите такой сценарий в приложении, который вам нужно проверить. Сценарий состоит из операций, которые часто выполняются типичным пользователем, и обычно включает не более четырех этапов (навигация между страницами, поиск и т. д.). Выполните этот сценарий так, как это делал бы пользователь, и отметьте любое увеличение в TWS. Далее, не закрывая приложение, снова повторите этот сценарий. Делайте так 10 раз подряд и каждый раз после прогона сценария записывайте значение TWS. Увеличение TWS для первых нескольких итераций совершенно нормальное явление, но потом его значения должны выйти на плато.

Увеличивается ли занимаемая вашим приложением память при каждом выполнении сценария, никогда не возвращаясь к исходному уровню? Если да, есть вероятность утечки памяти в этом сценарии, и вы должны рассмотреть следующие предположения. Если нет, отлично! Но не забудьте проверить другие сценарии в приложении, в частности те, которые применяются очень часто или используют крупные ресурсы вроде изображений. Но избегайте выполнения этого процесса в виртуальной машине или через Remote Desktop; в этих средах возможны ложные положительные результаты при поиске утечек и значения используемой памяти, превышающие реальные.

Применение средств обнаружения утечек памяти в версиях до выпуска Windows 8

Вероятно, вас интересует, можно ли использовать существующие средства обнаружения утечек памяти для идентификации проблем в приложениях Windows Store. Если эти средства не обновлены для работы с Windows 8, то скорее всего они «войдут в ступор» от того, что приложение не закрывается как обычно (и этот процесс был заменен на приостановку). Чтобы обойти эту проблему, можно задействовать функциональность Exit объекта AppObject для прямого закрытия приложения должным образом вместо принудительного закрытия средствами системы:

  • в C++ — CoreApplication::Exit();
  • в C# — Application.Current.Exit();
  • в JavaScript — window.close().

Применяя этот способ, не забудьте убрать этот код перед поставкой своего продукта. Ваше приложение не будет запускать код, который инициирует приостановку, и потребует реактивации (вместо возобновления) при каждом открытии. Используйте этот вариант только для отладочных целей и удаляйте его код до передачи приложения в Windows Store.

Распространенные источники утечек памяти

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

Обработчики событий  Пока что это самый распространенный источник утечек памяти, который мы видели в приложениях Windows Store. Фундаментальная проблема в отсутствии понимания того, как работают обработчики событий. Обработчики — это не просто код, выполняемый при определенных событиях; они создают объекты данных. Они хранят ссылки на другие сущности, и то, на что они хранят ссылки, может оказаться далеко не очевидным. С концептуальной точки зрения, создание экземпляра и регистрация обработчика события затрагивают три части.

  1. Источник события.
  2. Метод обработчика события (его реализацию).
  3. Объект, в котором находится этот метод.

В качестве примера рассмотрим приложение LeakyApp, показанное на рис. 2.

Рис. 2. LeakyApp

public sealed partial class ItemDetailPage :
  LeakyApp.Common.LayoutAwarePage
{
  public ItemDetailPage()
  {
    this.InitializeComponent();
  }
  Protected override void OnNavigatedTo(NavigationEventArgs e)
  {
    Window.Current.SizeChanged += WindowSizeChanged;
  }
  private void WindowSizeChanged(object sender,
    Windows.UI.Core.WindowSizeChangedEventArgs e)
  {
    // Реакция на изменение размера
  }
  // Другой код
}

Код LeakyApp показывает три части обработчика события:

  • Window.Current — объект, являющийся источником события (запускающий его);
  • экземпляр ItemDetailPage — объект, принимающий событие;
  • WindowSizeChanged — метод обработчика события в экземпляре ItemDetailPage.

После регистрации на уведомление о событии объект текущего окна получает ссылку на обработчик события в объекте ItemDetailPage (рис. 3). Эта ссылка заставляет объект ItemDetailPage существовать до тех пор, пока существует объект текущего окна или пока объект текущего окна не откажется от ссылки на экземпляр ItemDetailPage (на данном этапе мы проигнорируем другие внешние ссылки на эти объекты).

Ссылка на обработчик события
Рис. 3. Ссылка на обработчик события

 

Window Окно
Size Changed Размер изменился
Page Страница
WindowSizeChanged WindowSizeChanged

 

Заметьте: чтобы экземпляр ItemDetailPage работал корректно, Windows Runtime (WinRT) хранит все ресурсы, используемые этим экземпляр, пока он существует. Если этот экземпляр содержит ссылки на большие блоки памяти вроде массивов или изображений, данная память останется занятой до его освобождения. В итоге, регистрация обработчика события продлевает сроки жизни экземпляра объекта, содержащего этот обработчик, и всех его зависимостей до тех пор, пока существует источник события. Конечно, на этом этапе утечки ресурса еще нет. Это просто последовательность, связанная с подпиской на событие.

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

Второй экземпляр, зарегистрированный в текущем окне
Рис. 4. Второй экземпляр, зарегистрированный в текущем окне

После пяти переходов в текущем окне будет зарегистрировано уже пять объектов ItemDetailPage (рис. 5), и все они будут удерживать свои зависимые ресурсы.

Пять объектов, зарегистрированных в текущем окне
Рис. 5. Пять объектов, зарегистрированных в текущем окне

Эти более не используемые экземпляры ItemDetailPage являются ресурсами, которые никогда не применяются и не отзываются, и это уже утечка. Итак, запомните: отмена регистрации более ненужных обработчиков событий — лучший способ предотвратить самые распространенные случаи утечек памяти.

Чтобы исправить проблему в LeakyApp, нам нужно удалить ссылку на обработчик события SizeChanged из текущего окна. Это можно сделать отменой подписки из обработчика, когда страница выходит из области видимости, например:

protected override void OnNavigatedFrom(NavigationEventArgs e)
{
  Window.Current.SizeChanged -= WindowSizeChanged;
}

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

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

Круговые ссылки в обработчиках событий, пересекающие границы GC При создании обработчика для конкретного события вы начинаете с указания функции, которая будет вызываться при срабатывании события, а затем подключаете этот обработчик к объекту, который будет получать данное событие. При срабатывании события у функции-обработчика появляется параметр, представляющий объект, который изначально принял это событие, и он называется источником события (event source). В показанном ниже обработчике события щелчка кнопки источником события является параметр sender:

private void Button_Click(object sender, RoutedEventArgs e)
{
}

По определению, источник события имеет ссылку на обработчик, а иначе источник не смог бы инициировать событие. Если вы захватываете ссылку на источник внутри обработчика, то у этого обработчика появляется ссылка обратно на источник и тем самым вы создаете круговую ссылку (circular reference). Рассмотрим довольно распространенный шаблон этого в действии:

// gl объявляется в области,
// где он будет доступен множеству методов
Geolocator gl = new Geolocator();
public void CreateLeak()
{
  // Обработка события PositionChanged встраиваемой функцией
  gl.PositionChanged += (sender, args) =>
    {
      // При ссылке на gl вы создаете здесь круговую ссылку
      gl.DesiredAccuracy = PositionAccuracy.Default;
    };
}

В этом примере gl и sender — одно и то же. Ссылка на gl в лямбда-функции создает круговую ссылку, так как источник ссылается на обработчик и наоборот. Обычно этот вид круговых ссылок не создает проблем, поскольку сборщики мусора (GC) в CLR и JavaScript достаточно интеллектуальны для обработки таких случаев. Однако проблемы могут появиться, когда одна сторона круговой ссылки не относится к среде с поддержкой GC или принадлежит другой GC-среде.

Geolocator является WinRT-объектом. WinRT-объекты реализуются на C/C++ и поэтому используют систему учета ссылок вместо GC. Когда CLR GC пытается очистить эту круговую ссылку, он не в состоянии самостоятельно справиться с gl. Аналогично счетчик ссылок для gl никогда не обнулится, поэтому и на стороне C/C++ очистить его не удастся.

Конечно, это очень простой пример для демонстрации проблемы. Что будет, если это был бы не единственный объект, а большая группа UI-элементов вроде панели (или div в JavaScript)? Утечка распространилась бы на все эти объекты, и отслеживание источника превратилось бы в крайне сложную задачу.

Для многих таких круговых ссылок предусматриваются свои ухищрения, так что их удается обнаруживать и очищать с помощью GC. Так, круговые ссылки, включающие WinRT-источник событий, который «зациклен» с JavaScript-кодом (или имеет круговые ссылки с XAML-объектом как источником событий), удаляются корректно. Но эти ухищрения охватывают отнюдь не все формы «зацикленности» (такие как JavaScript-событие с обработчиком на C#), и по мере увеличения количества и сложности ссылок на источник событий они могут не сработать.

Отмена регистрации более ненужных обработчиков событий — лучший способ предотвратить самые распространенные случаи утечек памяти.

Если вам нужно создать ссылку на источник события, вы всегда можете явным образом отменять регистрацию обработчика события или обнулять ссылку впоследствии, чтобы разорвать «зацикленность» и избежать любых утечек. Но, если обработчик никогда не хранит ссылку на источник, вам не понадобится опираться на специфические средства, предоставляемые платформой, или на явный код для предотвращения потенциальной проблемы с утилизацией очень больших ресурсов.

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

К сожалению, у GC (или любого другого диспетчера памяти) нет способа решить, что делать с очень большими, но доступными структурами данных, которые никогда не будут использоваться. Чтобы избежать этой проблемы, жестко ограничивайте количество элементов, сохраняемых в кеше. Регулярно удаляйте устаревшие данные и не полагайтесь на то, что ваше приложение будет принудительно завершено для освобождения структур данных такого рода. Если хранимая информация быстро устаревает или ее легко реконструировать, подумайте о том, чтобы полностью опустошать кеш при приостановке вашего приложения. В ином случае сохраняйте кеш в локальном состоянии и освобождайте ресурс в памяти; при возобновлении его можно будет затребовать заново.

Избегайте хранения ссылок на крупные объекты при приостановке

Независимо от языка хранение ссылок на большие объекты (large references) в приостановленном приложении может привести к проблемам с производительностью. Ваше приложение будет оставаться приостановленным до тех пор, пока система будет в состоянии обслуживать запросы других выполняемых процессов без потребности в дополнительной памяти, которую можно получить лишь принудительным завершением каких-либо приложений. Поскольку к приостановленному приложению пользователь получает доступ быстрее, в ваших же интересах, чтобы в приостановленном состоянии оно удерживало минимальный объем памяти.

Простой способ добиться этого — освобождать при приостановке все ссылки на крупные объекты, которые можно реконструировать при возобновлении. Например, если ваше приложение хранит ссылку в памяти на локальные данные, то освобождение этой ссылки может значительно уменьшить размер вашего закрытого рабочего набора, а при возобновлении ее легко затребовать обратно, так как эти данные никуда не деваются. (Подробнее о данных приложения см. по ссылке bit.ly/MDzzIr.)

Чтобы полностью освободить какую-либо переменную, присвойте ей null (и всем ссылкам на эту переменную). В C++ это приведет к немедленному возврату памяти. В случае приложений Microsoft .NET Framework и JavaScript, когда приложение будет приостановлено, запускается GC для отзыва памяти, занимаемой этими переменными. Это подход с глубоко эшелонированной защитой, гарантирующий корректное управление памятью.

Но заметьте: если ваше приложение написано на JavaScript и имеет какие-то .NET-компоненты, при его приостановке .NET GC не запускается.

Управление памятью в приложениях Windows Store на JavaScript

Дам несколько советов по созданию эффективно использующих ресурсы приложений Windows Store на JavaScript. Это рекомендуемые решения распространенных проблем, которые мы видели в собственных приложениях, и проектирование с учетом этих решений поможет предотвратить множество потенциальных проблем до того, как они станут вашей головной болью.

Используйте инструменты для контроля качества кода Часто упускаемый из виду ресурс — бесплатные инструменты для контроля качества кода, доступные всем разработчикам на JavaScript. Эти инструменты анализируют ваш код на множество распространенных проблем, включая утечки памяти, и могут оказаться лучшим средством для отлова проблем на ранних стадиях разработки. Два наиболее полезных инструмента — JSHint (jshint.com) и JSLint (jslint.com).

Используйте строгий режим В JavaScript есть строгий режим (strict mode), который ограничивает то, как можно использовать переменные в коде. Эти ограничения проявляются ошибками периода выполнения, генерируемыми при нарушении дополнительных правил. Такие ограничения в кодировании помогают предотвращать распространенные ситуации с утечкой памяти, например неявное объявление переменных на глобальном уровне. Подробнее о строгом режиме, его применении и налагаемых ограничениях см. в статье MSDN Library «Strict Mode (JavaScript)» по ссылке bit.ly/RrnjeU.

Избегайте круговых ссылок замыкания (circular closure references) В JavaScript довольно сложная система хранения ссылок на переменные, когда используется лямбда-функция (подставляемая функция). В принципе, чтобы подставляемая функция работала корректно при вызове, JavaScript хранит контекст доступных переменных в наборе ссылок, известном как замыкание (closure). Эти переменные существуют в памяти, пока есть ссылки на саму подставляемую функцию. Рассмотрим пример:

myClass.prototype.myMethod = function (paramA, paramB) {
  var that = this;
  // Какой-то код...
  var someObject = new someClass(
    // Это замыкание подставляемой функция (inline function)
    // содержит ссылки на переменную that, а также
    // на переменные paramA и paramB
    function foo() {
      that.somethingElse();
    }
  );
  // Какой-то код: someObject сохраняется в другом месте
}

После сохранения someObject память, на которую ссылаются that, paramA и paramB, не будет возвращена, пока someObject не будет уничтожен или не освободит свои ссылки на подставляемую функцию, которую он передал в конструктор someClass.

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

function addClickHandler(domObj, paramA, paramB, largeObject) {
  domObj.addEventListener("click",
  // Это замыкание подставляемой функции ссылается на domObj,
  // paramA, paramB и largeObject
    function () {
      paramA.doSomething();
      paramB.somethingElse();
    },
  false);
}

В этом примере domObj содержит ссылку на подставляемую функцию (через слушатель событий), а замыкание подставляемой функции хранит ссылку обратно на эту переменную. Поскольку largeObject не используется, предполагалось, что он выйдет из области видимости и будет удален; однако ссылка в замыкании удерживает его в памяти, и domObj остается там же. Эта круговая ссылка приведет к утечке, если только domObj не удалит ссылку слушателя событий или не будет присвоен null и не подвергнется сбору мусора. Корректный способ решения подобной проблемы — использовать функцию, которая возвращает функцию, выполняющую ваши задачи, как показано на рис. 6.

Рис. 6. Применение области видимости функций для предотвращения круговых ссылок замыканий

function getOnClick(paramA, paramB) {
  // Это замыкание функции содержит ссылки на paramA и paramB
  return function () {
    paramA.doSomething();
    paramB.somethingElse();
  };
}
function addClickHandlerCorrectly(domObj, paramA, paramB, largeObject) {
  domObj.addEventListener(
    "click",
  // Поскольку largeObject не передается в getOnClick, ссылка
  // замыкания на него не создается и утечка не происходит
  getOnClick(paramA, paramB),
  false);
}

При таком решении ссылка замыкания на domObj исключается, но ссылки на paramA и paramB существуют по-прежнему, так как они нужны для реализации обработчика событий. Чтобы в paramA или paramB не было утечек, вам все равно требуется либо отменять регистрацию слушателя событий, либо просто ждать их автоматического удаления, когда domObj попадет под сбор мусора.

Аннулируйте все URL, созданные URL.createObjectURL Обычный способ загрузки данных для элемента audio, video или img — вызов метода URL.createObjectURL для создания URL, которым может пользоваться этот элемент. Этот метод сообщает системе хранить внутреннюю ссылку на ваши данные. Система использует эту внутреннюю ссылку для потоковой передачи объекта соответствующему элементу. Однако системе не известно, когда эти данные больше не нужны, и она хранит внутреннюю ссылку в памяти до тех пор, пока вы явно не укажете ей освободить эту ссылку. Такие внутренние ссылки могут расходовать значительные объемы памяти, и очень легко ненамеренно оставить их в памяти, когда необходимости в них больше нет. Освободить эти ссылки можно двумя способами.

  1. Вы аннулируете URL, явным образом вызывая метод URL.revokeObjectURL с передачей этого URL.
  2. Вы сообщаете системе автоматически аннулировать этот URL после того, как он один раз был использован. Для этого свойство oneTimeOnly для URL.createObjectURL устанавливается в true:
var url = URL.createObjectURL(blob, {oneTimeOnly: true});

Используйте слабые ссылки на временные объекты Вообразите, что у вас есть большой объект, на который ссылается узел Document Object Model (DOM), и что вам нужно использовать его в различных частях вашего приложения. Теперь допустим, что этот объект может быть освобожден в любой момент (скажем, node.innerHTML = ""). Как гарантированно избежать сохранения ссылок на этот объект, чтобы он в любой момент мог быть полностью освобожден? К счастью, Windows Runtime предоставляет решение этой проблемы, позволяющее хранить так называемые слабые ссылки (weak references) на объекты. Слабая ссылка не мешает GC очистить объект, на который она ссылается, и при разыменовании (dereference) она может вернуть либо этот объект, либо null. Чтобы лучше понять, насколько это полезно, рассмотрим пример на рис. 7.

Рис. 7. Утечка памяти в JavaScript-коде

function addOptionsChangedListener () {
  // WinRT-объект
  var query = Windows.Storage.KnownFolders.picturesLibrary.createFileQuery();
  // Переменная data – это JS-объект, срок жизни которого будет
  // связан с поведением приложения. Вообразите, что на него
  // ссылается DOM-узел, который может быть освобожден в любой
  // момент. В этом примере он просто моментально выходит
  // из области видимости для имитации проблемы.
  var data = {
    _query: query,
    big: new Array(1000).map(function (i) { return i; }),
    someFunction: function () {
      // Здесь что-то делаем
    }
  };
  // Событие WinRT-объекта обрабатывается обратным вызовом
  // JavaScript, который захватывает ссылку на данные
  query.addEventListener("optionschanged", function () {
    if (data)
      data.someFunction();
  });
  // Прочий код...
}

В этом примере объект данных не удаляется, так как на него ссылается слушатель событий в запросе. Поскольку в приложении предполагалось очистить объект данных (и дальнейших попыток сделать это не планировалось), теперь мы имеем утечку памяти. Чтобы избежать этого, можно использовать API-группу WeakWinRTProperty со следующим синтаксисом:

msSetWeakWinRTProperty(WinRTObj, "objectName", objectToStore);

WinRTObj — это любой WinRT-объект, поддерживающий IWeakReference, objectName — ключ доступа к данным и objectToStore — данные, которые нужно хранить.

Чтобы получить информацию, используйте:

var weakPropertyValue = msGetWeakWinRTProperty(WinRTObj, "objectName");

WinRTObj — WinRT-объект, где было сохранено свойство, а objectName — ключ, под которым были сохранены данные.

Возвращаемое значение равно null или изначально сохраненному значению (objectToStore).

На рис. 8 показан один из способов устранения утечки в функции addOptionsChangedListener.

Рис. 8. Использование слабых ссылок для предотвращения утечки памяти

function addOptionsChangedListener() {
  var query = Windows.Storage.KnownFolders.picturesLibrary.createFileQuery();
  var data = {
    big: new Array(1000).map(function (i) { return i; }),
    someFunction: function () {
      // Здесь что-то делаем
    }
  };
  msSetWeakWinRTProperty(query, "data", data)
  query.addEventListener("optionschanged", function (ev) {
    var data = msGetWeakWinRTProperty(ev.target, "data");
    if (data) data.someFunction();
  });
}

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

Проектирование архитектуры приложений Windows Store с использованием JavaScript

Проектирование приложения с учетом эффективного использования ресурсов может уменьшить потребность в срочных исправлениях (spot fixes) и приемах кодирования, специфичных для управления памятью, за счет изначального повышения устойчивости этого приложения к утечкам. Оно также позволяет предусматривать меры, упрощающие выявление утечек, если они все же происходят. В этом разделе мы обсудим два способа проектирования архитектуры приложения Windows Store, написанного на JavaScript. Эти способы можно использовать как независимо, так и совместно для создания приложения, эффективно работающего с ресурсами и простого в сопровождении.

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

var dispose = function (obj) {
  /// <summary>Безопасный вызов dispose объекта.</summary>
  /// <param name="obj">Объект, подлежащий удалению.</param>
  if (obj && obj.dispose) {
    obj.dispose();
  }
  obj = null;
};

Цель в том, чтобы у приложения была древовидная структура, где каждый объект имеет внутренний метод dispose, освобождающий свои ресурсы вызовами методов dispose во всех объектах, на которые ссылается данный объект, и т. д. Благодаря этому, чтобы полностью освободить объект и все его ссылки, достаточно вызвать dispose(obj)!

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

Архитектура Bloat Эта архитектура «раздувания» (bloat) упрощает выявление утечек памяти, делая объекта по-настоящему большими перед тем, как вы освобождаете их. Тем самым, если объект на самом деле не освобождается, его влияние на TWS вашего приложения будет более чем очевидным. Конечно, этот шаблон следует применять только на этапе разработки. Поставлять заказчикам приложение с таким кодом нельзя, поскольку резкие скачки в использовании памяти (даже временные) могут привести к тому, что система принудительно завершит другие приостановленные приложения.

Чтобы искусственно раздуть объект, можно поступить очень просто, например подключить к нему очень большой массив. Использование синтаксиса join позволяет быстро заполнить весь массив какими-то данными и сделать подключенный к нему объект заметно больше:

var bloatArray = [];
bloatArray.length = 50000;
itemToBloat.leakDetector = bloatArray.join("#");

Для эффективного использования этого шаблона нужен хороший способ идентификации того момента, когда объект считается освобожденным кодом. Это можно делать вручную для каждого освобождаемого объекта, но есть два способа получше. Если вы используете только что рассмотренную архитектуру Dispose, просто добавьте «раздувающий» код в метод dispose для анализируемого объекта. Тогда при вызове dispose вы узнаете, действительно ли данный объект удалил все свои ссылки. Второй подход — использование JavaScript-события DOMNodeRemoved для любых элементов, находящихся в DOM. Поскольку это событие срабатывает до удаления узла, вы можете «раздуть» размеры этих объектов и увидеть, освобождаются ли они на самом деле.

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

Заключение

Надеюсь, я сумел дать вам прочный фундамент для идентификации, диагностики и устранения утечек памяти в приложениях Windows Store. Утечки часто возникают из-за недопонимания того, как выделяется память под данные и как она возвращается системе. Знание этих нюансов в сочетании с простыми приемами вроде явного обнуления ссылок на крупные объекты позволит вам создавать эффективные приложения, не замедляющие работу компьютеров даже за несколько дней непрерывной работы с ними. Если вам нужна дополнительная информация, проверьте в MSDN Library статью «Understanding and Solving Internet Explorer Leak Patterns» (bit.ly/Rrta3P) от группы Internet Explorer, которая охватывает смежную тематику (bit.ly/Rrta3P).


Дэвид Теппер (David Tepper) — менеджер программ в группе Windows Application Experience. Занимается проектированием модели приложений и их развертыванием с 2008 г., уделяя основное внимание производительности приложений Windows Store и тому, как эти приложения могут расширять Windows для обеспечения глубоко интегрируемой функциональности.

Выражаю благодарность за рецензирование статьи экспертам Джерри Даниетцу (Jerry Dunietz), Майку Хиллбергу (Mike Hillberg), Матиасу Журдо (Mathias Jourdain), Кеймен Мутафов (Kamen Moutafov), Бренту Ректору (Brent Rector) и Чипало Стриту (Chipalo Street).