Кэширование изображений

58

Кэширование растровых изображений заставляет 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-кратного без заметных потерь в качестве. И что более важно, это позволит приложению обойтись без значительного расхода видеопамяти.

Пройди тесты
Лучший чат для C# программистов