代码改变世界

Windows phone应用开发[21]-图片性能优化

2013-11-07 16:53 chenkai 阅读(...) 评论(...) 编辑 收藏

在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 页面跳转事件调用顺序

无觅相关文章插件,快速提升流量