繁体   English   中英

RenderTargetBitmap GDI在Master-Details视图中处理泄漏

[英]RenderTargetBitmap GDI handle leak in Master-Details view

我有一个带有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它仍然显示了几千个它们尚未收集。

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对象中有一些空闲处理需要几秒钟的时间,然后是另一个更改以释放它的所有句柄)

我认为没有任何简单的解决方法 - 我已经尝试添加一个睡眠来减慢运动速度,甚至调用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)

如果你克隆到一个更简单的位图类型(并冻结)它将不会消耗尽可能多的gdi句柄,但它会慢一些。 如何在WPF中实现Image.Clone()的答案中,通过序列化进行了克隆?“

暂无
暂无

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM