Текст книги "Программирование мобильных устройств на платформе .NET Compact Framework"
Автор книги: Иво Салмре
Жанр:
Программирование
сообщить о нарушении
Текущая страница: 23 (всего у книги 69 страниц)
Как ранее уже отмечалось, "служебные" данные приложения – это те данные, с которыми пользователь непосредственно не взаимодействует и которые, таким образом, не являются "пользовательскими" данными. Эти данные представляют собой ресурсы, необходимые для эффективного функционирования приложения, а также отображения данных и манипулирования ими. Управление объектами и ресурсами, необходимыми для того, чтобы приложение могло эффективно выполняться, обычно может осуществляться с помощью простого конечного автомата. По мере прохождения приложением различных состояний ресурсы, которые полезно иметь под рукой, могут меняться. При переходе в новое состояние необходимые ресурсы могут создаваться и сохраняться в памяти приложения. Аналогичным образом, если приложение покидает некоторое состояние, то ресурсы, которые в новом состоянии непосредственно не требуются, могут быть удалены из памяти, а освобожденная память возвращена в пул свободной памяти. Часто эти состояния соответствуют отображаемым в данный момент формам или их вкладкам, которые пользователь имеет возможность листать, поочередно вызывая их на передний план. Во многих случаях при проектировании модели состояний приложения полезно идентифицировать дискретный ряд возможных режимов работы пользовательского интерфейса и применять их в качестве основы для построения модели состояний.
Рассмотрим в качестве примера простое приложение, ориентированное на работу с базами данных. Данное приложение обеспечивает хранение и обработку медицинских данных пациентов. Данные хранятся в базе данных и загружаются порциями, которые соответствуют отдельным пациентам, причем для загрузки или сохранения данных о пациенте требуется ввести защитный пароль. Для приложений такого рода можно выделить пять различных дискретных состояний, в которых пользователю для выполнения соответствующих задач предоставляются различные пользовательские интерфейсы. Такими состояниями являются следующими:
1. Загрузка данных из базы данных. Этот экран предоставляет пользователю возможность подтвердить свои права доступа к базе данных и загрузить данные конкретного пациента. К необходимым служебным данным приложения относятся следующие данные:
• Соединения с базой данных.
• Форма, отображающая пользовательский интерфейс, необходимый для входа в базу данных.
2. Сохранение данных в базе данных. Этот экран предоставляет пользователю возможность подтвердить свои права доступа к базе данных и сохранить данные конкретного пациента. К необходимым служебным данным приложения относятся следующие данные.
• Соединения с базой данных.
• Форма, отображающая пользовательский интерфейс, необходимый для входа в базу данных.
3. Основной экран приложения. Этот экран отображает данные истории болезни пациента, которые были загружены из базы данных, и предоставляет пользователю устройства возможность просматривать данные и переходить от одних данных к другим. К необходимым служебным данным приложения относятся следующие данные:
• Форма для основного экрана.
• Изображения общего назначения, используемые в пользовательском интерфейсе.
4. Экран, отображающий подробную информацию, необходимую для работы с конкретными данными и их редактирования. Этот экран отображается тогда, когда пользователю необходимо редактировать отдельные элементы данных о пациенте или вводить новые данные. К необходимым служебным данным приложения относятся следующие данные:
• Форма для редактирования загруженной записи.
• Пользовательские элементы управления формы, обеспечивающие нестандартные возможности ввода данных (например, пользовательский элемент управления для ввода данных о кровяном давлении с одновременной проверкой корректности ввода или пользовательский элемент управления для редактирования информации о медикаментозных дозах).
5. Экран диаграмм для отображения наборов точек, соответствующих данным. Этот экран предназначается для отображения графической информации, связанной с некоторыми аспектами истории болезни пациента. Например, могут быть построены диаграммы, представляющие изменение кровяного давления или количество белых кровяных телец с течением времени, по которым можно судить о наличии инфекции. К необходимым служебным данным приложения относятся следующие данные:
• Графические перья и кисти, используемые для рисования диаграмм.
• Объекты шрифтов, используемых для отображения надписей на диаграммах.
• Кэшированные фоновые изображения.
• Внеэкранная поверхность растрового изображения, используемая для подготовки изображения диаграммы перед его копированием на экран.
Некоторые из этих состояний разделяют общие ресурсы. Например, графическое перо черного цвета, или шрифт определенного размера, или растровое изображение могут использоваться в нескольких из перечисленных выше состояний. В соединении с базой данных нуждаются два состояния. Кроме того, некоторые объекты могут не требоваться для всех без исключения состояний, но их создание занимает длительное время; в этом случае целесообразно прибегнуть к кэшированию объектов, которое предварительно следует протестировать с точки зрения влияния на производительность.
Создать конечный автомат для управления этими видами ресурсов не составляет особого труда. Все, что требуется сделать при вхождении в новое состояние – это определить, какие объекты необходимо создать, если они к этому времени еще не существуют, и какие из размещенных в предыдущем cocтоянии объектов должны быть удалены из памяти. Управление этой логикой с помощью всего лишь одного конечного автомата, а не посредством кода, разбросанного по всему приложению, позволяет легко поддерживать данную информацию. Кроме того, конечный автомат облегчает проведение экспериментов с различными схемами оптимизации и настройку производительности приложения.
Управление объемом пользовательских данных, хранящихся в памятиИспользование интерфейса IDisposable и освобождение дорогостоящих неуправляемых ресурсов в .NET Compact Framework
Если объект больше не используется, код вашего приложения может освободить его, удалив все ссылки, которые на него указывают. Занимаемая объектом память будет восстановлена, когда во время очередной сборки мусора (обычно это происходит тогда когда приложению необходимо распределить память) среда выполнения обнаружит, что на данный объект не указывает ни одна ссылка. Обычно такая схема вас будет вполне устраивать, но из нее следует, что удаление объектов из памяти может откладываться на неопределенное время и производиться лишь тогда, когда острая необходимость в дополнительной памяти заставит систему произвести сборку мусора. Поскольку мы не можем точно сказать, когда именно это произойдет, а, следовательно, и судить о том, когда именно будет удален объект, о времени жизни такого объекта в памяти говорят как о "недетерминированном". Недетерминированность очистки памяти от ненужных объектов является обычной проблемой систем управления памятью, использующих сборку мусора. Поэтому, как правило, следует избегать использования в логике приложения кода, выполнение которого связывается с вызовом деструктора объекта поскольку в этом случае приложение не может явно контролировать, когда именно будет выполнен такой код.
Проблемы могут возникать также в тех случаях, когда освобождаемый за ненадобностью объект представляет такой "дорогостоящий неуправляемый ресурс", как соединение с базой данных, файловый дескриптор или некоторый графический ресурс. До тех пор, пока объект не будет удален из памяти во время сборки мусора, неуправляемый ресурс, который он представляет, будет удерживаться приложением. Это может отрицательно сказываться на производительности системы как в отношении клиентского устройства, так и в отношении ресурсов сервера удерживаемых клиентом.
Для того чтобы справиться с этой проблемой, в .NET Compact Framework предусмотрена схема позволяющая коду явно освобождать базовый ресурс, удерживаемый объектом. Для всех объектов, которые представляют дорогостоящие ресурсы, предусмотрен метод Dispose(). Определение этого метода содержится в интерфейсе IDisposable; классы, поддерживающие детерминированный отказ от своих ресурсов, реализуют этот интерфейс и, таким образом, включают в себя метод Dispose().
Если объект имеет метод Dispose(), то вы всегда можете вызвать этот метод, если необходимость в использовании данного объекта в приложении отпала. Метод Dispose() должен вызываться тогда, когда вы собираетесь удалить любые переменные ссылки на него, чтобы предоставить сборщику мусора возможность удалить этот объект из памяти. Тем самым гарантируется, что дорогостоящий объект, представляемый ресурсом, будет немедленно освобожден. Как и в других ситуациях, имеющих отношение к управлению памятью, если в случае приложений для настольных компьютеров такой подход является просто плодотворным, то в случае мобильных приложений, испытывающих дефицит системных ресурсов (таких, например, как дескрипторы операционной системы). ею применение жизненно необходимо.
Точно так же, проектируя класс, который представляет дорогостоящий ресурс, вы должны реализовать интерфейс IDisposable, тем самым предоставляя коду, использующему этот класс, возможность детерминированного освобождения удерживаемых данным классом ресурсов. Для подробного ознакомления с деталями надлежащей реализации этого свойства обратитесь к той части документации .NET Compact Framework, в которой описывается метод IDisposable.Dispose().
На заметку! В языке C# введено специальное ключевое слово, упрощающее вызов метода Dispose() в тех случаях, когда область использования ресурса ограничивается блоком кода функции. Вместо обязательного явного вызова метода .Dispose() можно объявить соответствующую переменную с помощью ключевого слова using.
Например:
using(System.Drawing.Graphics myGfx = System.Drawing.Graphics.FromImage(myImage)) {
//Выполнение всей необходимой работы с помощью функции myGfx…
} // Метод myGfx.Dispose() вызывается здесь автоматически…В случае использования такого объявления метод Dispose() вызывается автоматически при выходе за пределы области видимости переменной. Ключевое слово using очень удобно использовать в тех случаях, когда переменная существует только внутри некоторого блока кода.
Пользовательские данные представляют собой фактические данные, которые может просматривать или которыми может манипулировать пользователь приложения. Управление объемом и временем жизни пользовательских данных, хранящихся в памяти, может оказаться более сложным по сравнению с управлением служебными данными приложения, поскольку в зависимости от структуры и назначения приложения природа данных, сохраняемых в памяти, может быть самой различной. Пользовательские данные шахматной игры отличаются своей структурой от пользовательских данных истории болезни пациента. Состояние шахматной доски может храниться в целочисленном массиве фиксированных размеров. Объем данных истории болезни не может быть заранее определен; эти данные могут включать в себя результаты измерений, текстовые записи, изображения, ссылки на дополнительные данные и почти неограниченный объем любой другой подходящей информации.
Управление состоянием шахматной доски обычно сводится к принятию решений относительно того, когда именно следует загружать массив данных в память, сохранять его и удалять из памяти. С этим можно справиться при помощи конечного автомата, предназначенного для решения этой задачи.
Управление памятью, используемой для хранения сложных записей истории болезни пациента, выглядит значительно сложнее. Указанные записи могут иметь произвольный размер, а это, вероятно, означает, что вам придется прибегнуть для работы с ними к оконной модели, в которой пользователь устройства обеспечивается частичным представлением набора данных, причем не все данные должны храниться в памяти одновременно. У пользователя создается впечатление, что в памяти находятся все данные, тогда как на самом деле осуществляется подкачка необходимых данных. Соответствующий конечный автомат может быть как очень простым, так и необычайно сложным, в зависимости от того, что собой представляют управляемые данные, и какие ресурсы доступны для управления этими данными.
Для управления потенциально сложными пользовательскими данными целесообразно использовать подход, предполагающий создание класса с хорошо продуманной инкапсуляцией, который и реализует управление состоянием, хранящимся в памяти в каждый момент времени. Этот класс отвечает за загрузку новых данных и освобождение памяти от старых данных, когда необходимость в них отпадает. Он управляет доступом к пользовательским данным извне и создает иллюзию неограниченных запасов памяти. Любой другой код приложения, осуществляющий доступ к данным извне инкапсулирующего класса, ничего не должен знать о внутреннем состоянии этого класса, реализующего фактическое управление пользовательскими данными. Возложение ответственности за управление всеми пользовательскими данными на определенный класс обеспечивает значительную гибкость в процессе проектирования. Некоторые из преимуществ такого подхода перечисляются ниже:
■ Возможность автоматического управления объемом загруженных данных. Если вы обнаруживаете, что приложение испытывает острый дефицит памяти, можно уменьшить размеры окна данных, удерживаемых в памяти в каждый момент времени, не прибегая к внесению изменений в пределах всего приложения. Поскольку о том, какие данные кэшированы в памяти, а какие нуждаются в повторной загрузке, вне данного класса ничего не известно, вы получаете более гибкие возможности для настройки этого алгоритма.
■ Возможность иметь различные реализации для различных классов устройств. Если ваши целевые устройства охватывают несколько различных классов, то вы имеете возможность настроить ограничения на использование памяти и накопителей для каждого из этих классов по отдельности. Мобильный телефон и устройство PDA могут иметь различные характеристики памяти и различные возможности в отношении загрузки данных по требованию. Отмеченные различия могут потребовать от вас использования различных подходов к кэшированию данных. Концентрация соответствующей управляющей логики в одном месте существенно упрощает эту задачу.
Использование модели загрузки данных по требованиюДля размещения объектов в памяти существуют две стратегии:
1. При вхождении приложения в новое состояние создаются все объекты, которые требуются для этого состояния. Достоинством этой стратегии является ее простота. Когда приложение переходит в новое состояние, вы просто вызываете функцию, которая и обеспечивает доступность и возможность использования всех необходимых объектов. Эта стратегия очень хорошо работает в тех случаях, когда имеется уверенность в том, что в ближайшее время приложению потребуются все созданные объекты. Возможные проблемы связаны с тем, что если ваше приложение находится в стадии становления и в его проект могут вноситься изменения, то применение указанной стратегии может привести к хранению в памяти большого количества ненужных объектов. Поскольку старые объекты, необходимости в которых больше нет, все равно создаются и загружаются в память, то драгоценные ресурсы тратятся понапрасну. Будьте внимательны при групповом создании наборов объектов, ибо в процессе выполнения вашего приложения может наступить такой момент, когда создаваемые объекты не используются, но связанные с ними накладные расходы ухудшают производительность.
2. Создание любого объекта откладывается до тех пор, пока необходимость в его создании не станет очевидной. Эта модель немного сложнее в проектировании, но зато во многих случаях оказывается более эффективной, поскольку объекты создаются лишь тогда, когда в них возникает действительная необходимость. При обсуждении этой модели часто употребляются такие выражения, как "фабрика классов" ("class factory"), "диспетчер ресурсов" ("resource dispenser") и "отложенная загрузка" ("lazy loading").
Приведенный в листинге 8.1 пример кода иллюстрирует два способа отложенного создания и кэширования глобально используемых графических ресурсов. Существует два способа создания объектов:
1. Пакетное создание групповых ресурсов. Приведенный ниже код создает списочный массив, содержащий четыре растровых изображения. Эти изображения являются кадрами анимации, поэтому они загружаются все вместе и помещаются в индексированный массив, откуда их можно легко извлекать. Программный код, которому требуется доступ к этой коллекции изображений, должен использовать вызов GraphicsGlobals.PlayerBitmapsCollection();. Если массив изображений уже загружен в память, функция незамедлительно возвращает кэшированный объект. В противном случае отдельные ресурсы изображений сначала загружаются в массив и лишь затем возвращаются. Если приложение переходит в состояние, в котором пребывание изображений в памяти не требуются, код приложения может выполнить вызов GraphicsGlobals.g_PlayerBitmapsCollection_CleanUp();, в результате чего произойдет освобождение растровых ресурсов и массива. Системные ресурсы, задействованные для обслуживания растровых изображений, будут немедленно освобождены, а управляемая память, которую занимали эти объекты, будет соответствующим образом восстановлена в процессе сборки мусора.
2. Индивидуальное создание графических ресурсов. В случае ресурсов, которые не должны обязательно использоваться вместе, как в приведенном выше примере, часто оказывается удобным создать функцию кэшированного доступа, посредством которой и реализуется управление доступом к ресурсу. Когда происходит первое обращение к этой функции с запросом ресурса (например, GraphicsGlobals.g_GetBlackPen();), она создает его экземпляр. В случае часто используемых ресурсов такой подход оказывается намного более эффективным, чем постоянное создание и уничтожение экземпляров ресурса всякий раз, когда он требуется для выполнения того или иного фрагмента кода. Создавая приведенный ниже код, я допустил, что все ресурсы должны освобождаться одновременно, и написал функцию (GraphicsGlobals.g_CleanUpDrawingResources();), которая освобождает все кэшированные ресурсы, которые были созданы. Эта функция должна вызываться тогда, когда приложение переходит в состояние, в котором эти ресурсы не требуются.
Используя кэширование, руководствуйтесь здравым смыслом
Если предполагается многократное использование одних и тех же ресурсов внутри функции, то вызов функции (или свойства) кэшированного доступа всякий раз, когда требуется данный ресурс, является неэффективным. В подобных случаях лучше вызвать эту функцию один раз и хранить ресурс в локальной переменной в течение всего времени, пока в данном ресурсе будет существовать необходимость. Тем самым вы получаете выигрыш от того, что используете глобально кэшированный ресурс, а не создаете и уничтожаете ресурс внутри функции, и повышаете эффективность вычислений, поскольку избавляетесь от лишних вызовов функции в своем алгоритме. Здравый смысл подскажет вам, когда такой подход является оправданным.
Листинг 8.1. Применение отложенной загрузки, кэширования и освобождения графических ресурсов
using system;
public class GraphicsGlobals {
private static System.Drawing.Bitmap s_Player_Bitmap1;
private static System.Drawing.Bitmap s_Player_Bitmap2;
private static System.Drawing.Bitmap s_Player_Bitmap3;
private static System.Drawing.Bitmap s_Player_Bitmap4;
private static System.Collections.ArrayList s_colPlayerBitmaps;
//–
//Освободить все ресурсы
//–
public static void g_PlayerBitmapsCollection_CleanUp() {
//Если не загружено ни одно изображение, то и память освобождать не от чего
if (s_colPlayerBitmaps == null) {
return;
}
//Дать указание каждому из этих объектов освободить
//любые удерживаемые ими неуправляемые ресурсы
s_Player_Bitmap1.Dispose();
s_Player_Bitmap2.Dispose();
s_Player_Bitmap3.Dispose();
s_Player_Bitmap4.Dispose();
//Обнулить каждую из этих переменных, чтобы им не соответствовали
//никакие объекты в памяти
s_Player_Bitmap1 = 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_Bitmap1 = new System.Drawing.Bitmap(
thisAssembly.GetManifestResourceStream(assemblyName + ".Hank_RightRun1.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_LeftRun1.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_Bitmap1);
s_colPlayerBitmaps.Add(s_Player_Bitmap2);
s_colPlayerBitmaps.Add(s_Player_Bitmap3);
s_colPlayerBitmaps.Add(s_Player_Bitmap4);
//Возвратить коллекцию
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();
r_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;
}
} //Конец класса







