RenderTargetBitmap GDI句柄在“主详细信息”视图中泄漏

本文关键字:主详细信息 视图 泄漏 详细信息 GDI 句柄 RenderTargetBitmap | 更新日期: 2023-09-27 18:21:32

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

每次我从列表中选择不同的主项时,我的应用程序正在使用的GDI句柄的数量(如Process Explorer中所报告的)都会增加,最终会下降(有时会锁定)到10000个正在使用的GDI句柄。

我不知道如何解决这个问题,所以任何关于我做错了什么的建议(或者只是关于如何获得更多信息的建议)都将不胜感激。

我在一个名为"DoesThisLeak"的新WPF应用程序(.NET 4.0)中将我的应用程序简化为以下内容:

在主窗口.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;
    }
}

在主窗口.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,它仍然显示数千个尚未收集的对象。

RenderTargetBitmap GDI句柄在“主详细信息”视图中泄漏

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()

我想这就解释了为什么你会看到这么多RenderTargetBitmap。它还向我建议了一个缓解策略——假设它是一个框架/GDI错误。尝试将呈现代码(RenderImage)推送到允许重新启动底层COM组件的域中。最初,我会在它自己的单元中尝试一个线程(SetApartmentState(ApartmentState.STA)),如果不起作用,我会尝试AppDomain。

然而,尝试处理问题的根源会更容易,因为分配这么多图像的速度太快了,因为即使我得到了多达9000个GDI句柄并等待一段时间,在下一次更改后,计数正好回落到基线(在我看来,COM对象中有一些空闲处理,需要几秒钟的时间,然后再进行一次更改以释放它的所有句柄)

我不认为有任何简单的解决方法——我尝试添加睡眠来减缓移动,甚至调用ComponentDispatched.RiseIdle()——这两种方法都没有任何效果。如果必须以这种方式工作,我将尝试以可重启的方式运行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)

如果克隆到更简单的位图类型(并冻结),它不会占用那么多gdi句柄,但速度较慢。在如何实现镜像的答案中,有通过序列化进行克隆。克隆 );在WPF中?"

尝试使用此处描述的解决方案:渲染大型视觉效果时,RenderTargetBitmap。Render()引发OutOfMemoryException。

更新:另外,请查看RenderTargetBitmap内存泄漏。