Windows Phone 性能優化(一)


 

在實際的項目開發過程中,應用的性能優化是一個永恆的話題,也是開發者群里最常討論的話題之一,我在之

前的公司做 wp項目時,也遇到過性能的瓶頸。當頁面中加載的內容越來越多時,內存漲幅非常明顯(特別是

一些壁紙類的應用,當用戶向下滑動列表加載更多),當內存超過 120MB 有些機型的發熱明顯,如果內存繼

續上漲,發熱事小,內存泄露后,系統會直接關閉應用。

 

在 wp 系統中自帶的 ListBox 等控件也提供內存虛擬化,但是如果用得不好,可能會破壞虛擬化。

微軟 MSDNWindows Phone 的應用性能注意事項

 

MSDN 部分摘抄:

Silverlight中,為了將數據顯示給用戶,我們需要加載數據和綁定數據,但是哪個會導致性能問題呢?答案是:根據你的數據類型以及界面(UI)的復雜性而定。

通常,加載數據可以在UI線程或者后台線程中實現,數據存在的形式也不經相同,有的序列化為二進制數據,有的序列化為XML文件,有的則是圖片形式存在等等。而數據綁定又有三種不同的綁定形式:一次綁定(One  Time)、單向綁定(One Way)和雙向綁定(Two  Way)。

 

這里簡單介紹下什么是VSPVirtualizingStackPanel

將內容排列和虛擬化在一行上,方向為水平或垂直。“虛擬化”是指一種技術,通過該技術,可根據屏幕上所顯示的項來從大量數據項中生成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));
            }
        }
    }
View Code


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>
View Code

 


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 實現虛擬化功能,

其實原理很簡單,就是在列表中的項,不在屏幕的可視區域內時,動態的隱藏或者刪除,當滑動回來時,再重新

顯示或創建。

 

本文 demo 下載


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM