C# 解决WPF问题的任何方法';除了反射之外,GC.Collect(2)的调用是什么?

C# 解决WPF问题的任何方法';除了反射之外,GC.Collect(2)的调用是什么?,c#,wpf,reflection,garbage-collection,C#,Wpf,Reflection,Garbage Collection,我最近不得不在生产代码中签入这个怪物来操作WPF类中的私有字段:(tl;dr我如何避免这样做?) 要理解我为什么会做出如此疯狂的事情,你需要看看: 是的,这实际上是WPF强制执行gen 2垃圾收集,强制执行完全阻塞GC。在不阻塞gen 2堆的情况下发生。实际上,这意味着无论何时调用此方法,我们的整个应用程序都会被锁定。应用程序使用的内存越多,第2代堆越分散,所需时间就越长。目前,我们的应用程序缓存了相当多的数据,可以轻松占用一兆内存,强制GC可以将我们的应用程序锁定在慢速设备上几秒钟——每850

我最近不得不在生产代码中签入这个怪物来操作WPF类中的私有字段:(tl;dr我如何避免这样做?)

要理解我为什么会做出如此疯狂的事情,你需要看看:

是的,这实际上是WPF强制执行gen 2垃圾收集,强制执行完全阻塞GC。在不阻塞gen 2堆的情况下发生。实际上,这意味着无论何时调用此方法,我们的整个应用程序都会被锁定。应用程序使用的内存越多,第2代堆越分散,所需时间就越长。目前,我们的应用程序缓存了相当多的数据,可以轻松占用一兆内存,强制GC可以将我们的应用程序锁定在慢速设备上几秒钟——每850毫秒一次

因为尽管作者提出了相反的主张,但很容易出现这样一种情况,即这种方法被频繁调用。WPF的内存代码发生在从文件加载时。我们使用数千个项目,其中每个项目都由存储在磁盘上的缩略图表示。当我们向下滚动时,我们正在动态加载这些缩略图,并且GC正在以最大频率发生。因此,随着应用程序不断锁定,滚动变得难以置信的缓慢和起伏

在我上面提到的可怕的反射黑客中,我们强制计时器永远不会被满足,因此WPF永远不会强制GC。此外,似乎没有任何不利后果——内存随着滚动而增长,最终在不锁定主线程的情况下自然触发GC


有没有其他选项可以阻止那些对
GC.Collect(2)
的调用,这些调用不像我的解决方案那样明显可怕?我很想得到一个解释,说明到底是什么具体的问题,可能会产生后续通过这个黑客。我的意思是避免调用
GC.Collect(2)
的问题。(在我看来,自然发生的GC应该足够了)

我认为你所拥有的一切都很好。做得好,很好的黑客,反射是一个很棒的工具来修复不可靠的框架代码。我自己也用过很多次。只需将它的使用限制在显示ListView的视图中,让它一直处于活动状态太危险了

骇人听闻的ProcessAdd()黑客对潜在问题进行了一点探讨,当然是非常粗糙的。这是BitmapSource未实现IDisposable的结果。一个有问题的设计决策,因此充满了关于它的问题。然而,几乎所有这些都是关于相反的问题,这个计时器不够快,无法跟上。它就是不太管用

您无法更改此代码的工作方式。它处理的值是常量声明。根据15年前可能适用的值,此代码的可能年龄。它从1兆字节开始,调用“10兆字节”这是一个问题,当时的生活更简单:)他们忘了写它,所以它可以正常伸缩,GC.AddMemoryPressure()今天可能就可以了。太少太晚了,他们无法在不大幅改变程序行为的情况下修复此问题

你当然可以击败计时器,避免黑客攻击。当然,现在的问题是,它的间隔与用户在不阅读任何内容而只是试图查找感兴趣的记录时滚动列表视图的速度大致相同。这是一个UI设计问题,在具有数千行的列表视图中非常常见,您可能不想解决这个问题。您需要做的是缓存缩略图,收集下一步可能需要的缩略图。最好的方法是在线程池线程中这样做。测量时间在执行此操作时,最多可以花费850毫秒。但是,该代码不会比您现在拥有的代码更小,也不会比现在漂亮很多

注意:只有在应用程序出现瓶颈时才这样做,并确保您了解其后果-请参阅,以了解他们为什么将此放在WPF的首位

你有一些讨厌的代码试图修复框架中的一个讨厌的黑客。。。由于它都是静态的,并且在WPF中从多个位置调用,所以使用反射来打破它(其他解决方案可能是这样的)是再好不过的了

因此,不要指望有一个干净的解决方案。除非他们更改WPF代码,否则不存在这样的东西

但是我认为你的破解可以更简单,避免使用计时器:只要破解
\u totalMemory
值,你就完成了。它是一个
长的
,这意味着它可以变成负值。在这一点上有很大的负值

private static class MemoryPressurePatcher
{
    public static void Patch()
    {
        var memoryPressureType = typeof(Duration).Assembly.GetType("MS.Internal.MemoryPressure");
        var totalMemoryField = memoryPressureType?.GetField("_totalMemory", BindingFlags.Static | BindingFlags.NonPublic);

        if (totalMemoryField?.FieldType != typeof(long))
            return;

        var currentValue = (long) totalMemoryField.GetValue(null);

        if (currentValue >= 0)
            totalMemoryField.SetValue(null, currentValue + long.MinValue);
    }
}
在这里,现在你的应用程序在调用
GC.Collect
之前必须分配大约8 EB。不用说,如果发生这种情况,你将有更大的问题要解决。:)

如果您担心下溢的可能性,只需使用
long.MinValue/2
作为偏移量。这仍然会给您留下4个EB

请注意,
AddToTotal
实际上执行
\u totalMemory
的边界检查,但它通过
调试来执行此操作。Assert

由于您将使用.NET Framework的发布版本,这些断言将被禁用(使用
条件属性
),因此无需担心这一点


您已经问过这种方法可能会出现什么问题。让我们看一下。

  • 最明显的一点是:微软改变了你试图破解的WPF代码

    那么,在这种情况下,这在很大程度上取决于变化的性质

    • 他们更改类型名称/字段名称/字段类型:在这种情况下,将不会执行黑客攻击,您将返回到股票行为。反射代码是相当防御的,它不会抛出异常,它只是什么都不会做

    • 它们将
      Debug.Assert
      调用更改为启用的运行时检查
      /// <summary>
      /// Check the timers and decide if enough time has elapsed to
      /// force a collection
      /// </summary>
      private static void ProcessAdd()
      {
          bool shouldCollect = false;
      
          if (_totalMemory >= INITIAL_THRESHOLD)
          {
              // need to synchronize access to the timers, both for the integrity
              // of the elapsed time and to ensure they are reset and started
              // properly
              lock (lockObj)
              {
                  // if it's been long enough since the last allocation
                  // or too long since the last forced collection, collect
                  if (_allocationTimer.ElapsedMilliseconds >= INTER_ALLOCATION_THRESHOLD
                      || (_collectionTimer.ElapsedMilliseconds > MAX_TIME_BETWEEN_COLLECTIONS))
                  {
                      _collectionTimer.Reset();
                      _collectionTimer.Start();
      
                      shouldCollect = true;
                  }
                  _allocationTimer.Reset();
                  _allocationTimer.Start();
              }
      
              // now that we're out of the lock do the collection
              if (shouldCollect)
              {
                  Collect();
              }
          }
      
          return;
      }
      
      private static void Collect()
      {
          // for now only force Gen 2 GCs to ensure we clean up memory
          // These will be forced infrequently and the memory we're tracking
          // is very long lived so it's ok
          GC.Collect(2);
      }
      
      private static class MemoryPressurePatcher
      {
          public static void Patch()
          {
              var memoryPressureType = typeof(Duration).Assembly.GetType("MS.Internal.MemoryPressure");
              var totalMemoryField = memoryPressureType?.GetField("_totalMemory", BindingFlags.Static | BindingFlags.NonPublic);
      
              if (totalMemoryField?.FieldType != typeof(long))
                  return;
      
              var currentValue = (long) totalMemoryField.GetValue(null);
      
              if (currentValue >= 0)
                  totalMemoryField.SetValue(null, currentValue + long.MinValue);
          }
      }
      
      Debug.Assert(newValue >= 0);
      
      using System;
      using System.Collections.Concurrent;
      using System.Collections.Generic;
      using System.Collections.ObjectModel;
      using System.Threading;
      using System.Windows;
      using System.Windows.Controls;
      using System.Windows.Documents;
      using System.Windows.Media;
      using System.Windows.Media.Imaging;
      using System.Windows.Threading;
      
      namespace VirtualizedListView
      {
          public partial class MainWindow : Window
          {
              private const string ThumbnailDirectory = @"D:\temp\thumbnails";
      
              private ConcurrentQueue<WriteableBitmap> _writeableBitmapCache = new ConcurrentQueue<WriteableBitmap>();
      
              public MainWindow()
              {
                  InitializeComponent();
                  DataContext = this;
      
                  // Load thumbnail file names
                  List<string> fileList = new List<string>(System.IO.Directory.GetFiles(ThumbnailDirectory));
      
                  // Load view-model
                  Thumbnails = new ObservableCollection<Thumbnail>();
                  foreach (string file in fileList)
                      Thumbnails.Add(new Thumbnail(GetImageForThumbnail) { FilePath = file });
      
                  // Create cache of pre-built WriteableBitmap objects; note that this assumes that all thumbnails
                  // will be the exact same size.  This will need to be tuned for your needs
                  for (int i = 0; i <= 99; ++i)
                      _writeableBitmapCache.Enqueue(new WriteableBitmap(256, 256, 96d, 96d, PixelFormats.Bgr32, null));
              }
      
              public ObservableCollection<Thumbnail> Thumbnails
              {
                  get { return (ObservableCollection<Thumbnail>)GetValue(ThumbnailsProperty); }
                  set { SetValue(ThumbnailsProperty, value); }
              }
              public static readonly DependencyProperty ThumbnailsProperty =
                  DependencyProperty.Register("Thumbnails", typeof(ObservableCollection<Thumbnail>), typeof(MainWindow));
      
              private BitmapSource GetImageForThumbnail(Thumbnail thumbnail)
              {
                  // Get the thumbnail data via the proxy in the other app domain
                  ImageLoaderProxyPixelData pixelData = GetBitmapImageBytes(thumbnail.FilePath);
                  WriteableBitmap writeableBitmap;
      
                  // Get a pre-built WriteableBitmap out of the cache then overwrite its pixels with the current thumbnail information.
                  // This avoids the memory pressure being set in this app domain, keeping that in the app domain of the proxy.
                  while (!_writeableBitmapCache.TryDequeue(out writeableBitmap)) { Thread.Sleep(1); }
                  writeableBitmap.WritePixels(pixelData.Rect, pixelData.Pixels, pixelData.Stride, 0);
      
                  return writeableBitmap;
              }
      
              private ImageLoaderProxyPixelData GetBitmapImageBytes(string fileName)
              {
                  // All of the BitmapSource creation occurs in this method, keeping the calls to 
                  // MemoryPressure.ProcessAdd() localized to this app domain
      
                  // Load the image from file
                  BitmapFrame bmpFrame = BitmapFrame.Create(new Uri(fileName));
                  int stride = bmpFrame.PixelWidth * bmpFrame.Format.BitsPerPixel;
                  byte[] pixels = new byte[bmpFrame.PixelHeight * stride];
      
                  // Construct and return the image information
                  bmpFrame.CopyPixels(pixels, stride, 0);
                  return new ImageLoaderProxyPixelData()
                  {
                      Pixels = pixels,
                      Stride = stride,
                      Rect = new Int32Rect(0, 0, bmpFrame.PixelWidth, bmpFrame.PixelHeight)
                  };
              }
      
              public void VirtualizingStackPanel_CleanUpVirtualizedItem(object sender, CleanUpVirtualizedItemEventArgs e)
              {
                  // Get a reference to the WriteableBitmap before nullifying the property to release the reference
                  Thumbnail thumbnail = (Thumbnail)e.Value;
                  WriteableBitmap thumbnailImage = (WriteableBitmap)thumbnail.Image;
                  thumbnail.Image = null;
      
                  // Asynchronously add the WriteableBitmap back to the cache
                  Dispatcher.BeginInvoke((Action)(() =>
                  {
                      _writeableBitmapCache.Enqueue(thumbnailImage);
                  }), System.Windows.Threading.DispatcherPriority.Loaded);
              }
          }
      
          // View-Model
          public class Thumbnail : DependencyObject
          {
              private Func<Thumbnail, BitmapSource> _imageGetter;
              private BitmapSource _image;
      
              public Thumbnail(Func<Thumbnail, BitmapSource> imageGetter)
              {
                  _imageGetter = imageGetter;
              }
      
              public string FilePath
              {
                  get { return (string)GetValue(FilePathProperty); }
                  set { SetValue(FilePathProperty, value); }
              }
              public static readonly DependencyProperty FilePathProperty =
                  DependencyProperty.Register("FilePath", typeof(string), typeof(Thumbnail));
      
              public BitmapSource Image
              {
                  get
                  {
                      if (_image== null)
                          _image = _imageGetter(this);
                      return _image;
                  }
                  set { _image = value; }
              }
          }
      
          public class ImageLoaderProxyPixelData
          {
              public byte[] Pixels { get; set; }
              public Int32Rect Rect { get; set; }
              public int Stride { get; set; }
          }
      }
      
      typeof(BitmapImage).Assembly
        .GetType("MS.Internal.MemoryPressure")
        .GetField("_totalMemory", BindingFlags.NonPublic | BindingFlags.Static)
        .SetValue(null, Int64.MinValue / 2); 
      
      /// Avalon currently only tracks unmanaged memory pressure related to Images.  
      /// The implementation of this class exploits this by using a timer-based
      /// tracking scheme. It assumes that the unmanaged memory it is tracking
      /// is allocated in batches, held onto for a long time, and released all at once
      /// We have profiled a variety of scenarios and found images do work this way
      
      internal SafeMILHandleMemoryPressure(long gcPressure)
      {
          this._gcPressure = gcPressure;
          this._refCount = 0;
          GC.AddMemoryPressure(this._gcPressure);
      }
      
      internal SafeMILHandleMemoryPressure(long gcPressure)
      {
          this._gcPressure = gcPressure;
          this._refCount = 0;
          if (this._gcPressure > 8192L)
          {
              MemoryPressure.Add(this._gcPressure);   // Kills UI interactivity !!!!!
              return;
          }
          GC.AddMemoryPressure(this._gcPressure);
      }