我有一个带有Master-Details视图的应用程序。当您从“主”列表中选择一个项目时,它会使用一些图像(通过RenderTargetBitmap创建)填充“详细信息”区域。
每次我从列表中选择一个不同的主项目时,我的应用程序使用的GDI句柄数量(在Process Explorer中报告)会上升 - 最终会在10,000个GDI句柄中翻倒(或有时会锁定)使用
我对如何解决这个问题感到茫然,所以对于我做错了什么的建议(或者只是关于如何获取更多信息的建议)将不胜感激。
我在一个名为“DoesThisLeak”的新WPF应用程序(.NET 4.0)中将我的应用程序简化为以下内容:
在MainWindow.xaml.cs
中public partial class MainWindow : Window
{
public MainWindow()
{
ViewModel = new MasterViewModel();
InitializeComponent();
}
public MasterViewModel ViewModel { get; set; }
}
public class MasterViewModel : INotifyPropertyChanged
{
private MasterItem selectedMasterItem;
public IEnumerable<MasterItem> MasterItems
{
get
{
for (int i = 0; i < 100; i++)
{
yield return new MasterItem(i);
}
}
}
public MasterItem SelectedMasterItem
{
get { return selectedMasterItem; }
set
{
if (selectedMasterItem != value)
{
selectedMasterItem = value;
if (PropertyChanged != null)
{
PropertyChanged(this, new PropertyChangedEventArgs("SelectedMasterItem"));
}
}
}
}
public event PropertyChangedEventHandler PropertyChanged;
}
public class MasterItem
{
private readonly int seed;
public MasterItem(int seed)
{
this.seed = seed;
}
public IEnumerable<ImageSource> Images
{
get
{
GC.Collect(); // Make sure it's not the lack of collections causing the problem
var random = new Random(seed);
for (int i = 0; i < 150; i++)
{
yield return MakeImage(random);
}
}
}
private ImageSource MakeImage(Random random)
{
const int size = 180;
var drawingVisual = new DrawingVisual();
using (DrawingContext drawingContext = drawingVisual.RenderOpen())
{
drawingContext.DrawRectangle(Brushes.Red, null, new Rect(random.NextDouble() * size, random.NextDouble() * size, random.NextDouble() * size, random.NextDouble() * size));
}
var bitmap = new RenderTargetBitmap(size, size, 96, 96, PixelFormats.Pbgra32);
bitmap.Render(drawingVisual);
bitmap.Freeze();
return bitmap;
}
}
在MainWindow.xaml
<Window x:Class="DoesThisLeak.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Title="MainWindow" Height="900" Width="1100"
x:Name="self">
<Grid DataContext="{Binding ElementName=self, Path=ViewModel}">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="210"/>
<ColumnDefinition Width="*"/>
<ColumnDefinition/>
</Grid.ColumnDefinitions>
<ListBox Grid.Column="0" ItemsSource="{Binding MasterItems}" SelectedItem="{Binding SelectedMasterItem}"/>
<ItemsControl Grid.Column="1" ItemsSource="{Binding Path=SelectedMasterItem.Images}">
<ItemsControl.ItemTemplate>
<DataTemplate>
<Image Source="{Binding}"/>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
</Grid>
</Window>
如果单击列表中的第一项,然后按住向下光标键,则可以重现该问题。
通过使用SOS查看WinDbg中的gcroot,我无法找到保持这些RenderTargetBitmap对象活着的任何内容,但是如果我!dumpheap -type System.Windows.Media.Imaging.RenderTargetBitmap
,它仍会显示尚未收集的数千个。
答案 0 :(得分:7)
TL; DR:已修复。见底部。继续阅读我的发现之旅以及我失败的所有错误的小巷!
我已经做了一些探讨,我不认为它是这样泄漏的。如果我通过将循环的任一侧放在图像中来加强GC:
GC.Collect();
GC.WaitForPendingFinalizers();
GC.Collect();
您可以逐步(慢慢地)向下列表,并在几秒钟后看到GDI句柄没有变化。 实际上,使用MemoryProfiler进行检查确认了这一点 - 当从一个项目到另一个项目缓慢移动时,没有.net或GDI对象泄漏。
你很快就会在列表中快速移动 - 我看到进程内存超过1.5G,GDI对象在碰到墙壁时爬升到10000。每次在此之后调用MakeImage时,都会抛出COM错误,并且无法为该过程执行任何有用的操作:
A first chance exception of type 'System.Runtime.InteropServices.COMException' occurred in PresentationCore.dll
A first chance exception of type 'System.Runtime.InteropServices.COMException' occurred in PresentationCore.dll
A first chance exception of type 'System.Reflection.TargetInvocationException' occurred in mscorlib.dll
System.Windows.Data Error: 8 : Cannot save value from target back to source. BindingExpression:Path=SelectedMasterItem; DataItem='MasterViewModel' (HashCode=28657291); target element is 'ListBox' (Name=''); target property is 'SelectedItem' (type 'Object') COMException:'System.Runtime.InteropServices.COMException (0x88980003): Exception from HRESULT: 0x88980003
at System.Windows.Media.Imaging.RenderTargetBitmap.FinalizeCreation()
这个,我想解释为什么你会看到这么多RenderTargetBitmaps。它还向我建议了一个缓解策略 - 假设它是一个框架/ GDI错误。尝试将渲染代码(RenderImage)推送到一个允许重新启动底层COM组件的域中。最初,我在其自己的公寓(SetApartmentState(ApartmentState.STA))中尝试一个帖子,如果这不起作用,我会尝试使用AppDomain。
然而,更容易尝试处理问题的根源,即如此快速地分配如此多的图像,因为即使我将它达到9000 GDI句柄并等待一下,计数也是如此在下一次更改之后直接回落到基线(在我看来,在COM对象中有一些空闲处理,需要几秒钟没有任何东西,然后另一个更改以释放所有这些;&#39; s处理)
我认为没有任何简单的解决方法 - 我已尝试添加睡眠以减慢运动速度,甚至调用ComponentDispatched.RaiseIdle() - 这些都没有任何效果。如果我必须以这种方式工作,我会尝试以可重启的方式运行GDI处理(并处理可能发生的错误)或更改UI。
根据详细视图中的要求,最重要的是,右侧图像的可见性和大小,您可以利用ItemsControl的功能来虚拟化列表(但您可能必须在至少定义包含图像的高度和数量,以便它可以正确管理滚动条)。我建议返回一个ObservableCollection图像,而不是IEnumerable。
事实上,刚刚测试过,这段代码似乎会让问题消失:
public ObservableCollection<ImageSource> Images
{
get
{
return new ObservableCollection<ImageSource>(ImageSources);
}
}
IEnumerable<ImageSource> ImageSources
{
get
{
var random = new Random(seed);
for (int i = 0; i < 150; i++)
{
yield return MakeImage(random);
}
}
}
根据我的意见,这给运行时的主要内容是项目的数量(可枚举的,显然不是),这意味着它既不需要多次枚举,也不能猜测(!)。我可以用手指在光标键上向上和向下跑,而不用吹10k手柄,即使有1000个MasterItems,所以它看起来不错。 (我的代码也没有明确的GC)
答案 1 :(得分:2)
如果你克隆到一个更简单的位图类型(并冻结),它将不会消耗尽可能多的gdi句柄,但速度较慢。 在How achieve Image.Clone() in WPF?"
的答案中通过序列化进行克隆答案 2 :(得分:0)
尝试使用此处描述的解决方案: RenderTargetBitmap.Render() throws OutOfMemoryException when rendering large visuals.
<强>更新强> 另外,请查看RenderTargetBitmap Memory Leak。