【问题标题】:RenderTargetBitmap GDI handle leak in Master-Details viewMaster-Details 视图中的 RenderTargetBitmap GDI 句柄泄漏
【发布时间】:2012-02-20 12:07:34
【问题描述】:

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

【问题讨论】:

    标签: c# wpf bitmap gdi resource-leak


    【解决方案1】:

    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 个 MasterItems,我也可以用手指在光标键上上下移动列表,而不会吹出 10k 个句柄,所以它对我来说看起来不错。 (我的代码也没有显式 GC)

    【讨论】:

    • 注意,我也尝试过缓存 ObservableCollection。不幸的是,持有集合似乎最终也持有 GDI 句柄。
    • 谢谢,太好了。它确实为示例应用程序修复了它,我现在只需要尝试将它安装到实际应用程序中。我不确定为什么 ObservableCollection 在这里有帮助。如果只是因为大小,那么 List 应该也有同样的效果。
    【解决方案2】:

    如果你克隆成一个更简单的位图类型(并冻结),它不会用掉那么多的 gdi 句柄,但它会更慢。 在How achieve Image.Clone() in WPF?"的回答中有通过序列化进行克隆

    【讨论】:

    • WriteableBitmap 有一个接受 BitmapSource 的 ctor,因此将其克隆到该 ctor 中会更快,并且还可以解决问题。谢谢。
    【解决方案3】:

    【讨论】:

      猜你喜欢
      • 2013-06-10
      • 2020-12-20
      • 1970-01-01
      • 2017-10-14
      • 2011-10-06
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2019-10-28
      相关资源
      最近更新 更多