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

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

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

2013-11-06_173202

固然咱們也能夠經過獲取DeviceExtendedProperties.GetValue(String) 方法檢查 ApplicationWorkingSetLimit 值,藉此檢查應用可用的內存峯值.git

而相對Windows phone 7版本達到峯值後自動退出的作法. WP SDK 8.0 作了更多可選項. 而不是簡單粗暴採用直接退出的方式.爲了保證咱們應用可以覆蓋更多wp終端.對於低端配置的的手機.能夠請求更多的內存或是徹底放棄在低端適配.但願應用對全部手機均可用(但這同時可能會因佔用更多內存而影響其餘手機任務),必須將 FunctionalCapability 條目添加到應用清單文件。要退出 低內存設備,必須將 Requirements 條目添加到清單.github

2013-11-06_180318

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

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

首先拋開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 頁面跳轉事件調用順序

相關文章
相關標籤/搜索