Текст книги "Программирование мобильных устройств на платформе .NET Compact Framework"
Автор книги: Иво Салмре
Жанр:
Программирование
сообщить о нарушении
Текущая страница: 38 (всего у книги 69 страниц)
При написании кода для обработки графики часто оказывается так, что приложению необходимо многократно использовать одни и те же ресурсы. К числу распространенных классов, допускающих повторное использование, относятся битовые изображения (Bitmap), перья (Pen), кисти (Brush) и шрифты (Font). Было бы слишком расточительно многократно загружать или создавать одни и те же ресурсы; на повторную загрузку ресурса из хранилища или его повторный запрос у операционной системы затрачивается дополнительное процессорное время. Не меньшим расточительством является и одновременная загрузка эквивалентных ресурсов в память; например, хранить в памяти несколько экземпляров одного и того же битового изображения – напрасная трата ресурсов, если вместо этого можно обойтись совместным использованием единственного экземпляра. Наконец, освобождение памяти, занимаемой ресурсами, также ложится бременем на систему и снижает ее производительность. Чтобы избежать подобного снижения производительности, иногда оказывается полезным применять в приложении глобальный диспетчер ресурсов, который берет на себя все заботы, связанные с загрузкой, кэшированием и удалением часто используемых ресурсов.
В листинге 11.11 представлен пример, демонстрирующий три подхода к загрузке и обслуживанию ресурсов в памяти.
■ Подход 1, основанный на принципе "открытия задвижки " (Litch-based approach). Любой код, которому требуется общий ресурс, обращается к статическому свойству класса GraphicsGlobals для получения его значения. Если ресурс уже загружен, он возвращается запросившему его объекту. Если ресурс еще не был загружен, то он создается, кэшируется и после этого возвращается пользователю. Такой подход обладает двумя преимуществами: 1) запрашивающий объект не должен заботиться ни о каком инициализирующем коде; в результате запроса всегда будет возвращен действительный ресурс, и 2) управляемый ресурс может быть освобожден, если приложение считает, что в течение некоторого времени в нем не будет надобности; при необходимости он будет заново создан в результате следующего запроса. Единственным недостатком такого подхода являются незначительные дополнительные накладные расходы, связанные с необходимостью вызова функции для доступа к свойству всякий раз, когда требуется ресурс; обычно этими накладными расходами можно пренебречь.
■ Подход 2, основанный на групповой обработке ресурсов (batch-based approach). Если некоторые ресурсы используются одинаковым образом и имеют сравнимые времена существования, то они могут быть инициализированы сразу все вместе. Код, в котором должны будут использоваться эти глобальные ресурсы, может непосредственно получить доступ к переменным, но предварительно он должен удостовериться в том, что они уже были инициализированы. Когда приложение перейдет в состояние, в котором эти ресурсы в течение некоторого времени использоваться не будут, эти ресурсы должны быть освобождены, и для них должен быть вызван метод Dispose().
■ Подход 3, основанный на использовании коллекций (collection-based approach). Если некоторые ресурсы всегда используются в виде группы, как, например, в случае битовых изображений, образующих анимационную последовательность, то имеет смысл загружать их вместе и возвращать в виде массива или коллекции ресурсов. Если загрузка ресурсов обходится слишком дорого или требует использования значительных объемов памяти, то может оказаться целесообразным хранить их экземпляры, кэшированные централизованным образом, чтобы исключить повторное создание одних и тех же ресурсов. Как и в рассмотренном выше случае, когда необходимость в этих ресурсах отпадает, их необходимо освобождать и вызывать для них метод Dispose(), следуя предварительно намеченной стратегии
Листинг 11.11. Три полезных способа кэширования графических ресурсов
using System;
using System.Drawing;
internal class GraphicsGlobals {
//==========================================================================
//Подход 1: Создать ресурс по требованию
// и кэшировать его для последующего использования.
//
//Внешний код получает доступ к общедоступным свойствам для их просмотра, но
//сами переменные остаются внутренними переменными класса
//==========================================================================
private static Pen s_bluePen;
public static Pen globalBluePen {
get {
//Если перо еще не было создано
if (s_bluePen == null) {
s_bluePen =new System.Drawing.Pen(System.Drawing.Color.Blue);
}
return s bluePen;
}
} //Конец свойства
//=========================================================
//Подход 2:
//Загрузить глобально и кэшировать все
//используемые объекты Pen, ImageAttribute, Font и Brush.
//
//Внешний код получает доступ ко всем общедоступным членам,
//так что никакие функции доступа не нужны.
//=========================================================
public static Pen g_blackPen;
public static Pen g_whitePen;
public static System.Drawing.Imaging.ImageAttributes g_ImageAttribute;
private static bool s_alreadyInitialized;
public static Font g_boldFont;
public static Font g_smallTextFont;
public static Brush g_greenBrush;
public static Brush g_yellowBrush;
public static Brush g_redBrush;
public static Brush g_blackBrush;
//==============================================================
//Эта функция должна быть вызвана до попыток доступа к любому из
//вышеперечисленных глобальных объектов
//==============================================================
public static void InitializeGlobals() {
if (s_alreadyInitialized == true) {
return;
}
g_blackPen = new System.Drawing.Pen(Color.Black);
g_whitePen = new System.Drawing.Pen(Color.White);
g_ImageAttribute = new System.Drawing.Imaging.ImageAttributes();
g_ImageAttribute.SetColorKey(Color.White, Color.White);
g_boldFont = new Font(FontFamily.GenericSerif, 10, FontStyle.Bold);
g_smallTextFont = new Font(FontFamily.GenericSansSerif, 8, FontStyle.Regular);
g_blackBrush = new SolidBrush(System.Drawing.Color.Black);
g_greenBrush = new SolidBrush(System.Drawing.Color.LightGreen);
g_yellowBrush = new SolidBrush(System.Drawing.Color.Yellow);
g_redBrush = new SolidBrush(System.Drawing.Color.Red);
s_alreadyInitialized = true;
}
//=========================================================
//Подход 3: Возвратить массив связанных ресурсов.
// Кэшировать ресурсы локально, чтобы при многократных
// запросах не загружались (понапрасну) их дубликаты.
//=========================================================
private static Bitmap m_CaveMan_Bitmap1;
private static Bitmap m_CaveMan_Bitmap2;
private static Bitmap m_CaveMan_Bitmap3;
private static Bitmap m_CaveMan_Bitmap4;
private static System.Collections.ArrayList m_colCaveManBitmaps;
//–
//Создать и загрузить массив изображений для спрайта
//–
public static System.Collections.ArrayList g_CaveManPictureCollection() {
//Изображения загружаются лишь в том случае, если мы их еще не загрузили
if (m_CaveMan_Bitmap1 == null) {
//–
//Загрузить изображения. Эти изображения хранятся в виде
//встроенных ресурсов в нашем двоичном приложении.
//
//Загрузка изображений из внешних файлов осуществляется аналогичным
//образом, но выполнить ее проще (нам достаточно лишь указать
//имя файла в конструкторе растровых изображений).
//–
//Получить ссылку на нашу двоичную сборку
System.Reflection.Assembly thisAssembly = System.Reflection.Assembly.GetExecutingAssembly();
//Получить имя сборки
System.Reflection.AssemblyName thisAssemblyName = thisAssembly.GetName();
string assemblyName = thisAssemblyName.Name;
//Загрузить изображения в виде двоичных потоков из нашей сборки
m_CaveMan_Bitmap1 = new System.Drawing.Bitmap(
thisAssembly.GetManifestResourceStream(
assemblyName + ".Hank RightRunl.bmp"));
m_CaveMan_Bitmap2 = new System.Drawing.Bitmap(
thisAssembly.GetManifestResourceStream(
assemblyName + ".Hank_RightRun2.bmp"));
m_CaveMan_Bitmap3 = new System.Drawing.Bitmap(
thisAssembly.GetManifestResourceStream(
assemblyName + ".Hank_LeftRun1.bmp"));
m_CaveMan_Bitmap4 = new System.Drawing.Bitmap(
thisAssembly.GetManifestResourceStream(
assemblyName + ".Hank_LeftRun2.bmp"));
//Добавить их в коллекцию
m_colCaveManBitmaps = new System.Collections.ArrayList();
m_colCaveManBitmaps.Add(m_CaveMan_Bitmap1);
m_colCaveManBitmaps.Add(m_CaveMan_Bitmap2);
m_colCaveManBitmaps.Add(m_CaveMan_Bitmap3);
m_colCaveManBitmaps.Add(m_CaveMan_Bitmap4);
}
//Возвратить коллекцию
return m_colCaveManBitmaps;
}
} //Конец класса
Выполнение лишних операций размещения и уничтожения объектов в памяти является одной из наиболее распространенных причин ухудшения производительности графического кода. Графический код часто выполняется внутри циклов или многократно вызывается. Кроме того, поскольку графические объекты используют значительные объемы памяти, а также системные ресурсы, с их обслуживанием связаны сравнительно большие накладные расходы. Если ваше мобильное приложение распределяет и освобождает память всякий раз, когда необходимо визуализировать изображения, то вы неминуемо столкнетесь с проблемами нехватки памяти, что обусловит необходимость частого выполнения операций по сборке мусора, замедляющих работу приложения. В связи с этим необходимо предельно внимательно анализировать любые участки кода, требующие распределения памяти для объектов в процессе визуализации графики. Особенно это касается объектов, связанных с графическими ресурсами, но остается справедливым и по отношению к объектам другой природы (например, коллекциям, массивам, строкам), которые могут фигурировать в таком коде.
В частности, вы должны анализировать с этой точки зрения распределение памяти для следующих объектов.
■ Объекты Bitmap. Часто возникают ситуации, в которых в качестве временного хранилища для изображения или части изображения, создаваемых вашим приложением, удобно использовать объект Bitmap. Если вашему приложению требуется пространство для рисования, то в этом нет ничего необычного; единственное, чего следует избегать – так это создания и уничтожения многих битовых изображений в циклах рисования. Обычно гораздо лучше иметь одно рабочее пространство, многократно используемое в качестве общего ресурса, чем нести дополнительные затраты, связанные с непрерывным созданием и уничтожением временных изображений. Размер такой временной памяти должен быть равен размеру наибольшего временного изображения, которое может понадобиться вашим процедурам (но не более того). Если временную память необходимо очищать, прежде чем использовать ее для рисования, то это можно сделать легко и быстро при помощи вызова Graphics.Clear(). Кроме того, как продемонстрировано в приведенном выше примере кода, часто используемые растровые изображения имеет смысл загружать и кэшировать в памяти. Следите за тем, чтобы в каждый момент времени загружался только один экземпляр растрового изображения. Загрузка нескольких экземпляров идентичных изображений приведет к напрасному расходованию больших объемов памяти.
■ Объекты Graphics. Если требуется постоянно перерисовывать растровое изображение, как это, например, приходится делать при перерисовке кадров игрового поля, то, вероятно, наряду с целевым экранным изображением целесообразно кэшировать также объекты Graphics внеэкранных изображений. Поступая таким образом, вы избавитесь от необходимости непрерывного размещения этих объектов в памяти и их удаления. Не распределяйте в циклах рисования память для объектов Graphics растровых изображений или форм более одного раза; в идеальном случае следует вообще избегать размещения и удаления таких объектов внутри циклов. To же самое касается и любых других битовых карт, в которых вы должны выполнять рисование; кэшируйте их объекты Graphics, а не создавайте их каждый раз заново. Учтите, что объекты Graphics нужны только для тех битовых карт, которые вы используете для создания изображений; если битовая карта используется лишь в качестве источника информации об изображении, то объект Graphics для нее вам вообще не нужен.
■ Объекты Font, Brush, Pen, ImageAttribute. Распространенной ошибкой разработчиков, причины которой вполне объяснимы, является их чрезмерное увлечение управлением временем жизни ресурсов на микроуровне, не сопровождающееся глубоким анализом их использования на макроуровне. В процессе создания графического изображения вам может потребоваться пройти, скажем, через 30 отдельных этапов, в ходе которых рисуются линии, закрашиваются эллипсы, создается текст и копируются битовые образы. Выполнение каждой из этих операций требует использования некоторой комбинации перьев, кистей, шрифтов, то есть объектов Pen, Brush, Font, а если в дело включаются еще и маски прозрачности, то и объектов ImageAttribute. В случае мобильных устройств ситуация усугубляется тем, что в .NET Compact Framework, в отличие от .NET Framework, статические версии базовых кистей и перьев не предусмотрены. Так, в версии NET Framework, ориентированной на настольные компьютеры, существуют, например, объекты System.Drawing.Pens.Blue и System.Drawing.Brushes.DarkOrange, но в NET Compact Framework эти объекты приходится распределять. Решение этой проблемы заключается в создании собственного глобального набора объектов Pen и Brush, который вы будете использовать для нужд рисования во всем приложении. Вы должны тщательно просмотреть все циклы рисования в приложении, обращая особое внимание на случаи повторного создания идентичных ресурсов и избавляясь от подобной избыточности в коде. Если в вашем приложении непрерывно выполняются операции рисования, то такие объекты, как шрифты, перья, кисти и маски прозрачности, должны либо однажды распределяться в цикле рисования, либо глобально кэшироваться.
■ Типы, которые преобразуются (или, как еще говорят, "'упаковываются") в объекты. Наиболее распространенным типом значений, используемым в графических кодах, является структура System.Drawing.Rectangle. Обычно работа с типами, соответствующими значениям, отличается высокой эффективностью, поскольку их можно размещать в стеке (а не в глобальном пуле свободной памяти, или куче, как объекты). В то же время, значения также могут рассматриваться как объекты и размещаться в массивах или коллекциях или передаваться всюду, где допускается использование объектного типа. Тщательно следите за тем, чтобы не происходило неявное постоянное распределение и освобождение памяти, обусловленное "упаковкой" значений в объекты и их обратной "распаковкой".
Заботясь о том, чтобы в графическом коде приложения не выполнялось непрерывное создание и уничтожение часто используемых объектов, вы не должны забывать о необходимости следить за использованием глобальной памяти своего мобильного приложения. Если в графических функциональных возможностях нуждаются лишь отдельные части приложения, то следует подумать об освобождении занимаемых ими ресурсов, как только приложение входит в состояние, в котором в течение некоторого времени графические ресурсы ему не будут нужны. Хорошей практикой проектирования считается использование конечного автомата и определение тех графических ресурсов, от которых при выходе приложения из определенных состояний можно освободиться.
Резюме
От того, что вы напишете код, обеспечивающий отличную реакцию пользовательского интерфейса, алгоритмы вашего приложения работать быстрее не станут. Это никоим образом не ускорит победный марш разработки приложения "до полного завершения". Более того, код приложения может даже усложниться. Единственная причина, по которой следует заботиться о высокой реактивности интерфейса и стремиться к высокой производительности его кода, заключается в том, чтобы создать для пользователя более комфортные условия, но разве ради этого не стоит потрудиться? Если вы хотите, чтобы пользователь поставил самую высокую оценку вашему приложению, вы не должны жалеть времени на доводку и отшлифовку таких вещей, как способность интерфейса к отклику. Как и в случае других аспектов производительности, выполнение этой работы нельзя откладывать до завершающих этапов процесса разработки. В конце разработки вы еще смогли бы добавить что-то наподобие курсоров ожидания в тех точках, где приложение может тормозиться, но это все равно, что красиво покрасить фасад плохо построенного дома – это лучше, чем вообще ничего, но, в сущности, не так уж и много. Никогда не забывайте о необходимости обеспечения высокой интерактивности пользовательского интерфейса и всегда в явном виде включайте это требование в качестве критерия прохождения каждой контрольной точки разработки. Лучше всего решать подобные проблемы сразу же после их выявления, пока они не успели накрепко зацементироваться в структуру приложения.
В основе высокой производительности пользовательского интерфейса лежит активное управление условиями взаимодействия с ним конечного пользователя. Ваше мобильное приложение должно обеспечивать такие условия, при которых пользователь в процессе навигации в пределах приложения чувствует его ответную реакцию и не испытывает раздражения. Пользовательский интерфейс инициирует сложнейшую систему взаимодействий между кодом приложения и библиотеками времени выполнения, поверх которых приложение построено. Этот процесс сопровождается созданием элементов управления, используемых для заполнения форм, генерацией событий, а также многочисленными видами взаимодействия различного уровня между кодом и средой выполнения. Высокая производительность обеспечивается использованием оптимизированных в этом отношении средств среды выполнения, структурированием кода пользовательского интерфейса таким образом, чтобы пользователь не чувствовал себя изолированным от вычислительного процесса при выполнении задач, требующих длительного времени, а также целенаправленной количественной оценкой эффективности кода пользовательского интерфейса и включения в него средств контроля хода выполнения, которые позволят вам разобраться в том, что именно происходит за кулисами пользовательского интерфейса.
Изучайте свойства и методы элементов управления, которые вы используете. Для элементов управления часто предусматриваются специальные возможности оптимизации производительности. Внимательно анализируйте события, на которые реагирует ваше приложение. В частности, вы должны постараться хорошо представлять себе и экспериментально проверять, когда именно запускаются события, на которые приложению приходится реагировать. В процессе просмотра кода события могут казаться вам простыми и понятными, но они представляют ту область, в которой вы, как разработчик, вынуждены передавать управление выполнением приложения операционной системе. Именно операционная система и каркас пользовательского интерфейса определяют, когда именно будет выполняться код вашего обработчика события; это может случаться гораздо чаще, чем вы думаете, или происходить неожиданным для вас образом при обработке сообщений операционной системой. Между событиями, свойствами и методами элементов управления могут существовать такие тонкие виды взаимодействия, о которых вы даже не догадываетесь. Непредусмотренная и не являющаяся необходимой обработка событий пользовательского интерфейса порождает проблемы производительности и усложняет работу с приложением. Будьте особенно внимательны в тех случаях, когда существует возможность повторного входа в обработчик событий.
При работе с растровыми изображениями существенное значение имеют их размеры. Современные цифровые изображения с высоким разрешением занимают очень много места как на диске, так и в памяти. Цифровые фотографии, а также битовые образы, генерируемые на настольных компьютерах, представляют собой изображения, разрешение которых намного превышает возможности мобильных устройств. Никогда не забывайте о размерах экранов устройств, с которыми вы работаете, и размерах переносимых на них битовых образов; они должны соответствовать друг к другу. Для изображений, размеры которых довольно велики, неплохо использовать сжатые форматы хранения данных. В случае изображений, допускающих применение сжатие с потерями, рекомендуется использовать формат JPEG, тогда как в случае изображений, требующих полной достоверности, более подходящим является формат PNG. Помните, что, независимо от эффективности используемого алгоритма сжатия, занимаемый загруженным изображением объем памяти определяется размерами самого изображения.
Обработку графики следует продумывать независимо от кодирования высокоуровневого пользовательского интерфейса и оптимизировать отдельно. Оптимизация графического кода, предназначенного для рисования изображений, требует применения иных методик, нежели настройка кода высокоуровневого пользовательского интерфейса, и требует особого искусства. Поскольку графические изображения, генерируемые вашим мобильным приложением, обычно должны сосуществовать с элементами высокоуровневого пользовательского интерфейса, следует продумать, каким образом следует организовать взаимодействие графического кода с пользовательским интерфейсом; для этого существует несколько различных моделей. Для такого взаимодействия целесообразно использовать самый высокий из возможных уровней абстракции. Если вы генерируете статические изображения, то одним из приемлемых и концептуально несложных способов реализации указанного подхода может быть создание битового образа в памяти и его назначение свойству Image элемента управления PictureBox. Если требуется непрерывное обновление изображения, то приложение может создавать изображение во внеэкранном буфере и периодически копировать кадры на поверхность формы подобно кинопроектору. Пользовательские элементы управления особенно удобно использовать в тех случаях, когда требуется высокая степень интерактивности взаимодействия пользователя с создаваемой графикой, например, диаграммой, позволяющей переходить с одного уровня детализации данных, которые она представляет, на другой. Кроме того, пользовательские элементы управления могут генерировать пользовательские события, которые ваше приложение может перехватывать. Каждая из вышеперечисленных стратегий интеграции пользовательского интерфейса характеризуется своими видами взаимодействий, для которых она больше всего приспособлена; при этом очень важно явно сформулировать, какие именно задачи должна решать графика в мобильном приложении, и выбрать наиболее подходящую стратегию решения этих задач.
Работая с графическим кодом, вы должны хорошо представлять себе систематическую картину того, каким образом в приложении осуществляется визуализация изображений. При этом очень важно "не оказаться крохобором в мелочах, бездумно транжирящим крупные суммы" в обращении с ресурсами. Если имеются такие объекты Bitmap, Graphics, Pen, Brush, Font или ImageAttribute, которые будут использоваться во всех операциях рисования в приложении, то вы должны разработать систему, в задачи которой входит создание и кэширование указанных ресурсов. Применение предварительно создаваемых изображений сопряжено с дополнительным расходом ценной памяти, однако оно чрезвычайно ускоряет операции рисования; при разумном подходе это позволяет создавать приложения с привлекательным интерфейсом, сохраняющие высокую производительность. Исключайте случаи одновременной загрузки двух идентичных ресурсов – подобная расточительность никому не нужна.
Целесообразно также подумать над тем, нельзя ли представить приложение в виде таких различных состояний, чтобы распределение графических ресурсов или освобождение кэшируемых ресурсов, когда необходимость в них отпадает, можно было осуществлять для каждого состояния независимо от других.
Создание многофункциональных графических интерфейсов для мобильных устройств – вполне осуществимая задача, решение которой способствует повышению комфортности условий работы пользователя с вашим приложением. При построении графических пользовательских интерфейсов очень важно придерживаться подхода, являющегося одновременно творческим, аналитическим и систематическим. Сочетание творческого воображения художника, умеющего представлять себе то, чего еще не существует, аналитических способностей инженера, умеющего решать технические задачи, и проницательного взора бухгалтера, который не допустит неоправданных затрат, – вот что необходимо для того, чтобы ваше мобильное приложение было оценено конечными пользователями как высокопроизводительное, а взаимодействие с его привлекательным интерфейсом доставляло им одно удовольствие. Работа с графикой потребует от вас напряжения всех сил, но одновременно доставит огромное удовольствие; никогда не забывайте о производительности, и тогда выше вас – только небо!







