主细节视图中的 RenderTargetBitmap GDI 句柄泄漏

发布于 2024-12-29 11:17:09 字数 3721 浏览 4 评论 0原文

我有一个带有主详细信息视图的应用程序。当您从“主”列表中选择一个项目时,它会使用一些图像(通过 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 它仍然显示其中几千件尚未收集。

I have an app with a Master-Details view. When you select an item from the 'master' list, it populates the 'details' area with some images (created via RenderTargetBitmap).

Each time I select a different master item from the list, the number of GDI handles in use by my app (as reported in Process Explorer) goes up - and eventually falls over (or sometimes locks up) at 10,000 GDI handles in use.

I'm at a loss on how to fix this, so any suggestions on what I'm doing wrong (or just suggestions on how to get more information) would be greatly appreciated.

I've simplified my app down to the following in a new WPF Application (.NET 4.0) called "DoesThisLeak":

In 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;
    }
}

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

You can reproduce the problem if you click on the first item in the list, then hold down the Down cursor key.

From looking at !gcroot in WinDbg with SOS, I can't find anything keeping those RenderTargetBitmap objects alive, but if I do !dumpheap -type System.Windows.Media.Imaging.RenderTargetBitmap it still shows a few thousand of them that haven't been collected yet.

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。

评论(3

浊酒尽余欢 2025-01-05 11:17:09

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);
        }
    }
}

据我所知,运行时的主要内容是项目的数量(显然,可枚举的不是),这意味着它既不必多次枚举,也不必猜测(!)。即使有 1000 个 MasterItem,我也可以用手指在光标键上在列表中上下移动,而无需吹响 10k 句柄,所以它对我来说看起来不错。 (我的代码也没有显式GC)

TL;DR: fixed. See the bottom. Read on for my journey of discovery and all the wrong alleys I went down!

I've done some poking around with this, and I don't think it's leaking as such. If I beef up the GC by putting this either side of the loop in Images:

GC.Collect();
GC.WaitForPendingFinalizers();
GC.Collect();

You can step (slowly) down the list and see no change in the GDI handles after a few seconds.
Indeed, checking with MemoryProfiler confirms this - no .net or GDI objects leak when moving slowly from item to item.

You do get into trouble moving quickly down the list - I saw process memory heading past 1.5G and the GDI object climbing to 10000 when it hit a wall. Every time MakeImage was called after that, a COM error was thrown and nothing useful could be done for the process:

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

This, I think explains why you see so many RenderTargetBitmaps hanging around. It also suggests to me a mitigation strategy - assuming it's a framework/GDI bug. Try to push the render code (RenderImage) into a domain which will allow the underlying COM component to be restarted. Initially, I'd try a thread in it's own apartment (SetApartmentState(ApartmentState.STA)) and if that didn't work, I'd try an AppDomain.

However, it'd be easier to try to deal with the source of the problem, which is allocating so many images so quickly, because even if I get it up to 9000 GDI handles and wait a bit, the count falls right back down to the baseline after the next change (it seems to me as there's some idle processing in the COM object which needs a few seconds of nothing, and then another change to release all of it's handles)

I don't think there are any easy fixes for this - I've tried adding a sleep to slow the movement down, and even calling ComponentDispatched.RaiseIdle() - neither of these have any effect. If I had to make it work this way, I'd be trying to run the GDI processing in a restartable way (and dealing with the errors which would occur) or changing the UI.

Depending on the requirements in the detail view, and most importantly, the visibility and size of the images in the right hand side, you could take advantage of the ability of the ItemsControl to virtualise your list (but you probably have to at least define the height and number of the contained images so it can manage the scrollbars properly). I suggest returning an ObservableCollection of images, rather than an IEnumerable.

In fact, having just tested that, this code appears to make the problem go away:

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);
        }
    }
}

The main thing this gives the runtime, as far as I can see, is the number of items (which the enumerable, obviously, does not) meaning that it neither has to enumerate it multiple times, or guess (!). I can run up and down the list with my finger on the cursor key without this blowing 10k handles, even with 1000 MasterItems, so it looks good to me. (My code has no explicit GC either)

鼻尖触碰 2025-01-05 11:17:09

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

If you clone into a simpler bitmap type (and freeze) it won't use up as many gdi handles, but it's slower.
There's cloning via serialization in an answer to How achieve Image.Clone() in WPF?"

~没有更多了~
我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
原文