在實際的項目開發過程中,應用的性能優化是一個永恆的話題,也是開發者群里最常討論的話題之一,我在之
前的公司做 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來進行數據虛擬化時,我們需要注意以下幾點:
確保在DataTemplate 中的容器(如Grid)大小固定
在數據對象可以提供相應值時,盡量避免使用復雜的轉換器(Converter)
不要在ListBox中內嵌ListBox
加入動畫驗證 ListBox 項的動態創建和刪除
為了驗證 ListBox 在列表部分內容滑入、滑出屏幕可視區域時,內容是動態創建和刪除的,我在 ListBox 的
ItemTemplate 模版中給每個項加入動畫,並且通過 <EventTrigger RoutedEvent="StackPanel.Loaded">
進行觸發,當滑動列表時,運行效果:
當加載 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 對象,每隔兩秒,把當前應用所占的內存打印到頂部:
#region 內存使用情況 static System.Windows.Threading.DispatcherTimer dispacherTimer; void CheckMemory() { dispacherTimer = new System.Windows.Threading.DispatcherTimer(); dispacherTimer.Interval = TimeSpan.FromSeconds(2); dispacherTimer.Tick += new EventHandler(dispacherTimer_Tick); dispacherTimer.Start(); } static string total = "DeviceTotalMemory"; static string current = "ApplicationCurrentMemoryUsage"; static string peak = "ApplicationPeakMemoryUsage"; static long totlaBytes; static long currentBytes; static long peakBytes; void dispacherTimer_Tick(object sender, EventArgs e) { // 獲取設備的總內存 totlaBytes = (long)Microsoft.Phone.Info.DeviceExtendedProperties.GetValue(total); // 獲取應用當前占用內存 currentBytes = (long)Microsoft.Phone.Info.DeviceExtendedProperties.GetValue(current); // 獲取內存占用的峰值 peakBytes = (long)Microsoft.Phone.Info.DeviceExtendedProperties.GetValue(peak); txtMemory.Text = string.Format("當前:{0:F2}MB; 峰值:{1:F2}MB; 總:{2:F2}MB;",
currentBytes / (1024 * 1024.0), peakBytes / (1024 * 1024.0), totlaBytes / (1024 * 1024.0)); } #endregion
5)初始化 MainPage 中的 列表等操作:
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 的動畫:
當然,如果 ListBox 使用不當也會破壞它的虛擬化,比如有的項目中,把 ListBox 放在 一個 ScrollViewer
中,虛擬化就不起作用了,確實有些這種情況,並且開發者並沒有注意到這個問題所在。比如有的朋友在
ScrollViewer 里,上面放一個 幻燈片,下面放一個 ListBox(或者 ItemsControl 控件):
因為 ListBox 的虛擬化功能不被破壞是需要一定條件的,在后面的文章會介紹如何如何模擬 ListBox 實現虛擬化功能,
其實原理很簡單,就是在列表中的項,不在屏幕的可視區域內時,動態的隱藏或者刪除,當滑動回來時,再重新
顯示或創建。