Synchronization Context
Synchronization Context
Версия WPF зависнет напрочь, но, как выяснилось, версия Windows Forms будет «случайно» работать. По
умолчанию Windows Forms устанавливает контекст синхронизации при создании первого элемента
управления — в данном случае, когда я создаю m_mainForm (это поведение контролируется
[Link]). Поскольку «ожидание InitializeAsync» происходит после
создания формы, со мной все в порядке. Однако если бы я сделал вызов await перед созданием
m_mainForm, у меня возникла бы та же проблема. Решение состоит в том, чтобы сначала настроить
контекст синхронизации самостоятельно, следующим образом:
С#Копировать
[STAThread]
static void Main()
{
[Link]();
[Link](false);
[Link](
new WindowsFormsSynchronizationContext());
Содержание
Быстрый запуск
Ускоренный запуск пользовательского интерфейса
Управление загрузкой пользовательского интерфейса
Оптимизация привязки данных
Облегченный макет
Производительность рисования
текста и изображений
Применение методов рисования
Управление ресурсами
Заключение
Windows Forms позволяют создавать богатый и отзывчивый пользовательский интерфейс для ваших
приложений. В этой статье я расскажу о ряде методов, которые можно использовать, чтобы обеспечить
оптимальную производительность приложений на базе Windows® Forms. Я обсужу распространенные
сценарии, критичные к производительности, такие как запуск, контроль заполнения и контроль
рисования. Кроме того, я расскажу, как проектировать и писать код для повышения производительности
вашего приложения. Вместе эти методы должны дать вам хорошую базовую основу для получения
максимальной отдачи от ваших пользовательских интерфейсов.
Быстрый запуск
Чтобы получить эффект теплого запуска, вам не обязательно ранее запускать то же приложение. Простой
запуск любого управляемого приложения приведет к сохранению в памяти большого количества
страниц из двоичных файлов Microsoft® .NET Framework, что сократит время запуска любых
управляемых приложений, которые запускаются позже.
При проектировании производительности важно различать эти два сценария и ставить для них
отдельные цели. Хотя «теплый» запуск можно улучшить за счет снижения загрузки ЦП, на «холодный»
запуск в основном влияет дисковый ввод-вывод. Таким образом, некоторые оптимизации улучшают
некоторые сценарии, а не другие.
Один из способов улучшить холодный запуск — уменьшить количество библиотек DLL, загружаемых в
приложение. Это напрямую влияет на количество страниц, которые приходится трогать при запуске
приложения. Кроме того, уменьшение количества загружаемых модулей снижает нагрузку на ЦП,
связанную с загрузкой модуля, а также сокращает время горячего запуска.
Вы можете диагностировать, как модули загружаются в ваше приложение, с помощью фильтра событий
загрузки модуля в отладчике WinDbg в Platform SDK. Альтернативно вы можете использовать следующую
команду WinDbg, чтобы узнать, почему был загружен конкретный модуль:
Копировать
sxe ld:[Link]
Просмотрите стеки вызовов и посмотрите, можно ли избежать загрузки некоторых модулей. Вы также
можете использовать опцию «ca ml» управляемого отладчика MDbg, доступного как часть .NET
Framework SDK. В некоторых случаях вы можете избежать загрузки дополнительных модулей, просто
проведя рефакторинг кода. Если ваше приложение загружает множество принадлежащих вам DLL,
рассмотрите возможность объединения их в меньшее количество более крупных DLL, где это имеет
смысл.
JIT-компиляция методов может потреблять много циклов ЦП и легко стать узким местом во время
запуска приложения. Чтобы избежать этих накладных расходов, вы можете предварительно
скомпилировать сборки с помощью встроенного генератора изображений [Link] (см. Скорость: NGen
повышает производительность с помощью новых мощных функций ). NGen выполняет всю работу JIT-
компилятора, но выполняет эту работу заранее, а затем сохраняет эти изменения на диске, экономя
циклы ЦП во время выполнения. Использование NGen для предварительной компиляции двоичных
файлов дает дополнительный выигрыш в производительности: собственные кодовые страницы могут
использоваться совместно между процессами, тогда как кодовые страницы, скомпилированные JIT,
являются частными для процесса и не могут быть использованы повторно.
Другой метод — установить сборки со строгими именами в глобальный кэш сборок (GAC), чтобы
избежать проверки подписи строгого имени — дорогостоящего процесса, который затрагивает каждую
страницу сборки перед ее загрузкой. Если вы решите использовать NGen, еще важнее иметь в GAC
сборки со строгими именами и избегать перебазирования DLL.
Каждый двоичный файл (DLL или EXE) имеет предпочтительный базовый адрес — место в виртуальной
памяти, куда он должен быть загружен. Этот адрес указывается во время сборки. Если вы используете
компилятор Visual Basic® или C#, то все ваши двоичные файлы получат один и тот же базовый адрес
(0x400000), если не дано явных указаний на обратное. Таким образом, во время загрузки ваш EXE-файл
будет размещен по этому адресу, и все ваши библиотеки DLL придется разместить в другом месте в
виртуальной памяти (перебазировать), поскольку их предпочтительный базовый адрес занят.
Когда DLL перебазируется, загрузчик обновляет все абсолютные адреса в DLL, чтобы отразить новый
адрес загрузки. Это означает, что каждая страница, содержащая адрес, который необходимо изменить,
будет затронута при перебазировании DLL. Более того, чтобы стать доступной для записи, страницу
необходимо скопировать и сохранить в файле подкачки. На этом этапе страница становится частной для
процесса и не может быть передана другим процессам.
Образы NGen, как правило, значительно больше, чем изображения промежуточного языка (IL), и это
увеличивает влияние перебазирования. NGen устанавливает для создаваемого образа тот же базовый
адрес, что и адрес, указанный в IL-образе. Поэтому, если вы решите использовать NGen для своих
сборок, то при назначении базовых адресов для ваших IL-образов вы должны оставить достаточно места
для размещения NGen-образов. Обычно для образа NGen потребуется в два-три раза больше места, чем
для эквивалентного образа IL.
Вы можете убедиться, что назначение базового адреса работает правильно, проверив фактические
адреса загрузки и сравнив их с предпочтительными адресами загрузки. Для этого можно использовать
пример приложения списка задач [Link] и дампер двоичных файлов Microsoft COFF [Link]. Эта
команда показывает фактический адрес загрузки для каждой DLL, загруженной в контексте приложения с
идентификатором процесса 1240:
Копировать
tlist 1240
Dumpbin позволяет вам проверить предпочтительный базовый адрес вашей DLL, как показано здесь:
Копировать
dumpbin /headers [Link]
Копировать
75F70000 image base (75F70000 to 75F78FFF).
Это означает, что предпочтительный базовый адрес [Link] — 75F70000 и что диапазон адресов от
75F70000 до 75F78FFF должен быть доступен для загрузки этой DLL по предпочтительному базовому
адресу.
Если вы используете дорогостоящие вызовы сети или базы данных, сделайте их асинхронными в другом
потоке, чтобы избежать блокировки пользовательского интерфейса. В Windows Forms 2.0 компонент
BackgroundWorker можно использовать для передачи работы в фоновый поток. На рисунке
1 BackgroundWorker используется для рекурсивного поиска всех файлов, соответствующих заданному
шаблону (например, «*.txt») в заданной папке и ее подпапках. Фоновый поток обновляет поток
пользовательского интерфейса по мере его выполнения, а поток пользовательского интерфейса
добавляет файлы или подпапки в представление TreeView в ответ на обновления хода
выполнения. Полученный TreeView фильтруется так, что подпапки, не содержащие соответствующих
файлов, не отображаются.
Копировать
Imports [Link] Public Class Form1 Private DirInfo As DirectoryInfo Private Pattern
As String Private Sub Form1_Load(ByVal sender As [Link], _ ByVal e As
[Link]) Handles [Link] DirInfo =
[Link]("C:\\") Pattern = "*.txt" [Link] =
"Searching for " & Pattern & " files" [Link] = True
[Link] = True
[Link]() End Sub Private Sub BackgroundWorker1_DoWork( _
ByVal sender As [Link], _ ByVal e As [Link]) _
Handles [Link] For Each f As FileInfo In [Link](Pattern)
Dim treeNode As TreeNode = New TreeNode([Link])
[Link](0, treeNode) Next For Each SubDir As
DirectoryInfo In [Link]() Dim treeNode As TreeNode =
GetMatchingFiles(SubDir) If Not treeNode Is Nothing Then
[Link](0, treeNode) End If Next End Sub Private Sub
BackgroundWorker1_ProgressChanged(_ ByVal sender As Object, _ ByVal e As
[Link]) _ Handles
[Link] [Link]((CType([Link],
TreeNode))) End Sub Private Sub BackgroundWorker1_RunWorkerCompleted( _ ByVal sender
As Object, _ ByVal e As [Link]) _ Handles
[Link] [Link] = "Ready" End Sub Private Function
GetMatchingFiles(ByVal dir As DirectoryInfo) _ As TreeNode Dim treeNode As TreeNode =
Nothing For Each f As FileInfo In [Link](Pattern) If treeNode Is Nothing Then
treeNode = New TreeNode([Link]) End If [Link](New TreeNode([Link]))
Next For Each SubDir As DirectoryInfo In [Link]() Dim subNode As TreeNode
= GetMatchingFiles(SubDir) If Not subNode Is Nothing Then If treeNode Is Nothing Then
treeNode = New TreeNode([Link]) End If [Link](subNode) End If Next
Return treeNode End Function End Class
Операции, которые должны быть выполнены, но не нужны для отображения первого пользовательского
интерфейса, могут выполняться во время простоя системы или по требованию после отображения
первого пользовательского интерфейса. Например, если у вас есть TabControl, при запуске заполняйте
только самую верхнюю страницу и при необходимости извлекайте информацию для других страниц. Код
на рис. 2 включает форму с TabControl, содержащую шесть страниц TabPages. Я поместил все элементы
управления, необходимые для каждой TabPage, в UserControl и добавил их в коллекцию элементов
управления TabPage, когда TabPage будет отображаться. Для простоты я использовал один и тот же
UserControl для каждой TabPage и лишь немного изменил контекст некоторых дочерних элементов
управления, но вы легко можете создать отдельный UserControl, соответствующий каждой TabPage.
Копировать
public partial class Form2 : Form { int currentIndex = 1; public Form2(bool onDemand)
{ InitializeComponent(); if (!onDemand) InitTab(-1); else { InitTab(0);
[Link] += new EventHandler(Application_Idle); } } private void InitTab(int
index) { if (index == -1) { for (int i = 0; i < [Link]; i++)
{ InitTab(i); } return; } // if the TabPage has already been populated if
([Link][index].[Link] > 0) return; UserControl control = new
myTabPage(index); if (control != null) { [Link] = [Link];
[Link][index].[Link](control); } } private void
tabControl1_SelectedIndexChanged( object sender, [Link] e)
{ InitTab([Link]); } private void Application_Idle(object sender,
EventArgs e) { if (currentIndex >= [Link]) { [Link] -= new
EventHandler(Application_Idle); } else InitTab(currentIndex++); } }
Для самой верхней страницы я добавил UserControl в конструктор формы. Для других страниц я сделал
это в ответ на срабатывание события [Link], когда соответствующая страница
будет показана. Пользователь испытает некоторую задержку при первом выборе TabPage. Тогда в
следующий раз, когда он будет выбран, задержки не будет, поскольку TabPage уже инициализирован.
В этом примере я намеренно замедлил создание TabPages, показывая ListView с большим количеством
элементов. Если я инициализирую все страницы вкладок заранее, время, необходимое для отображения
формы, в моей системе (Pentium III, 800 МГц, 512 МБ ОЗУ) составит девять секунд. При заполнении
TabPages по требованию отображение каждой TabPage в первый раз занимает около 1,5 секунд. Если
заполнена только самая верхняя страница, вся форма отображается за 1,5 секунды.
Я могу пойти еще дальше, прослушивая событие [Link] и заполняя одну TabPage за раз, когда
происходит событие Idle. Когда отображается форма или отображается новая TabPage, пользователю
потребуется некоторое время, чтобы посмотреть на форму и переместить мышь, чтобы выбрать другую
страницу. Во время этой органической задержки в работе приложения будет обработано событие Idle и
будут заполнены дополнительные страницы TabPages.
Если другие методы не могут в достаточной степени повысить производительность запуска, вы можете
рассмотреть возможность использования экранов-заставок и индикаторов выполнения, чтобы держать
пользователей в курсе, как в этом примере в разделе « Создание формы экрана-заставки в отдельном
потоке» . Если вы пишете свое приложение на Visual Basic, вы можете использовать свойство
[Link] .NET Framework 2.0 (см. Свойство [Link] ).
Контроль населения
Многие элементы управления Windows Forms, такие как ListView, TreeView и Combobox, отображают
коллекции элементов. Добавление слишком большого количества элементов в эти коллекции может
стать узким местом, если не будут применены оптимизации. К счастью, в Windows Forms в .NET
Framework 2.0 было включено несколько улучшений контрольной совокупности. К ним относятся
использование методов BeginUpdate и EndUpdate внутри методов AddRange, оптимизация заполнения
TreeView с помощью AddRange и оптимизация отсортированного заполнения всех элементов
управления.
Копировать
[Link](); for(int i = 0; i < 10000; i++) { ListViewItem listItem = new
ListViewItem("Item"+[Link]() ); [Link](listItem); }
[Link]();
Массовые операции всегда более эффективны, чем отдельные изменения. Предпочтительный способ
добавления элементов в коллекции элементов управления, такие как ComboBox, ListView или TreeView, —
использовать метод AddRange, который позволяет одновременно добавлять массив предварительно
созданных элементов. Windows Forms выполняет все возможные оптимизации, чтобы сделать эту
операцию эффективной. Часто это просто означает, что вместо вас будут вызваны BeginUpdate и
EndUpdate. Однако в некоторых случаях будут выполнены дополнительные оптимизации. Например, для
элемента управления TreeView эти оптимизации значительны.
Заполнение ListView работает значительно быстрее, если оно выполняется после создания дескриптора
ListView. Если вам нужно отобразить форму, имеющую ListView со многими элементами, добавьте
элементы в ListView в обработчике событий [Link] или [Link]. На этом этапе дескриптор ListView
уже создан. Если вы добавите элементы ListView в конструктор формы (то есть до создания дескриптора
ListView), элементы будут добавлены быстро, но отображение формы займет значительное время из-за
медленной отрисовки ListView.
В отличие от ListView, TreeView заполняется значительно быстрее, если узлы добавляются до создания
дескриптора. Если вы используете [Link], вы не заметите разницы; ваш TreeView будет
быстро заполнен независимо от того, когда вы это сделаете. Однако если вы используете метод Add и
добавляете узлы в конструктор формы до создания дескриптора TreeView, ваш TreeView будет заполнен и
отображен так же быстро, как если бы вы использовали метод AddRange.
При заполнении элементов управления с привязкой к данным, таких как ComboBox или ListBox, более
эффективно устанавливать свойство DataSource последним, после установки ValueMember и
DisplayMember. В противном случае ваш элемент управления будет повторно заполнен в результате
изменения ValueMember:
Копировать
[Link] = "Name"; [Link] = "Name";
[Link] = test;
По этой причине только что показанный код более эффективен, чем этот:
Копировать
[Link] = test; [Link] = "Name"; [Link]
= "Name"
Эти методы предназначены для использования с простыми сценариями привязки, такими как привязка
данных TextBox или ComboBox. Код на рисунке 3 демонстрирует, что итерация, которая
приостанавливает и возобновляет привязку, работает примерно в пять раз быстрее, чем итерация,
которая этого не делает.
Копировать
private void button1_Click(object sender, EventArgs e) { DataTable tbl =
[Link] as DataTable; Stopwatch sw1 = new Stopwatch();
[Link](); for (int i = 0; i < 1000; i++) { [Link][0][0] = "row " + [Link]();
} [Link](); Stopwatch sw2 = new Stopwatch(); [Link]();
[Link](); for (int i = 0; i < 1000; i++) { [Link][0][0]
= "suspend row " + [Link](); } [Link](); [Link]();
[Link]([Link]("Trial 1 {0}\r\nTrial 2 {1}", [Link],
[Link])); } private void Form1_Load(object sender, EventArgs e)
{ DataTable tbl = new DataTable(); [Link]("col1"); [Link]("one");
[Link]("two"); [Link] = tbl;
[Link]("Text", this.bindingSource1, "col1"); }
Элементы управления, реализующие сложную привязку данных, такие как элемент управления
DataGridView, обновляют свои значения на основе событий изменения, таких как ListChanged, поэтому
вызов SuspendBinding не помешает им получать изменения в источнике данных. Эти методы можно
использовать в сложных сценариях привязки, если вы подавляете события ListChanged, задав для
свойства RaiseListChangedEvents значение false.
Легкий макет
Такие изменения, как изменение размера или выравнивание элемента управления, также могут вызвать
проблемы. Например, когда дочерние элементы управления добавляются в форму или ToolStrip,
запускается событие [Link]. Изменение размера или местоположения дочернего элемента
управления вызовет событие Layout в родительском элементе управления. Изменение размера шрифта
также вызовет событие Layout. В ответ на событие Layout форма масштабируется и упорядочивает
элементы управления. Обработка слишком большого количества событий макета при создании или
изменении размера элемента управления может существенно повлиять на производительность.
Помните, что SuspendLayout предотвращает выполнение событий Layout только для этого конкретного
элемента управления. Например, если на панель добавляются элементы управления, SuspendLayout и
ResumeLayout должны вызываться для панели, а не для родительской формы.
Изменение таких свойств, как «Границы», «Размер», «Местоположение», «Видимый» и «Текст» для
элементов управления AutoSize, вызовет событие Layout. Изменить эти свойства в [Link] будет еще
дороже, поскольку к тому времени будут созданы все дескрипторы и, следовательно, будет обработано
много сообщений. Это еще одно место, где вам следует добавить SuspendLayout и ResumeLayout, чтобы
предотвратить возникновение дополнительных событий Layout. Если возможно, внесите все эти
изменения в InitializeComponent — таким образом потребуется только одно событие Layout.
Несколько свойств определяют размер или расположение элемента управления: Ширина, Высота,
Сверху, Снизу, Слева, Справа, Размер, Местоположение и Границы. Установка [Link], а затем
[Link] требует двойного объема работы по сравнению с установкой их обоих вместе через
[Link]. Выполнение кода, показанного на рисунке 4, демонстрирует разницу.
Копировать
private void button1_Click(object sender, EventArgs e) { Stopwatch sw = new
Stopwatch(); [Link](); for (int i = 1; i < 1000; i++) { [Link] = i;
[Link] = i; } [Link](); Stopwatch sw2 = new Stopwatch(); [Link](); for
(int i = 1; i < 1000; i++) { [Link] = new Size(i, i); } [Link]();
[Link]([Link]("Trial 1 {0}\r\nTrial 2 {1}", [Link],
[Link])); }
Если дескрипторы созданы, вы заметите разницу, даже если используете вызовы SuspendLayout и
ResumeLayout. SuspendLayout предотвращает вызов Windows Forms OnLayout. Это не помешает отправке
и обработке сообщений об изменении размера. Вам следует попытаться установить свойство, которое
отражает большую часть имеющейся у вас информации. Если вы просто меняете размер, установите
Size; если вы меняете размер и местоположение, измените границы.
Картина Производительность
Если ваше приложение Windows Forms интенсивно использует рисование, оно может выиграть от
повышения производительности рисования. Во-первых, сведите к минимуму работу, выполняемую при
каждом событии Paint. Сделайте обработчик событий Paint максимально облегченным, исключив из него
все дорогостоящие операции. Например, если вам нужно визуализировать текст на основе вашего
текущего ClientSize, вы можете создать объект Font соответствующего размера в обработчике событий
Resize, а не в каждом событии Paint. Если вам нужно создать кисть для рисования, вы можете кэшировать
ее, а не повторно создавать.
Если вы хотите, чтобы ваш элемент управления был перерисован, вы можете вызвать его метод
Invalidate. Весь элемент управления будет перерисован, если вы не передадите этому вызову никаких
аргументов. Во многих случаях вы можете оптимизировать производительность отрисовки, тщательно
рассчитав область, которую необходимо перекрасить, и передав эту область в качестве аргумента
Invalidate.
Если это не работает достаточно хорошо, вы можете использовать структуру ClipRectangle, включенную в
параметр PaintEventArgs события OnPaint. Например, вы можете нарисовать часть изображения, которое
было признано недействительным, выполнив следующие действия:
Копировать
Protected Overrides Sub OnPaint(_ ByVal e As [Link])
[Link]([Link], [Link], _ [Link],
[Link]) End Sub
Однако использование этого метода часто требует тщательных расчетов (например, если используется
прокрутка или масштабирование).
Windows Forms и базовая архитектура Win32® предоставляют два уровня рисования для любого
элемента управления: фоновый и передний план. Функция [Link] отвечает за
рисование фоновых эффектов (обычно фонового изображения и цвета фона), а функция [Link]
отвечает за рисование эффектов переднего плана (изображений и текста). Если ваш элемент управления
не использует фоновое изображение или цвет фона, а вместо этого настраиваемо рисует все в OnPaint,
использование стиля элемента управления «Непрозрачный» может повысить производительность,
пропуская неиспользуемую логику рисования. В частности, если для элемента управления
[Link] установлено значение true, функция OnPaintBackground будет пропущена,
поскольку предполагается, что функция OnPaint выполнит всю работу по рисованию (включая рисование
фона). Кроме того,
Текст и изображения
Вы можете измерять и рисовать текст в элементе управления Windows Forms в .NET Framework 2.0 с
помощью класса TextRenderer. TextRenderer имеет два метода: MeasureText и DrawText, каждый из
которых имеет несколько перегрузок. Эффективность операций отрисовки зависит от выбранных вами
перегрузок и параметров, заданных в аргументе TextFormatFlags.
Для измерения однострочных строк лучше не использовать флаг [Link]. Когда этот
флаг установлен, GDI выполняет надежный, но дорогостоящий алгоритм для определения мест, на
которые необходимо разбить текст. Аналогичным образом, не используйте параметры
[Link] и [Link], если к
объекту Graphics не применялись обрезки или преобразования, поскольку эти параметры вызывают
выполнение некоторых дорогостоящих вычислений.
Чтобы использовать этот метод, сначала загрузите изображение из файла, а затем преобразуйте его в
растровое изображение с помощью PixelFormat.Format32bppPArgb. На рис. 5 этот метод используется
для установки фонового изображения на элементе управления.
Копировать
Public Overrides Property BackgroundImage() As [Link] Get Return
[Link] End Get Set(ByVal value As [Link]) [Link] = New
Bitmap([Link], [Link], _ [Link].Format32bppPArgb) Dim g As Graphics
= [Link]([Link]) [Link] =
[Link] [Link](value, New Rectangle(0, 0,
[Link], _ [Link])) [Link]() End Set End Property
Помимо добавления богатого набора функций фонового изображения в Windows Forms, значения
BackgroundImageLayout, отличные от значений по умолчанию, повышают производительность
рисования фонового изображения. Основным недостатком настройки Tile является то, что для
повторения рисунка изображения требуется текстурная кисть. Создание текстурной кисти обходится
дорого, поскольку предполагает сканирование всего изображения. Другие макеты просто используют
метод [Link], что приводит к значительному повышению производительности.
Двойная буферизация — это метод, позволяющий ускорить рисование и сделать его более плавным за
счет уменьшения мерцания. Основная идея состоит в том, чтобы взять операции рисования,
используемые для рисования вашего элемента управления, и применить их к внеэкранному
буферу. После завершения всех операций рисования этот буфер рисуется в элементе управления как
одно изображение. Обычно это уменьшает мерцание и ускоряет работу приложения. Такого поведения
можно добиться в .NET Framework 2.0, установив для стиля элемента управления значение
OptimizedDoubleBuffer, что эквивалентно настройке стилей DoubleBuffer и UserPaint в предыдущих
версиях Framework.
Чтобы воспользоваться преимуществами двойной буферизации, вам необходимо установить для обоих
этих стилей значение true или установить свойство DoubleBuffered в вашем элементе управления,
например:
Копировать
SetStyle([Link] | [Link],
true); // OR DoubleBufferred = true; // sets both flags
Здесь важно отметить, что установка свойства DoubleBuffered не всегда является оптимальным
решением проблем с отрисовкой. Это свойство следует использовать с осторожностью, в зависимости от
вашего приложения и целевых сценариев. Основным недостатком установки DoubleBuffered является то,
что для рисования выделяется значительный объем памяти. Если вам необходимо использовать двойную
буферизацию, рассмотрите также возможность сделать недействительными небольшие части элемента
управления, чтобы перерисовывать нужно было только самые важные части.
Другим побочным эффектом установки AllPaintingInWmPaint в значение true может быть видимый белый
след на дочерних элементах управления вашего элемента управления, когда окно, принадлежащее
другому приложению, перемещается по вашему окну. Это может произойти из-за того, что дочерние
окна управления получают много сообщений WM_ERASEBKGND и только одно сообщение
WM_PAINT. Поскольку сообщения WM_ERASEBKGND игнорируются, перерисовка для них происходит
слишком редко.
Экран на рис. 6 демонстрирует анимацию текста над пользовательским элементом управления, для
которого установлено фоновое изображение. Вы можете установить различные параметры рисования,
чтобы увидеть, как они влияют на плавность и скорость рисования (этот пример приложения доступен
для загрузки с веб-сайта журнала MSDN Magazine ).
Второй таймер используется для обновления фактической скорости анимации, отображаемой в правом
нижнем углу поверхности рисования. Под фактической скоростью анимации я подразумеваю количество
событий рисования, обработанных за предыдущую секунду. Фактическая скорость анимации может быть
ниже желаемой скорости анимации. Например, интервал в 10 мс означает, что вы хотите, чтобы текст
менял свое положение 100 раз в секунду.
Параметры макета фонового изображения позволяют сравнивать производительность рисования между
флажками «Плитка» и другими флажками макета изображения. Я выбрал макет по центру только в
качестве примера (масштаб или растягивание будут иметь тот же эффект, что и центр). В этом
приложении я выровнял изображение так, чтобы оно соответствовало размеру поверхности рисования,
независимо от выбранного макета изображения. Однако когда фон нарисован, расположение плиток
влияет на производительность. Кроме того, когда выбран макет «Центр», для вас автоматически
устанавливается параметр DoubleBuffered. Это не тот случай, когда выбран макет плитки.
В моих тестах наилучшие результаты были достигнуты, когда применялись методы двойной буферизации
и интеллектуальной отмены, для макета фонового изображения было установлено значение «Центр»
(или, точнее, любой макет, кроме плитки), а фоновое изображение предварительно визуализировалось с
использованием PixelFormat.Format32bppPArgb. При использовании всех этих техник картина получается
плавной и без мерцания. Кроме того, анимация работает быстрее.
Управление ресурсами
Объем памяти, сбор мусора и управление собственными ресурсами, такими как дескрипторы окон или
дескрипторы GDI, взаимозависимы для приложений Windows Forms. Неспособность освободить
неуправляемые ресурсы увеличивает нагрузку на сборщик мусора (GC). Windows Forms пытается
отслеживать использование дескрипторов и может принудительно выполнить дополнительные циклы
сборки мусора, вызывая [Link], если существует опасность нехватки ресурсов. Кроме того,
управляемые объекты, содержащие эти ресурсы, должны быть финализированы сборщиком мусора, что
обходится дороже, чем их упреждающее освобождение в вашем приложении через интерфейс
IDisposable.
Если время существования объекта явно известно, связанные с ним неуправляемые ресурсы должны
быть освобождены. Если используемый вами класс реализует шаблон Dispose и вы явно знаете, когда
закончите работу с объектом, обязательно вызовите Dispose. В методе Dispose вы вызовете тот же код
очистки, что и в Finalizer, и сообщите GC, что ему больше не нужно финализировать объект, вызвав
метод [Link]. Это повысит производительность, поскольку все объекты, на которые нет
ссылок в нулевом поколении, будут собраны в течение одного цикла сборки мусора. Если собираемый
объект необходимо финализировать, для его сбора потребуется как минимум два цикла GC.
Объекты, реализующие IDisposable, обычно делают это, потому что они удерживают ресурсы, которые
должны быть детерминированно освобождены. Например, элементы управления Windows Forms
содержат дескрипторы Windows, которые необходимо освободить. Если вы добавляете и удаляете
элементы управления из формы динамически, вам следует вызвать для них Dispose — в противном
случае вы накопите дополнительные нежелательные дескрипторы в своем процессе. Вам нужно удалить
только самый верхний элемент управления, поскольку его дочерние элементы будут удалены
автоматически.
Если вы показываете форму модально (с помощью метода ShowDialog), вам необходимо удалить ее
после закрытия. В этом случае он не удаляется автоматически, чтобы вы могли узнать его состояние
после закрытия. Обратите внимание, что другие формы автоматически удаляются при закрытии.
Графические объекты, такие как кисти, перья и шрифты, также реализуют IDisposable, поскольку они
удерживают дескрипторы GDI. Обычно эти объекты создаются в методе OnPaint, который вызывается
каждый раз, когда элемент управления необходимо перерисовать. Если вы не удалите их немедленно, вы
можете накопить большое количество этих объектов и удерживать множество дескрипторов GDI до того,
как эти объекты будут завершены сборщиком мусора. Если вы пишете код на C# или Visual Basic,
оператор using — удобный способ сделать это:
Если вы используете C++, его семантика распределения стека также очень полезна в этом отношении:
Копировать
Pen p(Color::Red, 1); e->Graphics->DrawLine(%p, 0,0, 10,10);
Вам следует стараться избегать вызова [Link], если это возможно. ГХ является самонастраивающимся
и настраивается в соответствии с требованиями приложения к памяти. В большинстве случаев
программный вызов GC будет препятствовать автоматической настройке. Единственный случай, когда
[Link] может быть полезен, — это когда вы знаете, что только что освободили много памяти и хотите,
чтобы она была собрана немедленно. Однако в целом более эффективно позволить GC просто управлять
собой.
Если ваша форма имеет более 200 дочерних элементов управления, вы можете подумать о том, как
сделать ее более эффективной. Каждый элемент управления потребляет собственные ресурсы, а
управление большим количеством элементов управления обходится дорого. Вместо этого рассмотрите
возможность использования ListView, TreeView, DataGridView, ListBox, ToolStrip и MenuStrip. Отдельные
элементы этих элементов управления обычно не требуют собственного дескриптора для поддержки
каждого элемента.
Если физическая память дефицитна и происходит подкачка, использование физического диска будет
высоким (поскольку страницы записываются на диск). В этом случае ваше приложение будет работать
невыносимо медленно, и вам будет крайне необходимо сократить использование памяти. В этом вам
поможет инструмент профилирования в Visual Studio Team System. Вы также можете использовать этот
инструмент для просмотра управляемого распределения памяти или воспользоваться поддержкой
профилирования CLR ( CLR Profiler: No Code Can Hide from the Profiling API в .NET Framework 2.0 ). Также
вы можете использовать инструмент Virtual Address Dump (VaDump), чтобы просмотреть рабочий набор
процессов:
Копировать
VaDump –sop <pid>
Заключение