×
Namespaces

Variants
Actions

Windows Phone 应用性能优化之减少内存占用

From Nokia Developer Wiki
Jump to: navigation, search

这篇文章介绍如何减少 Windows Phone 应用程序中的内存使用

WP Metro Icon UI.png
WP Metro Icon Graph1.png
SignpostIcon XAML 40.png
WP Metro Icon WP8.png
Article Metadata

代码示例
兼容于
文章
段博琼 在 07 Nov 2013 创建
最后由 hamishwillee 在 18 Nov 2013 编辑

Contents

如何减少 Windows Phone 应用内存占用(上)

这篇文章分为两部分,第一部分讲解 ListBox 中自带的虚拟化功能,以及使用时需要注意的地方, 在第二部分,会介绍如果通过算法减少页面中元素的绘制,从而减少内存的占用。

简介

在实际的项目开发过程中,应用的性能优化是一个永恒的话题,也是开发者群里最常讨论的话题之一,我在之 前的公司做 wp项目时,也遇到过性能的瓶颈。当页面中加载的内容越来越多时,内存涨幅非常明显(特别是 一些壁纸类的应用,当用户向下滑动列表加载更多),当内存超过 120MB 有些机型的发热明显,如果内存继 续上涨,发热事小,内存泄露后,系统会直接关闭应用。

在 wp 系统中自带的 ListBox 等控件也提供内存虚拟化,但是如果用得不好,可能会破坏虚拟化。

文档描述

微软 MSDN :Windows Phone 的应用性能注意事项


MSDN 部分摘抄: 在Silverlight中,为了将数据显示给用户,我们需要加载数据和绑定数据,但是哪个会导致性能问题呢?答案是:根据你的数据类型以及界面(UI)的复杂性而定。 通常,加载数据可以在UI线程或者后台线程中实现,数据存在的形式也不经相同,有的序列化为二进制数据,有的序列化为XML文件,有的则是图片形式存在等等。 而数据绑定又有三种不同的绑定形式:一次绑定(One Time)、单向绑定(One Way)和双向绑定(Two Way)。


这里简单介绍下什么是VSP(VirtualizingStackPanel)

将内容排列和虚拟化在一行上,方向为水平或垂直。“虚拟化”是指一种技术,通过该技术,可根据屏幕上所显示的项来从大量数据项中生成user interface (UI) 元素的子集。仅当 StackPanel 中包含的项控件创建自己的项容器时,才会在该面板中发生虚拟化。 可以使用数据绑定来确保发生这一过程。 如果创建项容 器并将其添加到项控件中,则与 StackPanel 相比,VirtualizingStackPanel 不能提供任何性能优势。

VirtualizingStackPanel 是 ListBox 元素的默认项宿主。 默认情况下,IsVirtualizing 属性设置为 true。当 IsVirtualizing 设置为 false 时,VirtualizingStackPanel 的行为与普通 StackPanel 一样。

我们可以将VSP理解为当需要时,VSP会生成容器对象,而当对象不在可视范围内时,VSP就把这些对象从内存中移除。当ListBox很想当大数据量的项目时, 我们不需要将不在可视范围中的对象加载到内存中,从而解决了内存的问题。另外VSP有一个属性CacheMode设置缓存表示形式,默认设为Standard。当我 们需要循环显示,可以将其设置为Recycling。

在ListBox中使用VSP来进行数据虚拟化时,我们需要注意以下几点: 1)确保在DataTemplate 中的容器(如Grid)大小固定 2)在数据对象可以提供相应值时,尽量避免使用复杂的转换器(Converter) 3)不要在ListBox中内嵌ListBox


加入动画验证 ListBox 项的动态创建和删除

为了验证 ListBox 在列表部分内容滑入、滑出屏幕可视区域时,内容是动态创建和删除的,我在 ListBox 的 ItemTemplate 模版中给每个项加入动画,并且通过 <EventTrigger RoutedEvent="StackPanel.Loaded">

进行触发,当滑动列表时,运行效果:

listbox 默认 虚拟化



当加载 200条数据时,看到内存检测才 22MB,实际如果没有虚拟化,内存可达150MB 以上。

Demo 的部分代码介绍(在接下来的 文章二 的列表加载是相似的逻辑)

1)首先自定义一个 News 类,包含两个字段,一个 Title ,一个 Picture

public class News : System.ComponentModel.INotifyPropertyChanged
{
string title;
public string Title
{
get
{
return title;
}
set
{
if (value != title)
{
title = value;
NotifyPropertyChanged("Titlte");
}
}
}
 
string photo;
public string Photo
{
get
{
return photo;
}
set
{
if (value != photo)
{
photo = value;
NotifyPropertyChanged("Photo");
}
}
}
 
public event System.ComponentModel.PropertyChangedEventHandler PropertyChanged;
 
public void NotifyPropertyChanged(string propertyName)
{
if (PropertyChanged != null)
{
PropertyChanged(this, new System.ComponentModel.PropertyChangedEventArgs(propertyName));
}
}
}


2)在工程的根目录下创建一个 Image 文件夹,里面放 10张示例新闻配图

3)MainPage 中只需要关注两个控件,一个是 页面顶部显示内存的:

<TextBlock x:Name="txtMemory" Style="{StaticResource PhoneTextNormalStyle}" Margin="12,0"/>

第二个是显示新闻列表的: 它的默认 ItemsPanelTemplate 是 VirtulizingStackPanel。在有些交换中,需要去掉 ListBox 的虚拟化功能,就可以把这个 VirtulizingStackPanel 换成 StackPanel

<ListBox.ItemsPanel>
<ItemsPanelTemplate>
<VirtualizingStackPanel/>
</ItemsPanelTemplate>
</ListBox.ItemsPanel>


在 ListBox 的 ItemTemplate 中放一个触发器,当 StackPanel 触发 Loaded 事件的时候,播放预定义动画(在 Blend 中设计的动画)。 从而可以判断每次当 ListBox 的 Item 创建完成后,就会触发一次这个动画。StackPanel 中放一个 TextBlock 和一个 Image,用来 显示 News 的 Title 和 Picture 字段。

 <ListBox.ItemTemplate>
<DataTemplate>
<StackPanel x:Name="stack" Orientation="Horizontal" Margin="10,30,0,0">
<StackPanel.Triggers>
<EventTrigger RoutedEvent="StackPanel.Loaded">
<BeginStoryboard>
<Storyboard x:Name="Storyboard1">
<!--略.....-->
</StackPanel>
</DataTemplate>


ListBox 的完整 xaml:

<ListBox x:Name="listbox" ItemsSource="{Binding}" >
<ListBox.ItemsPanel>
<ItemsPanelTemplate>
<VirtualizingStackPanel/>
</ItemsPanelTemplate>
</ListBox.ItemsPanel>
<ListBox.ItemTemplate>
<DataTemplate>
<StackPanel x:Name="stack" Orientation="Horizontal" Margin="10,30,0,0">
<StackPanel.Triggers>
<EventTrigger RoutedEvent="StackPanel.Loaded">
<BeginStoryboard>
<Storyboard x:Name="Storyboard1">
<DoubleAnimationUsingKeyFrames Storyboard.TargetProperty="(UIElement.Projection).(PlaneProjection.RotationX)" Storyboard.TargetName="stack">
<EasingDoubleKeyFrame KeyTime="0" Value="-180"/>
<EasingDoubleKeyFrame KeyTime="0:0:3" Value="0">
<EasingDoubleKeyFrame.EasingFunction>
<QuinticEase EasingMode="EaseOut"/>
</EasingDoubleKeyFrame.EasingFunction>
</EasingDoubleKeyFrame>
</DoubleAnimationUsingKeyFrames>
<DoubleAnimationUsingKeyFrames Storyboard.TargetProperty="(UIElement.Projection).(PlaneProjection.RotationY)" Storyboard.TargetName="stack">
<EasingDoubleKeyFrame KeyTime="0" Value="106"/>
<EasingDoubleKeyFrame KeyTime="0:0:3" Value="0">
<EasingDoubleKeyFrame.EasingFunction>
<QuinticEase EasingMode="EaseOut"/>
</EasingDoubleKeyFrame.EasingFunction>
</EasingDoubleKeyFrame>
</DoubleAnimationUsingKeyFrames>
<DoubleAnimationUsingKeyFrames Storyboard.TargetProperty="(UIElement.Projection).(PlaneProjection.RotationZ)" Storyboard.TargetName="stack">
<EasingDoubleKeyFrame KeyTime="0" Value="0"/>
<EasingDoubleKeyFrame KeyTime="0:0:3" Value="0">
<EasingDoubleKeyFrame.EasingFunction>
<QuinticEase EasingMode="EaseOut"/>
</EasingDoubleKeyFrame.EasingFunction>
</EasingDoubleKeyFrame>
</DoubleAnimationUsingKeyFrames>
<DoubleAnimationUsingKeyFrames Storyboard.TargetProperty="(UIElement.RenderTransform).(CompositeTransform.TranslateX)" Storyboard.TargetName="stack">
<EasingDoubleKeyFrame KeyTime="0" Value="246"/>
<EasingDoubleKeyFrame KeyTime="0:0:3" Value="0">
<EasingDoubleKeyFrame.EasingFunction>
<QuinticEase EasingMode="EaseOut"/>
</EasingDoubleKeyFrame.EasingFunction>
</EasingDoubleKeyFrame>
</DoubleAnimationUsingKeyFrames>
<DoubleAnimationUsingKeyFrames Storyboard.TargetProperty="(UIElement.RenderTransform).(CompositeTransform.ScaleX)" Storyboard.TargetName="stack">
<EasingDoubleKeyFrame KeyTime="0" Value="0.4"/>
<EasingDoubleKeyFrame KeyTime="0:0:3" Value="1">
<EasingDoubleKeyFrame.EasingFunction>
<QuinticEase EasingMode="EaseOut"/>
</EasingDoubleKeyFrame.EasingFunction>
</EasingDoubleKeyFrame>
</DoubleAnimationUsingKeyFrames>
<DoubleAnimationUsingKeyFrames Storyboard.TargetProperty="(UIElement.RenderTransform).(CompositeTransform.ScaleY)" Storyboard.TargetName="stack">
<EasingDoubleKeyFrame KeyTime="0" Value="0.4"/>
<EasingDoubleKeyFrame KeyTime="0:0:3" Value="1">
<EasingDoubleKeyFrame.EasingFunction>
<QuinticEase EasingMode="EaseOut"/>
</EasingDoubleKeyFrame.EasingFunction>
</EasingDoubleKeyFrame>
</DoubleAnimationUsingKeyFrames>
</Storyboard>
</BeginStoryboard>
</EventTrigger>
</StackPanel.Triggers>
<StackPanel.Resources>
 
</StackPanel.Resources>
 
<StackPanel.RenderTransform>
<CompositeTransform/>
</StackPanel.RenderTransform>
<StackPanel.Projection>
<PlaneProjection/>
</StackPanel.Projection>
<Image VerticalAlignment="Top" Source="{Binding Photo}" Width="150"/>
<TextBlock Text="{Binding Title}" Width="250" Foreground="Wheat" FontSize="25" Margin="10,0,0,0" TextWrapping="Wrap"/>
</StackPanel>
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>


4)创建示例新闻,通过 Random 类控制每条新闻的 标题长度和 配图是 随机的:

#region 示例数据源
Random rd = new Random();
void LoadNews(int Length)
{
for (int i = 0; i < Length; i++)
{
NewsList.Add(new News
{
Title = "不过需要注意的是——为了彰显自己对Kevin Kelly多年追随,而非跟风所为,
你最好能够熟记百度百科上有关他生平的介绍,如果记不全也没关系,知道《黑客帝国》
主创人员都被要求看《失控》这件事,就足以应付一干人等了。"
.Substring(0, rd.Next(20,100)),
Photo = "/Images/0" + rd.Next(0, 10) + ".png"
});
};
}
#endregion


在 MainPage 中自定义一个 DispathcerTimer 对象,每隔两秒,把当前应用所占的内存打印到顶部:

ObservableCollection<News> NewsList = new ObservableCollection<News>();//{ get; set; }
// 构造函数
public MainPage()
{
InitializeComponent();
 
this.Loaded += MainPage_Loaded;
}
 
void MainPage_Loaded(object sender, RoutedEventArgs e)
{
 
// 给 NewsList 加载两百条新闻
LoadNews(200);
 
// 设置当前页面的上下文
this.DataContext = NewsList;
 
// 开始打印内存
CheckMemory();
}

运行上面的代码,看到顶部的内存占用很少。当把 VirtualizingStackPanel 换成 StackPanel 时:

<ListBox.ItemsPanel>
<ItemsPanelTemplate>
<VirtualizingStackPanel/>
</ItemsPanelTemplate>
</ListBox.ItemsPanel>

换成:

 <ListBox.ItemsPanel>
<ItemsPanelTemplate>
<StackPanel/>
</ItemsPanelTemplate>
</ListBox.ItemsPanel>


运行工程,靠,内存直接上 200MB,是之前的约 20倍,如果在 512MB 的设备上,会直接被系统杀掉。 并且当滑动时,也不会触发 Loaded 的动画:

Loaded 200 items

当然,如果 ListBox 使用不当也会破坏它的虚拟化,比如有的项目中,把 ListBox 放在 一个 ScrollViewer 中,虚拟化就不起作用了,确实有些这种情况,并且开发者并没有注意到这个问题所在。比如有的朋友在 ScrollViewer 里,上面放一个 幻灯片,下面放一个 ListBox(或者 ItemsControl 控件): 06112638-f79a1dd0de7a401a9660b12e3bab1000.png

因为 ListBox 的虚拟化功能不被破坏是需要一定条件的,在后面的文章会介绍如何如何模拟 ListBox 实现虚拟化功能, 其实原理很简单,就是在列表中的项,不在屏幕的可视区域内时,动态的隐藏或者删除,当滑动回来时,再重新 显示或创建。

如何减少 Windows Phone 应用内存占用(下)

这篇文章的 demo 是在 (上)的基础上进行的调整,逻辑基本相似。本文只列和 上一篇出不同的代码。

为了实现自定义的虚拟化,把上一篇文章的 ListBox 换成 ScrollViewer + ItemsControl,这样组合在实际的项目 中又是还是会用到的,比如,如果我们需要对 ScrollViewer 进行很多的控制,比如获取它的“滑动”事件,ScrollViewer 中在放置其它控件,或者直接定制它的样式等等(当然可以通过 VisualTreeHelper 也可以获取 ListBox 中的 ScrollViewer)。

ListBox (继承自 ItemsControl)内部的实现就是封装了 ScrollViewer + ItemsControl 控件,在本 demo 中,使用的组合为:

<ScrollViewer x:Name="scrollViewer" Loaded="ScrollViewer_Loaded">
<ItemsControl x:Name="listbox" ItemsSource="{Binding}">
<ItemsControl.ItemsPanel>
<ItemsPanelTemplate>
<!---虽然设置为“虚拟面板”,但是它是不起虚拟作用的-->
<VirtualizingStackPanel/>
</ItemsPanelTemplate>
</ItemsControl.ItemsPanel>
<ItemsControl.ItemTemplate>
<DataTemplate>
<StackPanel Orientation="Horizontal" Margin="10,30,0,0">
<Image VerticalAlignment="Top" Source="{Binding Photo}" Width="150"/>
<TextBlock Text="{Binding Title}" Width="250" Foreground="Wheat"
FontSize="25" Margin="10,0,0,0" TextWrapping="Wrap"/>
</StackPanel>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
</ScrollViewer>


在上一篇 demo 的基础上,当加载 200条数据时,在 1G 的模拟器上运行时,内存占用竟达到 200+MB, 如果在 512MB 的模拟器上,还没加载数据完成,应用就崩溃了: memory is high

优化算法

下面 demo 的原理很简单,就是当列表中的项,在屏幕内的时候,把它的 Visibility 设置为 Visibility.Visible, 当在屏幕外面的时候,设置为 Visibility.Collapsed; 逻辑很简单,但是对内存的占用明显下降。但是,为了用户 体验,也就是如果当用户滑动列表到屏幕的地方,它的项目没有及时的显示,在用户的角度看,是会非常沮丧的,所以 需要一个算法检查当前列表中的项是否在屏幕内。

思路:

structure of demo


关于这个 demo 其它部分的代码请参考文章(上)。

1)首先在 xaml 页面放一个按钮,如上图所示,当应用加载完成时,默认不错任何处理,当点击 “虚拟化” 按钮时, 触发自定义虚拟化方法,页面中的 xaml:

<Button Content="虚拟化" HorizontalAlignment="Left" Margin="335,0,0,0"
VerticalAlignment="Top" Width="133" Height="72" Tap="Button_Tap"/>


相应的 C#:

       //当用户单击 按钮时,开启模拟虚拟化
private void Button_Tap(object sender, System.Windows.Input.GestureEventArgs e)
{
e.Handled = true;
 
Visualizition();
}


2)当点就按钮后,首先获取列表中,所有由 DataTemplate 中的 StackPanel 复制的每一项。因为 ListBox 继承自 ItemsControl, 并且它们 ItemContainerGenerator 属性的 ContainerFromIndex(int index) 方法可以获取列表中的指定的 Item,然后在通过 VisualTreeHelper 的静态方法,获取模版产生的 StackPanel。全部的代码:

void Visualizition()
{
// 自定义一个“字典”,用来保存每项的 Y坐标 和它“本身”的引用
Dictionary<double, StackPanel> dic = new Dictionary<double, StackPanel>();
 
double height_sum = 0;
for (int i = 0; i < listbox.Items.Count; i++)
{
// 因为 ListBox 是通过数据绑定生成的列表,所以需要通过 ItemContainerGenerator 获得
// 每一项的 StackPanel 这个父控件
FrameworkElement fe = listbox.ItemContainerGenerator.ContainerFromIndex(i) as FrameworkElement;
StackPanel sp = FindFirstElementInVisualTree<StackPanel>(fe);
 
dic.Add(height_sum, sp);
 
// 累加 Y高度
height_sum = height_sum + sp.ActualHeight + 30; //30 为模版中父容器的 margin-top
 
// 设置它的高度为自己的实际高度
sp.Height = sp.ActualHeight; //很重要,如果不为父容器指定固定高度,当子元素隐藏后,父容器高度变为0px
}
 
// 每0.5秒钟,循环检查一次列表中,哪些项在屏幕内,如果在屏幕内,则显示,如果
// 在屏幕外,则隐藏
Observable.Interval(TimeSpan.FromSeconds(.5)).ObserveOnDispatcher().Subscribe((_) =>
{
foreach (var keyValue in dic)
{
if (((scrollViewer.VerticalOffset - 500) > keyValue.Key || keyValue.Key > (scrollViewer.VerticalOffset + 900)))
{
keyValue.Value.Children[0].Visibility = System.Windows.Visibility.Collapsed;
keyValue.Value.Children[1].Visibility = System.Windows.Visibility.Collapsed;
}
else
{
 
keyValue.Value.Children[0].Visibility = System.Windows.Visibility.Visible;
keyValue.Value.Children[1].Visibility = System.Windows.Visibility.Visible;
}
}
});
}
 
// 查找“视图树”中的控件
private T FindFirstElementInVisualTree<T>(DependencyObject parentElement) where T : DependencyObject
{
var count = VisualTreeHelper.GetChildrenCount(parentElement);
if (count == 0) return null;
 
for (int i = 0; i < count; i++)
{
var child = VisualTreeHelper.GetChild(parentElement, i);
 
if (child != null && child is T)
{
return (T)child;
}
else
{
var result = FindFirstElementInVisualTree<T>(child);
if (result != null)
return result;
}
}
 
return null;
}


当加载 200 条新闻的时候,运行工程效果:

memory goes down

上面算法是每 0.5秒 遍历一下 Dictionary 的 keys,为了直观就没有再优化。比如每次遍历的时间, 屏幕的可视区域等。


默认运行时,内存占用 208MB ,效果:

no virtualization


单击按钮后,当上下滑动的时候,可以看到延迟显示的 item,内存占用减少了不少:

virtualization


另外,我想到可以使用 快速排序 的算法方法,可以更快找到新滑动到屏幕里的 Item,之前在屏幕外的 Item 如果还在屏幕外,则跳过,等等。关于如何优化上面算法这里就不在多讲了。因为项目只是在介绍减少内存的思路, 所以没有考虑在应用中如何在“加载更多..”时,如何再次添加新 item 等等实际交互。

还有就是关于 Reactive Extension 相关类库(已经集成在了 WP8 的sdk 中)的使用,这里也不过多介绍,它 确实是一个神奇的东西,园子里有朋友写过相关的文章,我前段时间也翻译了一下(译文链接),稍后会整理 更多关于 Rx 的文章。这里使用 Observable 作为计时器,当然也可以自定义 Timer ,不过感觉 Observable 用起来 更加方便。

上面代码中:Observable.Interval(TimeSpan.FromSeconds(.5)).ObserveOnDispatcher().Subscribe((_) => { //省略 }); 的含义是,每隔 0.5秒钟,在 UI 线程中 调用一次 Subscribe 注册的方法。

引申

通过这个 demo,开发者应该知道了,在页面中,尽量少的绘制元素,对于 Windows Phone 应用程序性能的提升,对于内存占用 的优化,有多么的明显。例如,尽量减少 UI 控件的嵌套;在 Pivot (或者 Panorama )页面控件中的项,如果 PivotItem 不在 当前屏幕中,则把它的 Child 设为隐藏,当用户切换到该 PivotItem 页面时,在给它显示出来。等等。

工程代码下载

File:OnByOneDemo.zip

File:VirtualizationListBoxDemo.zip


Version Hint

Windows Phone: [[Category:Windows Phone]]
[[Category:Windows Phone 7.5]]
[[Category:Windows Phone 8]]

Nokia Asha: [[Category:Nokia Asha]]
[[Category:Nokia Asha Platform 1.0]]

Series 40: [[Category:Series 40]]
[[Category:Series 40 1st Edition]] [[Category:Series 40 2nd Edition]]
[[Category:Series 40 3rd Edition (initial release)]] [[Category:Series 40 3rd Edition FP1]] [[Category:Series 40 3rd Edition FP2]]
[[Category:Series 40 5th Edition (initial release)]] [[Category:Series 40 5th Edition FP1]]
[[Category:Series 40 6th Edition (initial release)]] [[Category:Series 40 6th Edition FP1]] [[Category:Series 40 Developer Platform 1.0]] [[Category:Series 40 Developer Platform 1.1]] [[Category:Series 40 Developer Platform 2.0]]

Symbian: [[Category:Symbian]]
[[Category:S60 1st Edition]] [[Category:S60 2nd Edition (initial release)]] [[Category:S60 2nd Edition FP1]] [[Category:S60 2nd Edition FP2]] [[Category:S60 2nd Edition FP3]]
[[Category:S60 3rd Edition (initial release)]] [[Category:S60 3rd Edition FP1]] [[Category:S60 3rd Edition FP2]]
[[Category:S60 5th Edition]]
[[Category:Symbian^3]] [[Category:Symbian Anna]] [[Category:Nokia Belle]]

This page was last modified on 18 November 2013, at 08:05.
409 page views in the last 30 days.

Was this page helpful?

Your feedback about this content is important. Let us know what you think.

 

Thank you!

We appreciate your feedback.

×