Кэширование изображений
58WPF --- Графика и анимация WPF --- Кэширование изображений
Кэширование растровых изображений заставляет WPF взять содержимое растрового изображения, как оно есть, и скопировать в память видеокарты. С этого момента видеокарта отвечает за манипуляции этим растровым изображением и обновление дисплея. Это происходит быстрее, чем когда всю работу выполняет WPF, непрерывно взаимодействуя с видеокартой.
При правильном применении кэширование растровых изображений повышает производительность рисования приложения, но при неправильном приводит к пустой трате памяти видеокарты и даже снижению производительности. Поэтому, прежде чем прибегнуть к кэшированию растровых изображений, следует убедиться в его целесообразности. Ниже предложено несколько рекомендаций:
Если отображаемое содержимое нуждается в частой перерисовке, кэширование растровых изображений может быть оправдано. Дело в том, что каждая последующая перерисовка будет происходить быстрее. Примером может служить использование BitmapCacheBrush для рисования поверхности фигуры, пока некоторые другие анимированные объекты движутся сверху. Несмотря на то что фигура не изменяется, различные ее части скрываются и показываются, что требует перерисовки.
Если содержимое элемента меняется часто, то кэширование растровых изображений, скорее всего, не будет иметь смысла. Дело в том, что при каждом изменении визуального элемента среда WPF должна заново визуализировать растровое изображение и отправить его в кэш видеокарты, что требует времени. Это правило имеет некоторые исключения, поскольку определенные изменения не делают кэш недействительным. Примерами безопасных операций могут служить вращение и изменение масштаба элемента в рамках трансформации, кадрирования, изменения прозрачности либо применения эффекта. С другой стороны, изменение содержимого, компоновки и форматирования приводят к перерисовке растрового изображения.
Кэшируйте как можно меньший объем содержимого. Чем больше растровое изображение, тем больше времени требуется WPF для сохранения кэшированной копии и больше памяти расходуется на видеокарте. Как только память видеокарты будет заполнена, WPF придется вернуться к медленной программной перерисовке.
Неудачная стратегия кэширования может создать больше проблем с производительностью, чем не полностью оптимизированное приложение. Поэтому не применяйте кэширование, если не походят приведенные выше рекомендации. Также пользуйтесь инструментом профилирования вроде Perforator для проверки, повысила ли выбранная стратегия производительность.
Для лучшего понимания необходимо поэкспериментировать с примером. Ниже показан пример, где анимация перемещает простую фигуру — квадрат — по поверхности Canvas, которая содержит элемент Path со сложной геометрией. По мере передвижения квадрата по поверхности среда WPF вынуждена заново вычислять Path и заполнять пропущенные разделы. Это обеспечивает неожиданно большую нагрузку на центральный процессор, и анимация даже может стать прерывистой.
Существует несколько способов решения этой проблемы. Один из них — заменить фон растровым изображением, которым WPF может управлять более эффективно. Более гибкий вариант предусматривает использование кэширования растровых изображений, которое сохраняет фон как интерактивный элемент.
Чтобы включить кэширование растровых изображений, необходимо установить свойство CacheMode соответствующего элемента в BitmapCache. Каждый элемент поддерживает это свойство, что позволяет точно выбирать, какой именно элемент должен использовать кэширование:
<Window x:Class="Animation.CachingTest" Name="window"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="clr-namespace:Animation"
Title="CachingTest" Height="600" Width="800">
<Window.Resources>
<local:ArithmeticConverter x:Key="converter"></local:ArithmeticConverter>
</Window.Resources>
<Window.Triggers>
<EventTrigger RoutedEvent="Window.Loaded">
<BeginStoryboard>
<Storyboard>
<DoubleAnimation Storyboard.TargetProperty="(Canvas.Left)" Storyboard.TargetName="rect" AutoReverse="True" RepeatBehavior="Forever"
To="{Binding ElementName=window,Path=Width,Converter={StaticResource converter},ConverterParameter=-100}"
Duration="0:0:15"></DoubleAnimation>
</Storyboard>
</BeginStoryboard>
</EventTrigger>
</Window.Triggers>
<Grid Margin="5">
<Grid.RowDefinitions>
<RowDefinition></RowDefinition>
<RowDefinition Height="Auto"></RowDefinition>
</Grid.RowDefinitions>
<Canvas Name="canvas">
<Path Name="pathBackground" Stroke="DarkRed" StrokeThickness="1" ></Path>
<Rectangle Name="rect" Canvas.Left="10" Canvas.Top="100" Fill="Blue" Width="75" Height="75">
</Rectangle>
</Canvas>
<CheckBox Grid.Row="2" x:Name="chkCache" Content="Enable Caching"
IsChecked="False" Click="chkCache_Click"></CheckBox>
</Grid>
</Window>
using System;
using System.Text.RegularExpressions;
using System.Windows;
using System.Windows.Data;
namespace Animation
{
public class ArithmeticConverter : IValueConverter
{
private const string ArithmeticParseExpression = "([+\\-*/]{1,1})\\s{0,}(\\-?[\\d\\.]+)";
private Regex arithmeticRegex = new Regex(ArithmeticParseExpression);
public object Convert(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture)
{
if (value is double && parameter != null)
{
string param = parameter.ToString();
if (param.Length > 0)
{
Match match = arithmeticRegex.Match(param);
if (match != null && match.Groups.Count == 3)
{
string operation = match.Groups[1].Value.Trim();
string numericValue = match.Groups[2].Value;
double number = 0;
if (double.TryParse(numericValue, out number)) // this should always succeed or our regex is broken
{
double valueAsDouble = (double)value;
double returnValue = 0;
switch (operation)
{
case "+":
returnValue = valueAsDouble + number;
break;
case "-":
returnValue = valueAsDouble - number;
break;
case "*":
returnValue = valueAsDouble * number;
break;
case "/":
returnValue = valueAsDouble / number;
break;
}
return returnValue;
}
}
}
}
return null;
}
public object ConvertBack(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture)
{
throw new Exception("The method or operation is not implemented.");
}
}
}
Если кэшируется элемент, содержащий другие элементы, такой как контейнер компоновки, то все его подэлементы будут кэшированы в одном растровом изображении. Поэтому следует проявлять осторожность при добавлении кэширования к чему-либо вроде Canvas; делайте это только в том случае, если элемент Canvas мал и его содержимое неизменно.
После внесения этого единственного простого изменения можно сразу заметить разницу. Окно станет появляться чуть медленнее. Зато анимация будет происходить более гладко, и нагрузка на центральный процессор существенно сократится. В этом легко удостовериться, заглянув в диспетчер задач: нередко значение загрузки, близкое к 100%, может сократиться до менее чем 20%.
Обычно, когда включено кэширование растровых изображений, WPF делает снимок элемента в его текущих размерах и копирует его на видеокарту. Это может привести к проблемам, если впоследствии элемент увеличивается с помощью ScaleTransform. В таком случае увеличивается кэшированное растровое изображение, а не сам элемент, что может привести к ухудшению качества за счет укрупнения пикселей.
Например, представьте, что в предыдущем примере вторая параллельная анимация увеличивает Path в десять раз по сравнению с его исходными размерами и затем возвращает первоначальные размеры. Чтобы гарантировать хорошее качество, можно кэшировать растровое изображение Path с размерами, в пять раз большими текущих размеров:
<Path ...>
<Path.CacheMode>
<BitmapCache RenderAtScale="5"></BitmapCache>
</Path.CacheMode>
</Path>
Это решит проблему искажения пикселей. Кэшированное растровое изображение все равно меньше максимального анимированного размера Path (который в 10 раз больше начального), но видеокарта может удвоить размер растрового изображения с 5-кратного до 10-кратного без заметных потерь в качестве. И что более важно, это позволит приложению обойтись без значительного расхода видеопамяти.