Windows Phone 7 应用程序的性能





0/5 (0投票)
本文讨论了提高 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()
:模板已应用。SizeChanged
或LayoutUpdated
事件处理程序:布局已完成。
这样,你就能知道控件构造的哪个部分花费的时间最多。
使用分析器
分析的目的是寻找代码瓶颈,即花费时间最多的地方。(嗯,桌面分析器可以提供更多信息,但主要原因就是这个。)
在撰写本文时,唯一的一个 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 控件。)
这是另一个有趣的例子——MonthCalendar 控件用于选择多个日期。(当 MonthCalendar 显示一系列约会时,使用相同的 UI。)
总结: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 是一家拥有悠久移动编程传统的公司,涵盖许多平台,以及面向最终用户和开发者的应用程序工具。