Windows phone應用開發[21]-圖片性能優化


在windows phone 中常在列表中會常包含比較豐富文字和圖片混排數據信息. 針對列表數據中除了談到listbox等控件自身數據虛擬化問題外.雖然wp硬件設備隨着SDK 8.0 發布得到應用可使用內存空間得到了很大擴展. 但為了保證WP 平台在低配置機型同樣的應用操作用戶體驗. 性能調優則是無法避免的問題.

早期在Windows phone 7 版本是受制於當時CE內核對硬件上限制.單個應用最高內存峰值是90M.當應用程序內存超過該峰值沒有任何提示會自動退出.隨着windows phone 8 采用NT內核.硬件設備得到一定擴展.在WP SDK 8.0中 關於內存上限隨着設備不斷演化而存在不同的峰值.更高的內存上限也因設備的類型而有所不同。通常,擁有 1 GB 以上內存的手機被視為高內存設備,但是這也需視設備而定。例如,如果手機擁有高分辨率的相機,就應用用途而言,該手機將被視為低內存設備。下表列出了這些類別中的默認內存上限和更高內存上限:

2013-11-06_173202

當然我們也可以通過獲取DeviceExtendedProperties.GetValue(String) 方法檢查 ApplicationWorkingSetLimit 值,借此檢查應用可用的內存峰值.

而相對Windows phone 7版本達到峰值后自動退出的做法. WP SDK 8.0 做了更多可選項. 而不是簡單粗暴采用直接退出的方式.為了保證我們應用能夠覆蓋更多wp終端.對於低端配置的的手機.可以請求更多的內存或是完全放棄在低端適配.希望應用對所有手機都可用(但這同時可能會因占用更多內存而影響其他手機任務),必須將 FunctionalCapability 條目添加到應用清單文件。要退出 低內存設備,必須將 Requirements 條目添加到清單.

2013-11-06_180318

有了這樣的選項.則可以很自由選擇當前應用在內存使用對於機型的適配.當然對於最好的方法.還是從應用程序角度優化我們應用.達到低端機型也能適配的效果.

本篇來重點談談關於windows phone 應用程序中圖片性能的優化.

首先拋開xaml文件來說所windows phone 圖片常用支持的格式png和jpg. jpg相對於png 解碼的速度要更快.單個圖片顯示並不明顯.在大批量數據UI呈現上才能有一定體現.所以原則上來說對於圖片資源選擇jpg格式要優先.但如果UI呈現類似Backgound Image背景圖片有透明的要求.則只能選擇png格式.jpg並不支持背景透明.

把圖片拿到xaml文件中來說.默認情況下所有的圖片的解碼過程都是在UI線程同步進行,所以如果用如下方式顯示圖片將會將會在一定程度上阻塞UI線程:

   1:  <Image Source=”{Binding ImageUrl}”/>

xaml在ui顯示時會對bingding的圖片進行解碼.解碼完成后才會顯示ui上來.而這個過程中ui進程會一直阻塞到解碼結束才會顯示出來.所以一般情況我們針對Image圖片資源獲取采用后台進程方式來加載:

   1:  <Image>
   2:        <Image.Source>
   3:           <BitmapImage UriSource="{Binding ImgUrl}" CreateOptions="BackgroundCreation"/>
   4:        </Image.Source>
   5:  </Image>

但即使這樣設置我們實際測試發現並沒有明顯的出別. 但如果我們加載一個比較大圖片.加載的時間和UI阻塞上則會出現很明顯的卡頓延遲. 其實說到這里我們UI控制圖片顯示會分為兩種形式 一種綁定另外一種通過后台進行直接賦值的方式. 當我們在后台代碼中向一個Image控件直接賦值BitmapImage. 對於大圖片通常會遇到OutOfMemory內存溢出的問題.如下代碼加載一個大圖片就會出現:

   1:   using (var isoFile = IsolatedStorageFile.GetUserStoreForApplication())
   2:   {
   3:          const string filePath = @"Shared\ShellContent\FlipBackImage.jpg";
   4:          var filename = "Image.png";
   5:          var stream = !isoFile.FileExists(filename) ? null : isoFile.OpenFile(filename, FileMode.Open, FileAccess.Read);
   6:          if (stream != null)
   7:          {
   8:              if (isoFile.FileExists(filePath))
   9:                  isoFile.DeleteFile(filePath);
  10:              
  11:              Debug.WriteLine("currentMemory"+DeviceStatus.ApplicationCurrentMemoryUsage);
  12:              var bi = new BitmapImage();
  13:              bi.CreateOptions = BitmapCreateOptions.None;
  14:              bi.SetSource(stream); //out of memory exception getting here
  15:   
  16:              var wbm=new WriteableBitmap(bi);
  17:              using (var streamFront = isoFile.OpenFile(filePath, FileMode.Create))
  18:              {
  19:                  wbm.SaveJpeg(streamFront, 691, 336, 0, 80);
  20:              }
  21:              Deployment.Current.Dispatcher.BeginInvoke(() => Utils.DisposeImage(wbm));
  22:          }
  23:   }

其實出現outofMemory內存溢出的問題如果仔細分析會發現.加入這段代碼測試采用1500*1000寬高的高分辨率圖片.每個像素pixel占用內存空間為4KB.我們來計算一下一張大圖片在UI渲染顯示后占用的內存空間為:1500 x 1100 x 4 = 6600000 bytes = > 6.5 MB 接近了6.5M左右的大小.受限於手機有限的屏幕分辨率.更大的圖片應在低分辨率下取樣后顯示。如果圖片大於2000*2000其顯示會明顯減慢。特別在低端設備更為明顯.當然也有人針對windows phone 大圖片的顯示給出解決方案:

每次只顯示圖片的一部分。可以通過先將圖片載入到一個T:System.Windows.Media.Imaging.WriteableBitmap中,然后使用LoadJpeg(WriteableBitmap, Stream)擴展方法來載入圖片

這種能有效規避如上出現outofmemory內存泄露的異常.其實相對WriteableBitmap對象而言有一種更好的方式.當我們載入圖片資源后.其實內存很大一部分用在圖片解碼上消耗.如果我們可以采用WriteableBitmap的DecodePixelWidth 和DecodePixelHeight 屬性 來避免圖片重新解碼.獲取器數據流Stream在Memory內存中操作對象.這樣也就避免在顯示時內存過多的開銷.但這里指的注意的是DecodePixelWidth 和DecodePixelHeight在某些情況可能為空值既Null.需要額外處理一下.

另外如果你用過全景視圖控件.默認background是黑色.大多的情況我們會設置背景圖片來填充.其實background是一個特殊的元素,被限定到2048X2048。超過此大小的會被剪裁,這就意味着使用一個長寬超過2048像素的的圖片,該圖片不會完全顯示。為了避免這種情況的發生,WP7平台會自動的降低像素是圖片去適應2048X2048。這一點不同於桌面Silverlight應用,應為silverlight不存在上述限制.

這里指的一提的是.WriteableBitmap目前只在windows phone sdk提供了一個SaveJpeg方法既吧當前資源編碼成一個JPEG流.但並沒有提供png格式的方法. 如果你需要通過WriteableBitmap編碼輸出成一個png格式的stream流.你可以參考如下解決方案:

可以采用writeablebitmapex 開源第三方庫對WriteableBitmap添加一個擴展方法WritePNG擴展方法來實現png格式stream流的轉換

WriteableBitmapEX Project Document

其實本質就是基於WriteableBitmap對象添加了兩個擴展方法.該擴展方法是基於ToolStackCRCLib和ToolStackPNGWriterLib庫實現的.並移植了windows phone 版本,核心擴展類如下:

   1:  using System.IO;
   2:  using System.IO.IsolatedStorage;
   3:  using System.Windows.Shapes;
   4:  using System.Windows.Media;
   5:  using ToolStackCRCLib;
   6:  using ToolStackPNGWriterLib;
   7:   
   8:  namespace System.Windows.Media.Imaging
   9:  {
  10:      /// <summary>
  11:      /// WriteableBitmap Extensions for PNG Writing
  12:      /// </summary>
  13:      public static partial class WriteableBitmapExtensions
  14:      {
  15:          /// <summary>
  16:          /// Write and PNG file out to a file stream.  Currently compression is not supported.
  17:          /// </summary>
  18:          /// <param name="image">The WriteableBitmap to work on.</param>
  19:          /// <param name="stream">The destination file stream.</param>
  20:          public static void WritePNG(this WriteableBitmap image, System.IO.Stream stream)
  21:          {
  22:              WritePNG(image, stream, -1);
  23:          }
  24:   
  25:          /// <summary>
  26:          /// Write and PNG file out to a file stream.  Currently compression is not supported.
  27:          /// </summary>
  28:          /// <param name="image">The WriteableBitmap to work on.</param>
  29:          /// <param name="stream">The destination file stream.</param>
  30:          /// <param name="compression">Level of compression to use (-1=auto, 0=none, 1-100 is percentage).</param>
  31:          public static void WritePNG(this WriteableBitmap image, System.IO.Stream stream, int compression)
  32:          {
  33:              PNGWriter.DetectWBByteOrder();
  34:              PNGWriter.WritePNG(image, stream, compression);
  35:          }
  36:      }
  37:  }

只需要在調用WriteableBitmap 擴展方法中WritePng即可輕松實現把文件輸出一個png格式stream流對象.關於該類庫具體使用請參考官方的文檔.有關圖像的API中都是以圖片自身的分辨率解碼[除了Downsampling縮減像素采樣].如果下載一批600X600的圖片,但只以60X60那么小顯示,那么還以原分辨率解碼就會浪費應用程序的的內存資源.還好windows phone 平台上提供一個PictureDecoder的接口,能夠自定義分辨率去解碼圖片 來節省更多的內存開銷:

   1:  image.Source = PictureDecoder.DecodeJpeg(jpgStream, 60, 60);

剛才上面說到UI xaml文件我們處理圖片兩種方式:

兩種方式:

A: 通過后台代碼對Xaml文件中Image控件直接賦值

B:采用數據綁定方式呈現Image.

其實如果采用后台賦值的方式.首先需要Xaml文件中定義一個Image控件.采用通過UriSource或是Source方式進行賦值操作即可.其實如果你仔細研究過這個問題會發現.即使清空了Source屬性為null並且把Image從visual tree中移除掉,Image的內存還是不會釋放,查看內存占用並沒有少.且頁面退出並沒有立即圖片占用內容進行垃圾回收.本質的問題目前圖片緩存依然還占用着內存的資源.這是其實是一個預留的性能優化機制,實際上為了避免一遍又一遍的加載和解碼相同的圖片。windows phone 中在內存中開辟了一個緩沖區,利用它方便快捷的再利用圖片資源,減緩每次UI響應的時間.雖然圖片緩存對於提高性能有很大的幫助,但是有時候也會增加不必要的內存消耗。特別我們打算回收那些不會再顯示的圖片並把他內存及時釋放掉. 其實可以采用如下代碼來操作xaml中image空間方式能夠做到:

   1:   BitmapImage bitmapImage = image.Source as BitmapImage;
   2:   bitmapImage.UriSource = null;
   3:   image.Source = null;

在最上面xaml文件代碼中我們提到采用BitmapImage 對象的CreateOptions設置為BackgroundCreation. 來減少Ui線程在顯示上阻塞.其實BitmapImage對象CreateOptions默認值為DelayCreation. 當你在頁面這樣設置時.頁面一次加載成功后.當然嘗試去訪問這個圖片大小時,卻發現值為空Null.你有可能會問:為什么當已經完成圖片創建時仍不會返回圖片的大小?這是因為CreateOptions 屬性默認被設置成了“DelayCreation”,也就是說當BitmapImage被設置成一個Image的Source屬性上或者是可視化樹的ImageBrush上,這個對象只有在真正需要它的時候才會被創建.當這個真正被創建時xaml文件中Image控件會自動觸發ImageOpened事件.可以在這里獲取實際圖片大小.

再回到BitmapImage 對象的CreateOptions屬性.其實當頁面加載時.可以直接很多的BitmapImage,卻不消耗任何的資源(CPU或者是內存)直到程序需要特定的圖片才會真正創建它們.當然如果你想圖片在頁面創建立即顯示出來.也就是立即執行創建操作.你可以把CreateOptions設置成“None”即可.關於這個屬性合理使用 后面還會提到.

如上提到很多關於圖片在實際操作可能影響性能以及減少內存開銷一些小細節.其實真正實際項目應用場景.我們大多對於批量的圖片是采用集合控件Listbox等數據綁定來顯示的.很多人在都采用MVVM模式來綁定展現數據.如何在MVVM模式下來優化集合控件中圖片性能? 下半段重點講講這個問題.

首先新建一個項目.命名為:PictureMemoryUsageDemo. 在主頁面Xaml文件采用Listbox來采用MVVM形式來綁定圖片顯示,Xaml UI 布局如下:

   1:          <!--ContentPanel - place additional content here-->
   2:          <Grid x:Name="ContentPanel" Grid.Row="1" Margin="24,0,12,0">         
   3:              <ListBox x:Name="ControlMemory_LB" ItemsSource="{Binding TripPictureCol}" SelectionChanged="ControlMemory_LB_SelectionChanged">  
   4:                  <ListBox.ItemsPanel>
   5:                      <ItemsPanelTemplate>
   6:                          <tool:WrapPanel Orientation="Horizontal"></tool:WrapPanel>
   7:                      </ItemsPanelTemplate>
   8:                  </ListBox.ItemsPanel>
   9:                  <ListBox.ItemTemplate>
  10:                      <DataTemplate>
  11:                          <StackPanel Margin="10,0,0,0">
  12:                              <Image Source="{Binding TripPictureUrl}" Width="100" Height="100"></Image>
  13:                          </StackPanel>
  14:                      </DataTemplate>
  15:                  </ListBox.ItemTemplate>
  16:              </ListBox>
  17:          </Grid>

一個listBox很單一就是簡單綁定一個Image 進行橫向布局. 后台代碼綁定一個標准的ViewModel 綁定形式如下:

   1:     void MainPage_Loaded(object sender, RoutedEventArgs e)
   2:     {
   3:          if (_tripPicViewModel == null)
   4:              _tripPicViewModel = new TripPictureViewModel();
   5:          this.DataContext = _tripPicViewModel;
   6:     }

z在ViewModel 模擬一個圖片數據的ObserverCollection<T>集合進行綁定 ViewModel 代碼如下:

   1:      public class TripPictureViewModel:INotifyPropertyChanged
   2:      {
   3:          #region Property
   4:          public event PropertyChangedEventHandler PropertyChanged;
   5:          private ObservableCollection<TripPictureInfo> _tripPictureCol = new ObservableCollection<TripPictureInfo>();
   6:          public ObservableCollection<TripPictureInfo> TripPictureCol
   7:          {
   8:              get { return _tripPictureCol; }
   9:              set
  10:              {
  11:                  _tripPictureCol = value;
  12:                  BindProertyChangedEventHandler("TripPictureCol");
  13:              }
  14:          }
  15:          #endregion
  16:   
  17:          public TripPictureViewModel()
  18:          {
  19:              LoadTripAddressPictureData();
  20:          }
  21:   
  22:          #region Action
  23:   
  24:          public void BindProertyChangedEventHandler(string propertyName)
  25:          {
  26:              if (string.IsNullOrEmpty(propertyName))
  27:                  return;
  28:   
  29:              PropertyChangedEventHandler eventHandler = this.PropertyChanged;
  30:              if (eventHandler != null)
  31:                  eventHandler(this, new PropertyChangedEventArgs(propertyName));
  32:          }
  33:   
  34:          public void LoadTripAddressPictureData()
  35:          {
  36:              string pictureHealderStr = "/Images/670(";
  37:              string pictureFooterStr = ").jpg";
  38:              for (int count = 0; count < 20; count++)
  39:                  _tripPictureCol.Add(new TripPictureInfo() {  CityName="烏克蘭", StatusCode=(count+1).ToString(), TripPictureUrl=pictureHealderStr+(count+1).ToString()+pictureFooterStr});
  40:          }
  41:          #endregion
  42:   
  43:      }

一個標准的ViewModel綁定就這樣結束了.我們直接打開頁面可以發現UI呈現在UI圖片橫向的排列效果如下:

wp_ss_20131107_0002

一般情況下我們在頁面綁定一個ListBox 集合來呈現簡單圖片顯示.這個時候我們不做任何關於圖片的處理.在這個功能基礎對當前應用在使用過程內存使用情況進行記錄.在單獨添加一個內存使用記錄顯示頁面命名為MemoryLogView.xaml .可以通過DeviceStatus.ApplicationCurrentMemoryUsage 屬性來獲取當前應用內存使用情況.那我們應該在哪里添加日志記錄呢?

首先來看看第一次進入MainPage 時整個PhoneApplicationPage的生命周期.調用流程.首先通過構造方法MainPage().執行完InitializeComponent() 后再加載Loaded()事件.現在構造方法和Load事件開始結束位置分別添加當前內容監控日志:

   1:       public MainPage()
   2:          {
   3:              LogRecordHelper.AddLogRecord("MainPage Init Before:",  DeviceStatus.ApplicationCurrentMemoryUsage.ToString()+" B");
   4:              InitializeComponent();
   5:              this.Loaded += MainPage_Loaded;
   6:              LogRecordHelper.AddLogRecord("MainPage Init After:", DeviceStatus.ApplicationCurrentMemoryUsage.ToString() + " B");
   7:          }

Loaded方法 添加內存使用情況日志監控:

   1:      void MainPage_Loaded(object sender, RoutedEventArgs e)
   2:          {
   3:              LogRecordHelper.AddLogRecord("MainPage Load Before:", DeviceStatus.ApplicationCurrentMemoryUsage.ToString() + " B");
   4:              if (_tripPicViewModel == null)
   5:                  _tripPicViewModel = new TripPictureViewModel();
   6:              this.DataContext = _tripPicViewModel;
   7:              LogRecordHelper.AddLogRecord("MainPage Load After:", DeviceStatus.ApplicationCurrentMemoryUsage.ToString() + " B");
   8:          }

這時我們打開應用.日志會自動記錄當前內存使用情況. 第一次進入應用內存使用情況如下:

wp_ss_20131107_0003

可見當第一次進入應用時. 在MainPage 構造函數並沒有消耗更多的內存.只是在當數據成功載入時Load Before 和Load After變化偏大.在從日志界面backup 到MainPage 反復操作多次發現內存使用記錄如下:

wp_ss_20131107_0005

我們可以很明顯的發現.同樣的數據.在進過頁面跳轉后當前內存逐步增加. 而這個過程中並沒有增加ui數據.這是為何? 針對這個問題 我們在頁面添加MainPage 析構函數.並執行函數時獲取當前內存使用記錄:

   1:   ~MainPage()
   2:     {
   3:         LogRecordHelper.AddLogRecord("MainPage Destructor Excuted:", DeviceStatus.ApplicationCurrentMemoryUsage.ToString() + " B");
   4:     }

我們再重復執行剛才的操作.來看當前頁面析構函數是否執行?.MainPage頁面 是否在退出時正常釋放了該頁面數據內存,經過多次測試發現析構函數成功執行了,並記錄當前當前內存使用記錄:

wp_ss_20131104_0001

雖然析構函數成功執行了.並不是每次離開MainPage頁面才執行的. 首先需要說明析構函數對整個PhoneApplicationPage生命周期的意義.析構函數和構造函數的作用恰恰相反.當對象脫離其作用域時(例如對象所在的函數已調用完畢),系統自動執行析構函數來釋放資源.其實實際的情況是代碼中我們無法控制何時調用析構函數,因為這由垃圾回收器決定。垃圾回收器檢查是否存在應用程序不再使用的對象。如果垃圾回收器認為某個對象符合析構條件,則調用析構函數(如果有的話),回收該對象的內存。程序退出時同樣也會調用析構函數.

回到剛才的操作.雖然我們MainPage頁面成功執行了析構函數. 這里有幾個疑問是? 執行析構函數的並非是在離開MainPage頁面時執行的. 另外一個問題是雖然我們成功執行析構函數但我們發現實際內存使用的情況並沒有減少. 那么如何在采用數據綁定MVVM摸下釋放頁面占用的內存空間?

如果我們離開MainPage時發現析構函數沒有被執行.這時我們就要采取策略. 一般情況下我們采用DataContent=ViewModel進行數據綁定. 在頁面離開因為View的DataContent 屬性於ViewModel之間存在引用關系依賴. 致使View在離開得不到銷毀. 我們可以在OnRemovedFromJournal()方法中剔除掉這份引用關系. 並做強制做GC處理:

   1:      protected override void OnRemovedFromJournal(JournalEntryRemovedEventArgs e)
   2:      {
   3:            this.DataContext = null;
   4:            GC.Collect();
   5:            GC.WaitForPendingFinalizers();
   6:            base.OnRemovedFromJournal(e);
   7:      }

d可以在OnRemovedFromJournal方法中解除View和ViewModel依賴關系.這樣View就能安全的釋放.這里指的一提的是OnRemovedFromJournal方法執行時間點.這個方法事實上是view被彈出棧頂時調用的方法的.也在OnNavigatedFrom方法之后執行該操作. 這樣就足夠嗎? 事實上我們UI上圖片緩存的數據依然還占用着內存空間.也就是在離開頁面之前我們需簡要清除掉圖片緩存數據.

在上文Xaml文件我們ListBox綁定的實體設置Image Source針對的只是一個圖片Url地址 實體定義如下:

   1:    public class TripPictureInfo
   2:      {
   3:          public string CityName { get; set; }
   4:          public string StatusCode { get; set; }
   5:          public string TripPictureUrl { get; set; }
   6:      }

這時我們如果想在離開頁面時情況圖片緩存數據需要改造這個實體 新添加一個 屬性TirpPictureSource:

   1:   public class TripPictureInfo
   2:      {
   3:          public string CityName { get; set; }
   4:          public string StatusCode { get; set; }
   5:          public string TripPictureUrl { get; set; }
   6:   
   7:          private BitmapImage _tirpPictureSource = new BitmapImage() { CreateOptions = BitmapCreateOptions.BackgroundCreation 
   8:                                                                                      | BitmapCreateOptions.IgnoreImageCache 
   9:                                                                                      | BitmapCreateOptions.DelayCreation };
  10:          public BitmapImage TirpPictureSource
  11:          {
  12:              get 
  13:              {
 
  15:                  _tirpPictureSource.UriSource = new Uri(TripPictureUrl,UriKind.RelativeOrAbsolute);
  16:                  return _tirpPictureSource;
  17:              }
  18:          }
  19:      }

並設置該BitmapImage的CreateOptions屬性為BackgroundCreation、IgnoreImageCache 、DelayCreation .在UI上ListBox數據末班中修改Image Source綁定的實體屬性為TirpPictureSource.

   1:        <!--ContentPanel - place additional content here-->
   2:          <Grid x:Name="ContentPanel" Grid.Row="1" Margin="24,0,12,0">         
   3:              <ListBox x:Name="ControlMemory_LB" ItemsSource="{Binding TripPictureCol}" SelectionChanged="ControlMemory_LB_SelectionChanged">  
   4:                  <ListBox.ItemsPanel>
   5:                      <ItemsPanelTemplate>
   6:                          <tool:WrapPanel Orientation="Horizontal"></tool:WrapPanel>
   7:                      </ItemsPanelTemplate>
   8:                  </ListBox.ItemsPanel>
   9:                  <ListBox.ItemTemplate>
  10:                      <DataTemplate>
  11:                          <StackPanel Margin="10,0,0,0">
  12:                              <Image Source="{Binding TirpPictureSource}" Width="100" Height="100"></Image>
  13:                          </StackPanel>
  14:                      </DataTemplate>
  15:                  </ListBox.ItemTemplate>
  16:              </ListBox>
  17:          </Grid>

當頁面離開是在OnNavigatedFrom方法中清除所有的圖片緩存 其實本質其實就是設置BitmapImage 的UrlSource為空:

   1:     private void ClearImageCache()
   2:          {
   3:              if (_tripPicViewModel.TripPictureCol.Count > 0)
   4:              {
   5:                  foreach(TripPictureInfo queryInfo in _tripPicViewModel.TripPictureCol)
   6:                      queryInfo.TirpPictureSource.UriSource=null;
   7:              }
   8:          }

當頁面只有返回上一個頁面才會執行該操作.同時並清空ViewModel中ObserverCollection<T>集合中數據 當頁面離開添加當前內存使用情況的記錄.查看當頁面離開是頁面緩存圖片數據內存是否被回收 代碼如下:

   1:       protected override void OnNavigatedFrom(NavigationEventArgs e)
   2:          {
   3:              LogRecordHelper.AddLogRecord("Clear Before:", DeviceStatus.ApplicationCurrentMemoryUsage.ToString() + " B");
   4:              if (e.NavigationMode == NavigationMode.Back)
   5:              {
   6:                  ClearImageCache();
   7:                  _tripPicViewModel.TripPictureCol.Clear();
   8:              }
   9:              LogRecordHelper.AddLogRecord("Clear After:", DeviceStatus.ApplicationCurrentMemoryUsage.ToString() + " B");
  10:          }

這樣一來我們在進行同樣的操作通過內存使用日志來判斷當前頁面離開是否清空當前圖片緩存,內存日志如下:

wp_ss_20131107_0006

我們可以看到Clear Before 和Clear After之間當前內存使用情況的對比,Clear Before 內存是66637824 [B] Clear After 內存使用是 15417344 [B].來計算當頁面離開時清除圖片緩存數據實際大小為:[(Clear After-Clear Before)/1024/1024]=[(66637824-15417344)/1024/1024]=48.84765625 M. 也即是在頁面離開時清除圖片占用緩存大小為48.8M.

可以看到真正影響每次頁面離開后.內存占用主要問題是緩存圖片資源內存得不到釋放. 我們只需要在頁面退出時清空緩存圖片資源即可達到內存釋放.當疑惑的是發現在頁面離開后析構函數依然沒有執行.通常.NET Framework 垃圾回收器會隱式地管理對象的內存分配和釋放。但當應用程序封裝窗口、文件和網絡連接這類非托管資源時,應使用析構函數釋放這些資源。當對象符合析構時,垃圾回收器將運行對象的 Finalize 方法。雖然垃圾回收器可以跟蹤封裝非托管資源的對象的生存期,但它不了解具體如何清理這些資源。常見的非托管源有:ApplicationContext、Brush、Component、ComponentDesigner、Container、Context、Cursor、FileStream、Font、Icon、Image、Matrix、Object、OdbcDataReader、OleDBDataReader、Pen、Regex、Socket、StreamWriter、Timer、Tooltip 等.

如果你UI使用Brush類似這些非托管資源.會導致每次進入頁面UI會自動重繪.增加了內存的開銷.但目前我們當前並沒有如上這些對象,為何析構函數在退出后一段時間內依然沒有執行?其實問題存在主要因為在Code behind代碼中針對ListBox 添加了一個ControlMemory_LB_SelectionChanged 事件用來查看單張照片:

   1:          private void ControlMemory_LB_SelectionChanged(object sender, SelectionChangedEventArgs e)
   2:          {
   3:              if (e.AddedItems.Count > 0)
   4:              {
   5:                  TripPictureInfo tripPicInfo = this.ControlMemory_LB.SelectedItem as TripPictureInfo;
   6:                  if (tripPicInfo == null)
   7:                      return;
   8:   
   9:                  this.ControlMemory_LB.SelectedIndex = -1;
  10:                  this.NavigationService.Navigate(new Uri("/SinglePictureView.xaml?Url="+tripPicInfo.TripPictureUrl,UriKind.RelativeOrAbsolute));
  11:              }
  12:          }

ListBox對象作為因為該事件存在訂閱的引用關系. 同時ListBox作為Mainpage子對象. 從而導致Mainpage頁面在離開得不到銷毀.所以這里非常值得一提的是.在離開頁面.如果頁面對象訂閱了后台事件.一定取消該事件的訂閱.才能保證View能夠退出時正常的銷毀,添加一個方法來取消ListBox事件訂閱:

   1:       private void ClearRegisterUIElementEvent()
   2:          {
   3:              this.ControlMemory_LB.SelectionChanged -= ControlMemory_LB_SelectionChanged;
   4:          }

同時在OnRemovedFromJournal方法調用清除事件訂閱方法:

   1:          protected override void OnRemovedFromJournal(JournalEntryRemovedEventArgs e)
   2:          {
   3:              ClearRegisterUIElementEvent();
   4:              this.DataContext = null;
   5:              GC.Collect();
   6:              GC.WaitForPendingFinalizers();
   7:              base.OnRemovedFromJournal(e);
   8:          }

這樣一來延續上面重復的操作.來看看內存使用和析構函數的執行情況:

wp_ss_20131107_0008

可以看到當第一次進入頁面離開是成功清除了圖片的緩存.當再次進入MainPage時會立即執行析構函數.這樣一來再次創建新的MainPage對象時內存中原來第一次創建MainPage對象View就會被立即的銷毀.這樣就能及時銷毀原來View. 在測試過程中.發現Page析構函數被調用,但內存並沒有降低. 說明析構函數只是gc回收View,不代表其內部申請的資源都釋放了.所以針對圖片緩存的數據進行單獨的處理,當然如果你的UI比較復雜. 包含一些非托管資源.則需要你在析構函數中手動釋放資源內存的占用.

如果使用IOC+MVVM開發模式.需要在OnRemovedFromJournal函數中添加如下代碼:

   1:  Messenger.Default.Unregister<bool>(this, MessageToken.MessageListChanged); 

Messenger.Default.Unregister<bool>(this, MessageToken.MessageListChanged);這個語句很重要,如果vm在init時在Messenger中注冊了觀察者,系統默認不會將這個vm關聯的view銷毀,所以我們可以在這里對他進行銷毀。當然這樣的Messenger銷毀我們一般也可以直接寫在vm里以保持生命周期的統一性.雖然MVVM模式能夠富文本模式采用DataBinding方式提高我們了開發效率.但同時也增加我們處理windows phone 內存泄露的難度.同時要參考整個PhoneApplicationPage生命周期來合理處理內存釋放.

源碼下載:[https://github.com/chenkai/ReduceMemoryUseageDemo]

Contact Me: [@chenkaihome]

參考資料:

Clear image cache from Grid background  

OutOfMemoryException occure on WriteableBitmap 

BitmapImage.CreateOptions 

How to pass BitmapImage reference to BackgroundWorker.DoWork

Windows Phone 頁面跳轉事件調用順序


免責聲明!

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



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