我最近不得不将这个 怪物 checkin 生产代码以操作 WPF 类中的私有(private)字段:(tl;dr 我如何避免必须这样做?)
private static class MemoryPressurePatcher
{
private static Timer gcResetTimer;
private static Stopwatch collectionTimer;
private static Stopwatch allocationTimer;
private static object lockObject;
public static void Patch()
{
Type memoryPressureType = typeof(Duration).Assembly.GetType("MS.Internal.MemoryPressure");
if (memoryPressureType != null)
{
collectionTimer = memoryPressureType.GetField("_collectionTimer", BindingFlags.Static | BindingFlags.NonPublic)?.GetValue(null) as Stopwatch;
allocationTimer = memoryPressureType.GetField("_allocationTimer", BindingFlags.Static | BindingFlags.NonPublic)?.GetValue(null) as Stopwatch;
lockObject = memoryPressureType.GetField("lockObj", BindingFlags.Static | BindingFlags.NonPublic)?.GetValue(null);
if (collectionTimer != null && allocationTimer != null && lockObject != null)
{
gcResetTimer = new Timer(ResetTimer);
gcResetTimer.Change(TimeSpan.Zero, TimeSpan.FromMilliseconds(500));
}
}
}
private static void ResetTimer(object o)
{
lock (lockObject)
{
collectionTimer.Reset();
allocationTimer.Reset();
}
}
}
要理解为什么我会做如此疯狂的事情,您需要查看
MS.Internal.MemoryPressure.ProcessAdd()
:/// <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;
}
重要的一点是接近尾声,它调用方法
Collect()
: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);
}
是的,这实际上是 WPF 强制执行第 2 代垃圾收集,从而强制执行完全阻塞的 GC。 A naturally occurring GC 发生时不会阻塞在第 2 代堆上。这在实践中意味着每当调用此方法时,我们的整个应用程序都会锁定。您的应用程序使用的内存越多,第 2 代堆的碎片越多,所需的时间就越长。我们的应用程序目前缓存了相当多的数据,很容易占用大量内存,强制 GC 可以将我们的应用程序锁定在慢速设备上几秒钟——每 850 毫秒。
因为尽管作者提出了相反的抗议,但很容易得出这种方法被频繁调用的场景。当从文件加载
BitmapSource
时,会出现 WPF 的此内存代码。我们 virtualize a listview 有数千个项目,其中每个项目都由存储在磁盘上的缩略图表示。当我们向下滚动时,我们正在动态加载这些缩略图,并且 GC 以最大频率发生。因此,随着应用程序不断锁定,滚动变得令人难以置信的缓慢和断断续续。通过我在上面提到的那个可怕的反射 hack,我们强制永远不会满足计时器,因此 WPF 从不强制 GC。此外,似乎没有不利的后果——内存随着滚动而增长,最终自然触发 GC 而不锁定主线程。
是否有任何其他选项可以阻止对
GC.Collect(2)
的调用不像我的解决方案那么公然可怕?很想得到一个解释,说明跟进这个黑客可能会出现什么具体问题。我的意思是避免调用 GC.Collect(2)
的问题。 (在我看来,自然发生的 GC 应该就足够了)
最佳答案
Notice: Do this only if it causes a bottleneck in your app, and make sure you understand the consequences - See Hans's answer for a good explanation on why they put this in WPF in the first place.
你有一些讨厌的代码试图修复框架中的一个讨厌的黑客......因为它都是静态的并且从 WPF 中的多个地方调用,你真的不能比使用反射来破坏它做得更好(其他解决方案是 much worse ) .
所以不要指望那里有一个干净的解决方案。除非他们更改 WPF 代码,否则不存在这样的事情。
但我认为你的黑客可以更简单,避免使用计时器:只需破解
_totalMemory
值,你就完成了。它是 long
,这意味着它可以变为负值。并且非常大的负值。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 艾字节。不用说,如果发生这种情况,您将有更大的问题需要解决。 :)如果您担心下溢的可能性,只需使用
long.MinValue / 2
作为偏移量。这仍然给您留下 4 艾字节。请注意,
AddToTotal
实际上执行 _totalMemory
的边界检查,但它使用 Debug.Assert
here 执行此操作:Debug.Assert(newValue >= 0);
由于您将使用 .NET Framework 的发布版本,这些断言将被禁用(使用
ConditionalAttribute
),因此无需担心。您已经问过这种方法可能会出现什么问题。让我们来看看。
那么,在这种情况下,这在很大程度上取决于变化的性质。
Debug.Assert
调用更改为在发布版本中启用的运行时检查。在这种情况下,您的应用程序注定要失败。任何从磁盘加载图像的尝试都会失败。哎呀。由于他们自己的代码几乎是黑客行为,因此可以减轻这种风险。他们不打算扔它,它应该被忽视。他们希望它安静地坐着,默默地失败。让图像加载是一个更重要的特性,它不应该被一些内存管理代码削弱,这些代码的唯一目的是将内存使用量保持在最低限度。
GC.Collect
调用。在这种情况下,明显的风险是内存使用量增加。由于收集的频率较低,因此在给定时间将分配更多内存。这应该不是什么大问题,因为当第 0 代填满时,收集仍然会自然发生。
您还会有更多的内存碎片,这是较少集合的直接结果。这对您来说可能是也可能不是问题 - 因此请配置您的应用程序。
更少的集合也意味着更少的对象被提升到更高的代。这是一件好事。理想情况下,您应该在第 0 代中有短期对象,在第 2 代中有长期对象。频繁的收集实际上会导致短期对象被提升到第 1 代,然后被提升到第 2 代,你最终会得到在第 2 代中有许多无法访问的对象。这些对象只会被第 2 代收集清理,会导致堆碎片,并且实际上会增加 GC 时间,因为它必须花费更多时间来压缩堆。这实际上是为什么自己调用
GC.Collect
被认为是一种不好的做法的主要原因 - 您正在积极地击败 GC 策略,这会影响整个应用程序。 在任何情况下,正确的方法是加载图像,缩小它们并在您的 UI 中显示这些缩略图。所有这些处理都应该在后台线程中完成。对于 JPEG 图像,加载嵌入的缩略图 - 它们可能已经足够了。并使用对象池,这样你就不需要每次都实例化新的位图,这完全绕过了
MemoryPressure
类问题。是的,这正是其他答案所暗示的;)
关于c# - 除了反射之外,还有什么方法可以解决 WPF 对 GC.Collect(2) 的调用?,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/36044796/