Опубликован: 21.01.2010 | Доступ: свободный | Студентов: 1142 / 195 | Оценка: 3.88 / 3.81 | Длительность: 11:48:00
Специальности: Программист
Самостоятельная работа 2:

Особенности управления памятью при создании мобильных приложений

< Лекция 6 || Самостоятельная работа 2: 12 || Лекция 7 >
Аннотация: Уровни управления памятью. Служебные данные приложения. Управление памятью на микроскопическом "уровне алгоритма". "Структуры" и .NET Compact Framework. Использование строк в алгоритмах.

Цель лабораторной работы - знакомство с основными методами и способами управления памятью при создании мобильного приложения.

Для выполнения данной лабораторной работы необходимо наличие ранее созданного проекта. Изучив теоретические основы управления памятью при создании приложений для мобильных устройств, необходимо применить описанные приемы и методы в созданном приложении (разумеется, при наличии таковой возможности). Результатом лабораторной работы должна являться таблица сравнения результатов выполнения алгоритмов до оптимизации и после, при аналогичной программно - аппаратной базе.

Задание на лабораторную работу

  1. Ознакомиться с уровнями управления памятью.
  2. Применить метод Dispose() в целесообразных ситуациях, если в использовании данного метода нет необходимости - обосновать причины такого решения.
  3. Рассмотреть модели загрузки данных по требованию.
  4. Разбить программный код приложения на "структуры".
  5. Оптимизировать работу со строками.
  6. Составить сравнительную таблицу времени выполнения алгоритмов до и после оптимизации.

8.1. Уровни управления памятью:

  1. Управление памятью на макроскопическом "уровне приложения". Этот уровень относится к данным и ресурсам уровня приложения, которые поддерживаются вашим приложением в процессе выполнения. Эти данные обычно существуют в течение длительного времени, и их область видимости не ограничивается пределами отдельных функций. Для создания эффективно функционирующего мобильного приложения очень важно иметь надежную модель, управляющую объемом данных, подлежащих хранению в памяти в каждый момент времени, и удалением из памяти данных и ресурсов, непосредственное использование которых в ближайшее время не ожидается. Чрезмерный объем долгоживущих данных состояния загромождает память, которую можно было бы использовать для кэширования JIT-компилированного кода или как рабочую память для функций, и заставляет многократно и не самым эффективным образом очищать память от "мусора".
  2. Распределение памяти па микроскопическом "уровне алгоритма". Временную память для выполнения команд, определяемых вашими алгоритмами, распределяют функции. Эффективность этого процесса зависит от вашей стратегии реализации алгоритмов. Например, при написании кода, который должен выполняться в циклах, необходимо как можно тщательнее продумывать его эффективность в отношении использования ресурсов, чтобы свести к минимуму непроизводительные накладные расходы. Уделяя пристальное внимание эффективности распределения памяти в создаваемых вами алгоритмах, вы сможете значительно повысить общую производительность приложения.

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

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

Макромодель управления памятью

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

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

8.2. Служебные данные приложения

Эти данные представляют собой ресурсы, необходимые для эффективного функционирования приложения, а также отображения данных и манипулирования ими.

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

  1. Загрузка данных из базы данных. Этот экран предоставляет пользователю возможность подтвердить свои права доступа к базе данных и загрузить данные конкретного пациента.
  2. Сохранение данных в базе данных. Этот экран предоставляет пользователю возможность подтвердить свои права доступа к базе данных и сохранить данные конкретного пациента.
  3. Основной экран приложения. Этот экран отображает данные истории болезни пациента, которые были загружены из базы данных, и предоставляет пользователю устройства возможность просматривать данные и переходить от одних данных к другим.
  4. Экран, отображающий подробную информацию, необходимую для работы с конкретными данными и их редактирования. Этот экран отображается тогда, когда пользователю необходимо редактировать отдельные элементы данных о пациенте или вводить новые данные.
  5. Экран диаграмм для отображения наборов точек, соответствующих данным. Этот экран предназначается для отображения графической информации, связанной с некоторыми аспектами истории болезни пациента. Например, могут быть построены диаграммы, представляющие изменение кровяного давления или количество белых кровяных телец с течением времени, по которым можно судить о наличии инфекции. К необходимым служебным данным приложения относятся следующие данные:
    • Графические перья и кисти, используемые для рисования диаграмм.
    • Объекты шрифтов, используемых для отображения надписей на диаграммах.
    • Кэшированные фоновые изображения.
  6. Внеэкранная поверхность растрового изображения, используемая для подготовки изображения диаграммы перед его копированием на экран.

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

Если объект имеет метод Dispose, то вы всегда можете вызвать этот метод, если необходимость в использовании данного объекта в приложении отпала. Метод Dispose () должен вызываться тогда, когда вы собираетесь удалить любые переменные ссылки на него, чтобы предоставить сборщику мусора возможность удалить этот объект из памяти. Тем самым гарантируется, что дорогостоящий объект, представляемый ресурсом, будет немедленно освобожден. Как и в других ситуациях, имеющих отношение к управлению памятью, если в случае приложений для настольных компьютеров такой подход является просто плодотворным, то в случае мобильных приложений, испытывающих дефицит системных ресурсов (таких, например, как дескрипторы операционной системы), его применение жизненно необходимо.

Управление объемом пользовательских данных, хранящихся в памяти

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

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

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

Использование модели загрузки данных по требованию

Для размещения объектов в памяти существуют две стратегии:

  1. При вхождении приложения в новое состояние создаются все объекты, которые требуются для этого состояния. Достоинством этой стратегии является ее простота. Когда приложение переходит в новое состояние, вы просто вызываете функцию, которая и обеспечивает доступность и возможность использования всех необходимых объектов. Эта стратегия очень хорошо работает в тех случаях, когда имеется уверенность в том, что в ближайшее время приложению потребуются все созданные объекты. Возможные проблемы связаны с тем, что если ваше приложение находится в стадии становления и в его проект могут вноситься изменения то применение указанной стратегии может привести к хранению в памяти большого количества ненужных объектов. Поскольку старые объекты, необходимости в которых больше нет, все равно создаются и загружаются в память, то драгоценные ресурсы тратятся понапрасну. Будьте внимательны при групповом создании наборов объектов, ибо в процессе выполнения вашего приложения может наступить такой момент, когда создаваемые объекты не используются, но связанные с ними накладные расходы ухудшают производительность.
  2. Создание любого объекта откладывается до тех пор, пока необходимость в его создании не станет очевидной. Эта модель немного сложнее в проектировании, но зато во многих случаях оказывается более эффективной, поскольку объекты создаются лишь тогда, когда в них возникает действительная необходимость. При обсуждении этой модели часто употребляются такие выражения, как "фабрика классов" ("class factory"), "диспетчер ресурсов" ("resource dispenser ) и отложенная загрузка" ("lazy loading").

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

  1. Пакетное создание групповых ресурсов. Приведенный ниже код создает списочный массив, содержащий четыре растровых изображения. Эти изображения являются кадрами анимации, поэтому они загружаются все вместе и помещаются в индексированный массив, откуда их можно легко извлекать. Программный код, которому требуется доступ к этой коллекции изображений, должен использовать вызов GraphicsGlobals. PlayerBitmapsCollection()/. Если массив изображений уже загружен в память, функция незамедлительно возвращает кэшированный объект. В противном случае отдельные ресурсы изображений сначала загружаются в массив и лишь затем возвращаются. Если приложение переходит в состояние, в котором пребывание изображений в памяти не требуются, код приложения может выполнить вызов GraphicsGlobals.g_PlayerBitmapsCollection_CleanUp();, в результате чего произойдет освобождение растровых ресурсов и массива. Системные ресурсы, задействованные для обслуживания растровых изображений, будут немедленно освобождены, а управляемая память, которую занимали эти объекты, будет соответствующим образом восстановлена в процессе сборки мусора.
  2. Индивидуальное создание графических ресурсов. В случае ресурсов, которые не должны обязательно использоваться вместе, как в приведенном выше примере, часто оказывается удобным создать функцию кэшированного доступа, посредством которой и реализуется управление доступом к ресурсу. Когда происходит первое обращение к этой функции с запросом ресурса (например, GraphicsGlobals .g_GetBlackPen ();), она создает его экземпляр. В случае часто используемых ресурсов такой подход оказывается намного более эффективным, чем постоянное создание и уничтожение экземпляров ресурса всякий раз, когда он требуется для выполнения того или иного фрагмента кода. Создавая приведенный ниже код, я допустил, что все ресурсы должны освобождаться одновременно, и написал функцию ( GraphicsGlobals.g_CleanUpDrawingResources (); ), которая освобождает все каптированные ресурсы, которые были созданы. Эта функция должна вызываться тогда, когда приложение переходит в состояние, в котором эти ресурсы не требуются.
public static void g_PlayerBitmapsCollection_CleanUp(){
//Если не загружено ни одно изображение, то и память освобождать не от чего if(s_colPlayerBitmaps == null)  
{ return; }
//Дать указание каждому из этих объектов освободить 
//любые удерживаемые ими неуправляемые ресурсы s_Player_Bitmapl.Dispose();   
s_Player_Bitmap2.Dispose(); 
s_Player_Bitmap3.Dispose(); 
s_Player_Bitmap4.Dispose();
//Обнулить каждую из этих переменных, чтобы им не соответствовали 
//никакие объекты в памяти
s_Player_Bitmapl = null; 
s_Player_Bitmap2 = null; 
s_Player_Bitmap3 = null;        
s_Player_Bitmap4 = null;
//Избавиться от массива s_colPlayerBitmaps = null;
//Функция: возвращает коллекцию изображений
public static System.Collections.ArrayList g_PlayerBitmapsCollection()
{
//	
//Если изображения уже загружены, их достаточно только возвратить
//	
if(s_colPlayerBitmaps != null)  {return s_colPlayerBitmaps;)
 //Загрузить изображения как ресурсы из исполняемого двоичного файла 
System.Reflection.Assembly thisAssembly =System.Reflection.Assembly.GetExecutingAssembly(); 
System.Reflection.AssemblyName thisAssemblyName =thisAssembly.GetName();
string assemblyName = thisAssemblyName.Name; /
/Загрузить изображения
s_Player_Bitmapl = new System.Drawing.Bitmap(thisAssembly.GetManifestResourceStream(assemblyName + 
".HankJRightRunl.bmp"));
s_Player_Bitmap2 = new System.Drawing.Bitmap(thisAssembly.GetManifestResourceStream(assemblyName +
".Hank_RightRun2.bmp")); 
s_Player_Bitmap3 = new System.Drawing.Bitmap(thisAssembly.GetManifestResourceStream(assemblyName +
".Hank_LeftRunl.bmp")); 
s_Player_Bitmap4 = new System.Drawing.Bitmap(thisAssembly.GetManifestResourceStream(assemblyName +
".Hank_LeftRun2.bmp"));
//Добавить изображения в коллекцию
s_colPlayerBitmaps = new System.Collections.ArrayList(); 
s_colPlayerBitmaps.Add(s_Player_Bitmapl);
 s_colPlayerBitmaps.Add(s_Player_Bitmap2); 
s_colPlayerBitmaps.Add(s_Player_Bitmap3); 
s_colPlayerBitmaps.Add(s_PlayerJBitmap4);
//Возвратить коллекцию return s_colPlayerBitmaps;
}
private static System.Drawing.Pen s_blackPen; 
private static System.Drawing.Pen s_whitePen; 
private static System.Drawing.Imaging.ImageAttributes s_ImageAttribute;
private static System.Drawing.Font s_boldFont;
//	
//Вызывается для освобождения от любых графических
//ресурсов, которые могли быть кэшированы
//	
private static void g_CleanUpDrawingResources() {
//Освободить память от черного пера, если таковое имеется if(s_blackPen != null)
{s_blackPen.Dispose(); s_blackPen = null;}
// Освободить память от белого пера, если таковое имеется if(s_whitePen != null)
{ s_whitePen.Dispose(); s_whitePen = null;}
//Освободить память от атрибута ImageAttribute, если таковой имеется. 
//Примечание. Метод Dispose() для этого типа не предусмотрен, 
//поскольку все его данные являются управляемыми 
if(s_ImageAttribute != null) {s_ImageAttribute = null;}
//Освободить память от полужирного шрифта, если таковой имеется 
if(s_boldFont != null)
{s_boldFont.Dispose(); s_boldFont = null;}
//	
//Эта функция позволяет получить доступ
//к черному перу, находящемуся в кэш-памяти
//	
private static System.Drawing.Pen g_GetBlackPen() {
//Если перо еще не существует, создать его
if(s_blackPen == null)
{
s_blackPen = new System.Drawing.Pen( System.Drawing.Color.Black) ;
}
//Возвратить черное перо return s blackPen;
//	
//Эта функция позволяет получить доступ
//к белому перу, находящемуся в кэш-памяти
//	
private static System.Drawing.Pen g_GetWhitePen() {
//Если перо еще не существует, создать его
if(s_whitePen == null)
{s_whitePen   = new System.Drawing.Pen(
System.Drawing.Color.White);} //Возвратить белое перо return s_whitePen;
}
//	
//Эта функция позволяет получить доступ
//к полужирному шрифту, находящемуся в кэш-памяти
//	
private static System.Drawing.Font g_GetBoldFont() {
//Если перо еще не существует, создать его
if(s_boldFont == null)
{
s_boldFont   = new System.Drawing.Font( System.Drawing.FontFamily.GenericSerif, 10, System.Drawing.FontStyle.Bold) ;
}
//Возвратить полужирный шрифт return s_boldFont;
}
//	
//Эта функция позволяет осуществлять доступ
//к находящемуся в кэш-памяти объекту imageAttributes,
// который мы используем для изображений с прозрачностью
//	private static System.Drawing.Imaging.ImageAttributes g_GetTransparencyImageAttribute()
{
//Если объект не существует, создать его
if(s_ImageAttribute == null)
{
//Создать атрибут изображения s_ImageAttribute =new System.Drawing.Imaging.ImageAttributes(); 
s_ImageAttribute.SetColorKey(System.Drawing.Color.White,
System.Drawing.Color.White);
}
//Возвратить его return s_ImageAttribute;
}
} //Конец класса
< Лекция 6 || Самостоятельная работа 2: 12 || Лекция 7 >