65.9K
CodeProject 正在变化。 阅读更多。
Home

Windows Phone 7 应用程序的性能

2011年1月5日

CPOL

12分钟阅读

viewsIcon

53576

本文讨论了提高 WP7 应用程序性能的各种方法。

Silverlight 和移动设备——这个组合在不久前还被认为是不可行的。原因很简单——Silverlight 灵活强大,但需要很高的计算能力。

那么,在 WP7 设备使用几个月后,现实情况如何?

我们将讨论性能。好吧,那么“性能”是什么意思?

大多数用户——当他们看到两秒钟内没有任何变化时——会认为有什么东西坏了,并采取相应行动。处理这种情况,你有两个选择:

  • 让你的应用程序运行得更快,即提高实际性能。
  • 让你的应用程序看起来运行得更快,即提高感知性能。

事实上,积极的感知性能是向 WP7 市场提交应用程序时的核心要求之一。应用程序不应该看起来无响应,你需要在冗长的计算过程中吸引用户,展示一些动作、动画等。

接下来,我们将讨论提高性能的各种方法。然后,我们将尝试用具体的例子来展示这些一般性的观点。

必备组件

目标受众是具备 Silverlight 阅读能力的 C# .NET 程序员。至少,读者应该理解基本的 Silverlight 控件和布局。

您需要安装 Windows Phone Developer Tools。

如何分析性能

问题

一个重要的问题是时间测量。不幸的是,WP7 Silverlight 只能测量毫秒。这意味着任何结果都必须谨慎对待。方法越短,性能测量越困难。

第二个大问题是随机性(例如,由垃圾回收器引起)。为了减少这种影响,应该重复分析几次。

手动测量

如果你知道问题出在哪里,你可以自己测量,例如使用这段代码:

GC.Collect();          // Optional; reduces the randomness of  the garbage collector

int  startTime = Environment.TickCount;
//....  measured code ...
Debug.WriteLine("{0}  msec", Environment.TickCount - startTime);
// You  could also use the Stopwatch class to measure the time it takes.

下面是一个可用于手动测量的实用类:

/// A simple profiling tool. Represents single instance of  the standard StopWatch class.
/// Remarks: All methods are compiled conditionally in the  DEBUG mode.
public class DebugWatch
                static  private System.Diagnostics.Stopwatch m_watch;
                
                static  private void AssertValid()  {
                                if  (m_watch == null)
                                                m_watch  = new System.Diagnostics.Stopwatch();
                }
                
                ///  <summary>Resets time measurement.</summary>

               [System.Diagnostics.Conditional("DEBUG")]
                static  public void Reset()  {
                                m_watch  = null;
                }
                
                ///  <summary>Starts time measurement.</summary>

               [System.Diagnostics.Conditional("DEBUG")]
                static  public void Start()  {
                                AssertValid();
                                m_watch.Start();
                }
                
                ///  <summary>Stops time measurement</summary>

               [System.Diagnostics.Conditional("DEBUG")]
                static  public void Stop()  {
                                AssertValid();
                                m_watch.Stop();
                }
                
                ///  <summary>Outputs the specified prompt followed by " n  msec".</summary>

               ///  <param name="prompt">The prompt.</param>
               [System.Diagnostics.Conditional("DEBUG")]
                static  public void Print(string prompt)  {
                                AssertValid();
                                System.Diagnostics.Debug.WriteLine("{0}  {1} msec", 
                      prompt, m_watch.ElapsedMilliseconds);
                }
}

如何测量实例化控件所需的时间

你可以在控件的构造函数中开始时间测量,并在这些地方测量经过的时间:

  • Loaded 事件处理程序:此时控件已构造并添加到对象树中;样式已应用。
  • OnApplyTemplate():模板已应用。
  • SizeChangedLayoutUpdated 事件处理程序:布局已完成。

这样,你就能知道控件构造的哪个部分花费的时间最多。

使用分析器

分析的目的是寻找代码瓶颈,即花费时间最多的地方。(嗯,桌面分析器可以提供更多信息,但主要原因就是这个。)

在撰写本文时,唯一的一个 WP7 分析器来自 Equatec。这是一个简单易用的工具,你可以在几分钟内学会使用。免费版本只有一个限制——它只能分析一个程序集。

Equatec 分析器的问题在于它只会测量你的代码。

事实上,它会对你所有的 C# 方法执行与上面描述类似的测量。这通常很有用,但如果瓶颈在于缓慢的 UI/渲染、绑定等,它就无能为力了。

根据非官方报告,微软正在开发一款 WP7 分析器,该分析器应该能够检查渲染可视化树时花费的时间。如果是这样,我们可以期待另一款有用的工具,它可能会识别出性能问题的另一部分。

无论如何,现在我们别无选择,只能深入了解 WP7 应用程序内部发生了什么,它如何与 SVL 和 .NET 交互等。

分析可视化树

你看到的单个 XAML 对象实际上可能代表一个复杂的结构。例如,一个 TextBox 控件包含三个嵌套控件:Grid、Border 和 ContentControl。(并且有大量的绑定。)

相比之下,TextBlock 完全没有内部结构!

当然,使用 TextBlock 你会失去边框、背景、填充,但也许你不需要这些属性,或者你可以更有效地实现它们。

概括来说:仔细研究你的可视化树,找出可能的简化之处。
要查看应用程序的可视化树,你可以使用 Dave Relyea 的 TreeHelper 类(http://dl.dropbox.com/u/254416/blog/TreeHelper.cs)。

一些通用的布局相关规则

  • 不要嵌套过多的面板(多个布局通道)
  • 使用简单的构造:画布(而不是网格),固定大小的项目
  • 隐藏元素:不涉及处理(但切换到可见意味着完全重新创建和重绘)

更多性能提示

请记住,大多数高级技术都有代价。在桌面设备上受欢迎的东西在移动设备上可能会导致严重的减慢。

虽然本文的目的不是收集网络上散落的所有性能相关提示,但这里至少有一些。

代码中效率更高的操作

  • 数据绑定、转换器:如果可能,用专用属性替换它们。
  • 在代码中创建控件比 Xaml + 绑定更快。
  • 可见性转换器可以通过代码更有效地处理。

典型的低效构造

  • 不必要的背景颜色
  • 不透明度为 0 的 TextBlock
  • 非矩形裁剪、不透明度蒙版
  • 大纹理(任一维度大于 2K 像素)

适用于动画的规则

  • 要在 GPU 上运行,只动画简单的属性——变换和不透明度。
  • 如果依赖属性有更改处理程序,动画该属性将在 UI 线程上进行(速度慢)。
  • 位图缓存意味着元素在第一次渲染通道后被存储并用作位图。
  • 通过将不透明度设置为 0 来隐藏缓存元素可能比隐藏元素更快。

您将找到有关图像、数据/UI 虚拟化、ListBox、Popup、Pivot 和 Panorama 控件等的其他专用提示。

示例——优化 MonthCalendar 控件

MonthCalendar(Resco MobileForms Toolkit, Windows Phone 7 Edition 的一部分)是一个相当高级的控件,可用于各种目的。下面您可以看到日期选择器。(上半部分的 TextBlock 不属于 MonthCalendar 控件。)

WP7_performance_01.png

这是另一个有趣的例子——MonthCalendar 控件用于选择多个日期。(当 MonthCalendar 显示一系列约会时,使用相同的 UI。)

WP7_performance_02.png

总结:MonthCalendar 显示可滚动日历、单选和多选(“蓝点”)。

MonthCalendar 显示一个 6x7 的矩阵(一个 Grid 控件),代表包含当前月份的 6 周。

为了实现流畅的动画——网格延伸到可见区域的上方和下方。(总共 3 页,即 3x6x7=126 个元素)

每个网格单元包含一个 ContentPresenter 项,该项使用此 DataTemplate 映射到数据对象(MonthCalendarItem 类)。

<DataTemplate x:Key="itemTemplate">
                <Border  Background="Transparent" 
                                HorizontalAlignment="Stretch"  VerticalAlignment="Stretch" 
                  Height="69"  Padding="3" >

                                <TextBlock 
                                                FontSize="{StaticResource  PhoneFontSizeSmall}"
                                                Text="{Binding  Label}"
                                                Opacity="{Binding  EnabledOpacity}" 
                                                HorizontalAlignment="Left"  VerticalAlignment="Bottom" />

                </Border>
                
                <span class="code-comment"></span>
                <Border 
                                Visibility="{Binding  Path=HasValue, Converter={StaticResource VisConverter}}" 
                                Background="{StaticResource  PhoneAccentBrush}"

                                BorderThickness="1"
                                BorderBrush="{StaticResource  PhoneBorderBrush}"
                                Width="16"  Height="16" Margin="1"

                                HorizontalAlignment="Right"  VerticalAlignment="Top" />
</DataTemplate>

正如您所见——每个单元格显示几个指示符:

  • 标签(日期编号)。如果标签不属于当前月份,则标签不透明度会降低。
  • 可选的日历事件(“蓝点”)
  • 选择(背景已更改)

问题

126 个 ContentPresenter 在 MonthCalendar 加载时创建。这似乎是一个耗时的操作。

滚动实现得很高效——现有的 ContentPresenter 被重新映射到其他数据项。

以下是我们为优化 MonthCalendar 性能所采取步骤的简要描述。最终,该控件加载速度提高了 5 倍。(嗯,如果我们考虑到托管 MonthCalendar 的测试页面本身需要 60 毫秒才能加载,那么收益甚至更大。)

1. 原始状态 — 2145 毫秒

我们测量了 MonthCalendar 构造函数和 Loaded 事件之间经过的时间。

(在设备上测量,该数字是多次试验的平均值。为了减少波动,在每次测量前都调用了 GC.Collect()。)

2. 移除可见性转换器 — 1903 毫秒

我们移除了“蓝点”的转换器,并在代码中直接操作了可见性。

3. 移除 DataTemplate — 1115 毫秒

我们在代码中创建了 UI 元素:它变成了一个 Grid 实例,所有子元素都是手动创建的。

节省的成本

  • 排除了 ContentPresenter 层(更简单的可视化树)
  • 省略了 DataTemplate 处理

4. 移除绑定 — 772 毫秒

所有三个绑定都替换为直接代码操作。

5. 优化标题 — 699 毫秒

不解释细节,我们只需说这是简化表头可视化树的结果。

6. 按需创建指示器 — 510 毫秒

我们将跳过这里的细节。我们只说那些不常用的子控件(如“蓝点”)不在加载时创建,而是在首次使用时创建。

7. 算法更改——仅在可见区域上方/下方使用 3 行日期 — 408 毫秒

最终优化基于日历的工作方式:一旦向上或向下滚动 50%,就会强制进入新月份。
这意味着我们只需要上半页和下半页,即项目矩阵减少到 2x6x7 = 84 个项目。

未在最终版本中使用的其他优化

  • MonthCalendarItem 从 Canvas 继承而不是 Grid——节省少量。
    (这个优化最终没有使用,因为它需要更多的代码更改。)
  • 设置 CacheMode = new BitmapCache() 虽然之后(动画期间)可能有用,但它对加载有负面影响。

结论

在避免使用几个 SVL 核心功能(模板、绑定、转换器)后,我们可以将 MonthCalendar 的加载速度优化到原来的 5 倍以上。

让我们添加一个有点争议的评论:MonthCalendar 控件的源代码变得更简单了。

示例——不确定 ProgressBar(“动画点”)

我们检查的另一个案例是经常使用的漂亮动画——屏幕上运行的五个小点。从技术上讲,它是一个 ProgressBar 控件,IsIndeterminate 属性设置为 true。

你们中的许多人可能已经注意到了 Jeff Wilcox 的文章,他警告不要使用标准的 ProgressBar(http://www.jeff.wilcox.name/2010/08/performanceprogressbar/),因为它基于动画五个滑块控件,性能影响非常糟糕。ProgressBar 的创建者选择滑块控件,因为它是唯一一个可以在固定间隔(0-100%)和未知宽度上进行动画的标准 SVL 控件。(未知宽度是指在定义动画时。)

相反,Jeff 提供了一个更高效的替代 ProgressBar 模板(称为 PerformanceProgressBar)。Jeff 的解决方案(基于在 SizeChanged 处理程序中更新动画范围)在网上得到了广泛推荐。

我们选择了两种不同的场景进行测试:

  • 一个工作线程以手持 CPU 允许的最大速度运行。在这种场景下,瓶颈是手持处理器。
  • 从 Web 服务下载大量数据并将其存储到数据库中。在这种场景下,瓶颈是缓慢的 HTTP 通信,数据库操作的影响较小。

在这两种情况下,我们都测试了不同的进度呈现可能性。

PerformanceProgressBar 的简要描述

简化的控件布局如下(5 个小透明矩形):

<Grid><Border>

                <Grid>
                                <Rectangle  Height="4" Width="4" x:Name="R1"  Opacity="0" 
                      CacheMode="BitmapCache">

                                                <Rectangle.RenderTransform>
                                                                <TranslateTransform  x:Name="R1TT"/>
                                                </Rectangle.RenderTransform>
                                </Rectangle>

                                //  .... Identical definitions for rectangles R2..R5 
                  // (with transforms R2TT..R5TT)
                </Grid>
</Border></Grid>

使用的简化动画:

<Storyboard RepeatBehavior="Forever"  Duration="00:00:04.4">

                <DoubleAnimationUsingKeyFrames  BeginTime="00:00:00.0" 
                                Storyboard.TargetProperty="X"  Storyboard.TargetName="R1TT">
                                <LinearDoubleKeyFrame  KeyTime="00:00:00.0" Value="0.1"/>

                                <EasingDoubleKeyFrame  KeyTime="00:00:00.5" Value="33.1"/>
                                <LinearDoubleKeyFrame  KeyTime="00:00:02.0" Value="66.1"/>

                                <EasingDoubleKeyFrame  KeyTime="00:00:02.5" Value="100.1"/>
                </DoubleAnimationUsingKeyFrames>
                <DoubleAnimationUsingKeyFrames  BeginTime="00:00:00.0" 
                                Storyboard.TargetProperty="Opacity"  Storyboard.TargetName="R1">

                                <DiscreteDoubleKeyFrame  KeyTime="0" Value="1"/>
                                <DiscreteDoubleKeyFrame  KeyTime="00:00:02.5" Value="0"/>

                </DoubleAnimationUsingKeyFrames>
                
                // ....  Identical animations for R2 (delayed by 0.2 sec) … 
         // R5 (delayed by 0.8 sec)
</Storyboard>

基本上,每个矩形显示 2.5 秒,从左到右移动。

第二个矩形延迟 0.2 秒,第三个延迟 0.4 秒,依此类推,因此整个移动需要 3.3 秒。

然后有 1.1 秒的暂停,整个周期重复。

为繁重的后台计算使用不确定进度条

测试设置

我们创建了一个简单的测试环境,其中 ProgressBar 和繁重的计算运行在后台线程上。
为了模拟完整的 CPU 负载,我们使用了这个类:

public class Worker
{
                private  Thread _thread=null;
                private  static int _count = 0;
                private  int _startTime;
                private  static bool _done = false;           
                public  void StartStop()
                {
                                if  (_thread == null)
                                {
                                                _count  = 0;
                                                _done  = false;
                                                _startTime  = Environment.TickCount;
                                                _thread  = new Thread(DoThread);
                                                _thread.Start();
                                }
                                else

                                {
                                                _done  = true;
                                                _thread  = null;
                                                int  dt = Environment.TickCount - _startTime;
                                                Debug.WriteLine("\n{0}  msec, Count/sec={2}\n", dt, 
                               (_count*1000.0)/dt);
                                }
                }                
                public  static void DoThread()
                {
                                while  (!_done)
                                {
                                                _count++;
                                                int  cnt = 100 + _count % 100;     // a  number with 3 digits

                                               cnt.ToString();
                                }
                }
}

每次线程停止时,我们都会打印 Count/sec 的值,该值表征了后台线程完成的工作。(由于波动,每次测试都重复了几次。)

XAML 代码定义了一个复选框和一个进度条。每当选中复选框时,进度条就被设置为不确定状态。

<StackPanel Orientation="Horizontal">
                <CheckBox x:Name="ProgressCheckBox" />

                <ProgressBar  Width="250"
                                IsIndeterminate="{Binding ElementName=ProgressCheckBox, Path=IsChecked}" 
                                Visibility="{Binding ElementName=ProgressCheckBox, Path=IsChecked, 
                                                Converter={StaticResource BoolToVisibilityConverter}}"  />
</StackPanel>

在 C# 代码中,我们确保当复选框被选中时,工作线程正在运行。

public partial class TestPage : PhoneApplicationPage
{
                private Worker _worker;
 
                public TestPage()
                {
                                InitializeComponent();
                                ProgressCheckBox.Click += new RoutedEventHandler(ProgressCheckBox_Click);
                                _worker = new Worker();
                }
 
                void ProgressCheckBox_Click(object sender, RoutedEventArgs e)
                {
                                bool ?isChecked = ProgressCheckBox.IsChecked;
                                if (isChecked.HasValue)
                                                _worker.StartStop();
                }
}

测试用例和结果

除了标准的进度条,我们还使用了 Jeff Wilcox 的模板(打包在 PerformanceProgressBar 类中——Resco MobileForms Toolkit, Windows Phone 7 Edition 的一部分)。

与描述的测试设置的唯一变化是 Xaml 文件,我们在其中使用了:

<r:PerformanceProgressBar Width="250" 
                IsIndeterminate="{Binding  ElementName=ProgressCheckBox, Path=IsChecked}" 
                Visibility="{Binding  ElementName=ProgressCheckBox, Path=IsChecked,
                                Converter={StaticResource  BoolToVisibilityConverter }}" />

此外,我们测试了修改后的 PerformanceProgressBar,将 Duration 属性设置为 8.8 秒。(即,可见动画之间的暂停时间更长。)

下面展示了典型的结果(数字当然是四舍五入的):

测试用例 每秒计数
无进度条 435000
标准 ProgressBar 153000
PerformanceProgressBar (4.4 秒) 192000
PerformanceProgressBar (8.8 秒) 248000

我们还测试了 PerformanceProgressBar 的其他修改,例如:

  • 动画 3 个点而不是 5 个
  • 动画的其他简化,例如排除不透明度更改或使用 AutoReverse 等。

尽管我们注意到了性能计数器(例如 UI 线程帧率)的改进,但这些更改并未为后台线程性能带来任何可衡量的效果。

显然,单个动画步骤中完成的工作量较小,但后台线程并未运行得更快。看起来动画引擎试图利用可用的计算资源。(一种解释可能是负责动画的线程具有更高的优先级,并适应优化的收益。)

结论

虽然动画可以进行优化,但在计算量大的环境中,最稳妥的做法是根本不使用任何动画。

在 Web 通信期间使用不确定进度条

仅作简要总结

我们使用了 Resco CRM 产品,在下载完整的客户数据(SOAP 协议,2000 名客户)期间测试了各种进度指示器。

以下是典型的数字(近似下载时长):

完全没有进度消息 50 秒
标准 ProgressBar,IsIndeterminate 115 秒
PerformanceProgressBar,IsIndeterminate 75 秒
进度消息代替进度条 55 秒

结论

  • PerformanceProgressBar 带来了重要改进,但仍然效率低下。
  • 虽然通常可以优化动画对象的布局,但主要问题是动画本身。
  • 永远不要使用任何动画来指示繁重后台计算的进度。

关于作者

Jan Slodicka。编程超过 30 年。涵盖了多个桌面平台和编程语言。自 2003 年以来一直为 Resco 从事移动技术工作——Palm OS、Windows Mobile,现在是 Windows Phone 7。

您可以通过 `jano at resco.net` 或通过 Resco 论坛联系我。

Resco MobileForms Toolkit, Windows Phone 7 Edition 可从 http://www.resco.net/developer/mobilelighttoolkit 下载。该工具包包含一组有用的控件,可简化 Windows Phone 7 编程。除了 WP7,还有 Windows Mobile、Android 和 iOS 版本。

Resco 是一家拥有悠久移动编程传统的公司,涵盖许多平台,以及面向最终用户和开发者的应用程序工具。

© . All rights reserved.