Главная » Разработка для Windows Phone 7 » Поучительная история Windows Phone 7

0

В предыдущих главах я показал, как с помощью метода CompositionTarget.Rendering перемещать и изменять визуальные объекты синхронно с частотой обновления экрана. Эта методика Silverlight хорошо подходит для некоторых сценариев, но пользоваться ею надо с осторожностью. Если вы действительно хотите применять CompositionTarget.Rendering в полноценных игровых циклах, например, пора подумать о XNA.

Большая проблема в том, что иногда CompositionTarget.Rendering работает не так хорошо, как ожидается. Например, вспомним приложение Spiral из главы 13 и рассмотрим приложение RotatedSpiral (Вращающаяся спираль), в котором делается попытка использовать CompositionTarget.Rendering для вращения спирали.

Проект Silverlight: RotatedSpiral Файл: MainPage.xaml.cs (фрагмент)

public partial class MainPage : PhoneApplicationPage {

RotateTransform rotateTransform = new RotateTransform();

public MainPage() {

InitializeComponent(); Loaded += OnLoaded;

}

void OnLoaded(object sender, RoutedEventArgs args) {

Point center = new Point(ContentPanel.ActualWidth / 2 – 1,

ContentPanel.ActualHeight / 2 – 1); double radius = Math.Min(center.X, center.Y);

Polyline polyline = new Polyline();

polyline.Stroke = this.Resources["PhoneForegroundBrush"] as Brush; polyline.StrokeThickness = 3;

for (double angle = 0; angle < 3600; angle += 0.25) {

double scaledRadius = radius * angle / 3600;

double radians = Math.PI * angle / 180;

double x = center.X + scaledRadius * Math.Cos(radians); double y = center.Y + scaledRadius * Math.Sin(radians); polyline.Points.Add(new Point(x, y));

}

ContentPanel.Children.Add(polyline);

rotateTransform.CenterX = center.X; rotateTransform.CenterY = center.Y; polyline.RenderTransform = rotateTransform;

CompositionTarget.Rendering += OnCompositionTargetRendering;

}

void OnCompositionTargetRendering(object sender, EventArgs args) {

TimeSpan elapsedTime = (args as RenderingEventArgs).RenderingTime; rotateTransform.Angle = 360 * elapsedTime.TotalSeconds / 3 % 360;

}

}

Код здесь практически аналогичен приложению Spiral, но обратите внимание на поле RotateTransform. В конце обработчика события Loaded этот RotateTransform задается как значение свойства RenderTransform объекта Polyline, определяющего спираль, и прикрепляется обработчик события CompositionTarget.Rendering. Этот обработчик события меняет свойство Angle объекта RotateTransform, обеспечивая один поворот спирали каждые 3 секунды.

В этом коде нет никаких ошибок, но его производительность просто ужасная. Экран обновляется лишь один или два раза в секунду, и получаемая анимация очень прерывистая.

Мы рассмотрим три пути решения этой проблемы и повышения производительности. К счастью все они достаточно универсальны и могут применяться не только в данном конкретном приложении.

Решение 1:Упрощение графических элементов. Спираль реализована как полилиния, включающая 14400 точек. Это много больше, чем достаточно. Если в цикле for задать приращение 5, а не 0,25, спираль останется такой же гладкой, но и анимация станет более сглаженной. Вывод: чем меньше визуальных объектов, тем лучше производительность. Упростите свои графические элементы и деревья визуальных элементов.

Решение 2: Кэширование визуальных элементов. Silverlight пытается вращать Polyline, состоящий из множества отдельных точек. Эта задача была бы намного проще, если бы спираль была не сложным Polyline, а обычным растровым изображением. Можно создать WriteableBitmap этого графического объекта и вращать его. Или можно позволить Silverlight самостоятельно выполнить эквивалентную оптимизацию, просто задав для Polyline следующее свойство:

polyline.CacheMode = new BitmapCache();

Таким образом, Silverlight получает команду создать растровое изображение заданного элемента и использовать это растровое изображение для формирования визуального представления. Не следует применять данную методику к динамически изменяющимся векторным графическим элементам. Однако сложные графические элементы, в целом статические, но к которым может применяться анимация, являются замечательными кандидатурами для растрового кэширования. В XAML это выглядит следующим образом:

CacheMode="BitmapCache"

Перепишем приложение RotatedSpiral, сохранив число точек в Polyline и не прибегая к растровому кэшированию, но заменив CompositionTarget.Rendering объектом DoubleAnimation:

Проект Silverlight: AnimatedSpiral Файл: MainPage.xaml.cs (фрагмент)

public partial class MainPage : PhoneApplicationPage {

public MainPage() {

InitializeComponent(); Loaded += OnLoaded;

}

void OnLoaded(object sender, RoutedEventArgs args) {

Point center = new Point(ContentPanel.ActualWidth / 2 – 1,

ContentPanel.ActualHeight / 2 – 1); double radius = Math.Min(center.X, center.Y);

Polyline polyline = new Polyline();

polyline.Stroke = this.Resources["PhoneForegroundBrush"] as Brush; polyline.StrokeThickness = 3;

for (double angle = 0; angle < 3600; angle += 0.25) {

double scaledRadius = radius * angle / 3600; double radians = Math.PI * angle / 180;

double x = center.X + scaledRadius * Math.Cos(radians); double y = center.Y + scaledRadius * Math.Sin(radians); polyline.Points.Add(new Point(x, y));

}

ContentPanel.Children.Add(polyline);

RotateTransform rotateTransform = new RotateTransform(); rotateTransform.CenterX = center.X; rotateTransform.CenterY = center.Y; polyline.RenderTransform = rotateTransform;

DoubleAnimation anima = new DoubleAnimation {

From = 0, To = 360,

Duration = new Duration(TimeSpan.FromSeconds(3)), RepeatBehavior = RepeatBehavior.Forever

Storyboard.SetTarget(anima, rotateTransform); Storyboard.SetTargetProperty(anima,

new

PropertyPath(RotateTransform.AngleProperty));

Storyboard storyboard = new Storyboard();

storyboard.Children.Add(anima);

storyboard.Begin();

}

}

И получаем намного более сглаженную анимацию, чем в предыдущем случае.

Чем обусловлена такая большая разница? Ведь анимации Silverlight на некотором уровне используют некоторый эквивалент CompositionTarget.Rendering, правда?

По правде говоря, это не так. Это во многом справедливо для настольной версии Silverlight, но Silverlight for Windows Phone доработан, чтобы обеспечить более активное применение графического процессора (graphics processing unit, GPU). GPU обычно ассоциируют с аппаратными ускорениями обработки сложных текстур и другими алгоритмами 3D графики, но в Silverlight GPU применяется для простых 2D анимаций.

Большинство приложений на Silverlight выполняются в одном потоке, который называют потоком пользовательского интерфейса или UI-потоком. UI-поток обрабатывает сенсорный ввод, компоновку и событие CompositionTarget.Rendering. Также используются некоторые рабочие потоки для таких задач, как растеризация, декодирование мультимедиа, датчики и асинхронный веб-доступ.

Silverlight for Windows Phone поддерживает поток компоновщика или обрабатывающий поток, в котором используется GPU.Обрабатывающий поток используется для нескольких типов анимаций свойств типа double, в частности:

•         Трансформаций, задаваемых для свойства RenderTransform.

•         Трансформаций перспективы, задаваемых для свойства Projection.

•         Присоединенных свойств Canvas.Left и Canvas.Top.

•         Свойства Opacity.

•         Всех видов прямоугольных вырезаний.

Анимации, целевыми свойствами которых являются свойства типа Color или Point, по- прежнему выполняются в UI-потоке. Непрямоугольное вырезание или применение OpacityMask также осуществляются в UI-потоке и могут приводить к снижению производительности.

В качестве небольшой демонстрации рассмотрим проект UlThreadVsRenderThread (Сравнение Ul-потока с обрабатывающим потоком), в котором вращение текста реализовано двумя разными способами:

Проект Silverlight: UIThreadVsRenderThread Файл: MainPage.xaml (фрагмент)

<Grid x:Name="ContentPanel" Grid.Row="1" Margin="12,0,12,0"> <Grid.RowDefinitions>

<RowDefinition Height="*" /> <RowDefinition Height="*" /> <RowDefinition Height="Auto" /> </Grid.RowDefinitions>

<TextBlock Grid.Row="0"

Text="UI Thread"

FontSize="{StaticResource PhoneFontSizeLarge}" HorizontalAlignment="Center" VerticalAlignment="Center" RenderTransformOrigin="0.5 0.5"> <TextBlock.RenderTransform>

<RotateTransform x:Name="rotate1" /> </TextBlock.RenderTransform> </TextBlock>

<TextBlock Grid.Row="1"

Text="Render Thread"

FontSize="{StaticResource PhoneFontSizeLarge}" HorizontalAlignment="Center" VerticalAlignment="Center" RenderTransformOrigin="0.5 0.5">

<TextBlock.RenderTransform>

<RotateTransform x:Name="rotate2" /> </TextBlock.RenderTransform> </TextBlock>

<Button Grid.Row="2"

Content="Hang for 5 seconds" HorizontalAlignment="Center" Click="OnButtonClick" />

</Grid>

Вращение первого TextBlock описывается в коде с использованием событий CompositionTarget.Rendering. Для второго TextBlock создается анимация с помощью следующего Storyboard, описанного в коллекции Resources страницы:

Проект Silverlight: UIThreadVsRenderThread Файл: MainPage.xaml (фрагмент)

<phone:PhoneApplicationPage.Resources> <Storyboard x:Name="storyboard">

<DoubleAnimation Storyboard.TargetName="rotate2"

Storyboard.TargetProperty="Angle" From="0" To="3 60" Duration="0:1:0" RepeatBehavior="Forever" />

</Storyboard> </phone:PhoneApplicationPage.Resources>

Конструктор MainPage запускает анимацию и подключает обработчик события CompositionTarget.Rendering.

Проект Silverlight: UIThreadVsRenderThread Файл: MainPage.xaml.cs (фрагмент)

public partial class MainPage : PhoneApplicationPage {

DateTime startTime;

public MainPage() {

InitializeComponent();

storyboard.Begin(); startTime = DateTime.Now;

CompositionTarget.Rendering += OnCompositionTargetRendering;

}

void OnCompositionTargetRendering(object sender, EventArgs args) {

TimeSpan elapsedTime = DateTime.Now – startTime; rotate1.Angle = (elapsedTime.TotalMinutes * 360) % 360;

}

void OnButtonClick(object sender, RoutedEventArgs args) {

Thread.Sleep(5000);

}

}

Здесь для CompositionTarget.Rendering применяется логика синхронизации по времени, что обеспечивает одинаковую скорость перемещения обеих анимаций. Но нажмите эту кнопку, и произойдет нечто удивительное: TextBlock, вращение которого обеспечивается событиями CompositionTarget.Rendering, полностью замирает на пять секунд, тогда как TextBlock,

приводимый в движение DoubleAnimation, продолжает перемещаться! Это продолжает выполнение обрабатывающий поток даже несмотря на то, что Ul-блокирован.

В случае применения к тексту вращения и масштабирования полезно знать другую настройку. Это присоединенное свойство, которое задается в XAML следующим образом:

TextOptions.TextHintingMode="Animated"

Альтернативой ему является свойство Fixed (Фиксированный), которое используется по умолчанию. Никакой оптимизации для повышения четкости и разборчивости текста, обозначенного как Animated, не производится.

Silverlight имеет три встроенные функции для визуализации проблем с

производительностью. Все они могут использоваться на эмуляторе телефона, но приложения на эмуляторе обычно выполняются быстрее, чем на телефоне, поэтому мы все равно не сможем получить реальной картины производительности.

Более подробно вопросы производительности рассматриваются в документе «Creating High Performance Silverlight Applications for Windows Phone[19]», который доступен онлайн. Класс Settings пространства имен System.Windows.Interop включает три свойства типа Boolean, которые помогут визуализировать производительность. Доступ к этим трем свойствам осуществляется посредством объекта SilverlightHost (Хост Silverlight), который доступен как свойство Host (Хост) текущего объекта Application. Этим свойствам придается настолько важное значение, что они уже заданы – и все, кроме одного, закомментированы – в конструкторе класса App в стандартном файле App.xaml.cs.

Вот первое из них:

Application.Current.Host.Settings.EnableFrameRateCounter = true;

Данный флаг активирует небольшое окно сбоку экрана телефона, в котором отображается несколько элементов:

•           Частота кадров (в кадрах в секунду) обрабатывающего потока (GPU).

•           Частота кадров (в кадрах в секунду) Ul-потока (CPU).

•           Объем используемой видео-памяти в килобайтах.

•           Число текстур, хранящихся в GPU.

•           Число промежуточных объектов, созданных для сложных графических элементов.

•          Доля закрашенных пикселов экрана в каждом кадре.

Самыми важными являются первые два показателя. При выполнении приложения на телефоне они должны составлять около 30 кадров в секунду. Вероятно, лучше всего использовать эти числа (и другие) для сравнения: запомните, чему они равны для относительно простых приложений, и затем отслеживайте изменения по мере того, как приложения становятся более сложными.

Второй используемый для диагностики флаг:

Application.Current.Host.Settings.EnableRedrawRegions = true;

Этот флаг достаточно любопытен. Каждый раз, когда Ul-потоку требуется растеризировать графические элементы определенной области экрана, эта область выделяется другим цветом. (Иногда эти области называют «грязными», потому что они требуют обновления новыми

визуальными элементами.) Если мерцание наблюдается на больших областях экрана, это значит, что UI-поток не справляется с его обновлением. В идеале мы должны видеть лишь редкие вспышки цвета, которые возникают, когда анимации выполняются обрабатывающим потоком, а не UI-потоком.

И наконец, третий флаг:

Application.Current.Host.Settings.EnableCacheVisualization = true;

Данный флаг использует наложение цвета для выделения областей экрана, кэшированных в растровые изображения. Этот флаг можно применить в приложении RotatedSpiral (версией, использующей CompositionTarget.Rendering). При запуске этого приложения в том виде, в котором оно приведено выше, подцвечен весь экран. Это означает, что весь экран нуждается в растеризации при каждом изменении местоположения Polyline. На это уходит некоторое время, и поэтому производительность так низка. Теперь зададим следующее:

polyline.CacheMode = new BitmapCache();

На этот раз мы видим, что спираль находится в подцвеченном прямоугольнике, и вращается именно этот прямоугольник. Это и есть кэшированное растровое изображение.

Источник: Чарльз Петзольд, Программируем Windows Phone 7, Microsoft Press, © 2011.

По теме:

  • Комментарии