Master-Details视图中的RenderTargetBitmap GDI句柄泄漏问题

12

我有一个带有主从视图的应用程序。当您从“主”列表中选择项目时,它会通过RenderTargetBitmap创建一些图像填充“详细信息”区域。

每次我从列表中选择不同的主项目时,我的应用程序使用的GDI句柄数量(在Process Explorer中报告)都会增加,并最终在使用了10,000个GDI句柄后崩溃(或有时锁定)。

我不知道如何修复这个问题,因此非常感谢任何关于我做错了什么的建议(或只是关于如何获取更多信息的建议)。

我已将我的应用程序简化到以下新WPF应用程序(.NET 4.0)中,名为“DoesThisLeak”:

在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>

如果您点击列表中的第一项,然后按住向下箭头键,就可以重现这个问题。

通过使用WinDbg和SOS中的!gcroot查看,我找不到任何东西使这些RenderTargetBitmap对象保持活动状态,但是如果我执行!dumpheap -type System.Windows.Media.Imaging.RenderTargetBitmap,它仍然显示了数千个未被收集的对象。

3个回答

8
TL;DR: 已修复。请看结尾。继续阅读我的发现之旅和我走过的所有错误方向!
我对此进行了一些探索,我认为它并不是泄漏。如果我在Images循环的两边加上以下代码来加强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对象中有一些空闲处理需要几秒钟的空闲时间,然后进行另一个更改以释放所有它的句柄)。
我认为没有任何简单的解决办法——我尝试添加睡眠来减慢移动速度,甚至调用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);
        }
    }
}

据我所见,这给运行时提供的主要内容是项目数(显然可枚举对象没有),这意味着它既不必多次枚举,也不必猜测!即使有1000个MasterItems,我也可以在光标键上跑来跑去而不会导致10k手柄错误,所以对我来说看起来很好。(我的代码也没有显式GC)


注意,我尝试过对ObservableCollection进行缓存。不幸的是,持有该集合似乎最终也会持有GDI句柄。 - James Ogden
谢谢,太好了。这确实修复了示例应用程序的问题,现在我只需要尝试将其适配到真实应用程序中。但我不确定为什么ObservableCollection在这里有帮助。如果仅仅是因为大小的原因,那么List<T>应该具有相同的效果。 - Wilka

2

如果您克隆到一个更简单的位图类型(并冻结),它将不会使用太多gdi句柄,但速度会变慢。 在如何在WPF中实现Image.Clone()? 的答案中有通过序列化进行克隆的方法。


WriteableBitmap有一个接受BitmapSource的构造函数,因此将其克隆到其中会更快,并且还可以解决这个问题。谢谢。 - Wilka

0

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接