×
Namespaces

Variants
Actions
(Difference between revisions)

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

From Nokia Developer Wiki
Jump to: navigation, search
段博琼 (Talk | contribs)
(段博琼 - - 加入动画验证 ListBox 项的动态创建和删除)
段博琼 (Talk | contribs)
(段博琼 - - 优化算法)
Line 486: Line 486:
 
上面算法是每 0.5秒 遍历一下 Dictionary 的 keys,为了直观就没有再优化。比如每次遍历的时间,
 
上面算法是每 0.5秒 遍历一下 Dictionary 的 keys,为了直观就没有再优化。比如每次遍历的时间,
 
屏幕的可视区域等。  
 
屏幕的可视区域等。  
 +
 +
 +
默认运行时,内存占用 208MB ,效果:
 +
[[File:07144152-f7f67a7db2c84defa5c0fa15b91d8bda.gif|no virtualization]]
 +
 +
 +
单击按钮后,当上下滑动的时候,可以看到'''延迟显示'''的 item,内存占用减少了不少:
 +
[[File:07144341-3a4b87b765c74b0c8add355b7932e60e.gif|virtualization]]
 +
  
 
另外,我想到可以使用 '''快速排序''' 的算法方法,可以更快找到新滑动到屏幕里的 Item,之前在屏幕外的 Item  
 
另外,我想到可以使用 '''快速排序''' 的算法方法,可以更快找到新滑动到屏幕里的 Item,之前在屏幕外的 Item  

Revision as of 10:08, 7 November 2013


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

WP Metro Icon UI.png
WP Metro Icon Graph1.png
SignpostIcon XAML 40.png
WP Metro Icon WP8.png
Article Metadata
Compatibility
Platform(s):
Windows Phone 8
Article
Created: 段博琼 (07 Nov 2013)
Last edited: 段博琼 (07 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;
 
// 设置它的高度为自己的实际高度
sp.Height = sp.ActualHeight;
}
 
// 每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 如果还在屏幕外,则跳过,等等。关于如何优化上面算法这里就不在多讲了。

还有就是关于 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]]

289 page views in the last 30 days.