RenderTargetBitmap GDI обрабатывает утечку в представлении Master-Details

У меня есть приложение с представлением "Мастер-детали". Когда вы выбираете элемент из списка "master", он заполняет область "детали" некоторыми изображениями (созданными с помощью 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 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>

Вы можете воспроизвести проблему, если вы нажмете на первый элемент в списке, затем нажмите и удерживайте клавишу курсора вниз.

От взгляда на! gcroot в WinDbg с SOS, я не могу найти ничего, поддерживая объекты RenderTargetBitmap, но если я сделаю !dumpheap -type System.Windows.Media.Imaging.RenderTargetBitmap, он покажет несколько тысяч из них, которые еще не собраны.

Ответ 1

TL; DR: исправлено. См. Нижнюю часть. Читайте дальше для моего путешествия открытия и всех неправильных переулков, я спустился!

Я сделал несколько попыток с этим, и я не думаю, что это утечка как таковая. Если я усилю GC, поместив эту сторону цикла в Images:

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 перезапустимым способом (и справиться с ошибками, которые могут произойти) или изменить пользовательский интерфейс.

В зависимости от требований в подробном представлении и, что самое важное, видимости и размера изображений в правой части, вы можете воспользоваться возможностью, чтобы 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)

Ответ 2

Если вы клонируете в более простой тип растрового изображения (и замерзаете), он не будет использовать столько дескрипторов gdi, но медленнее. Там клонирование через сериализацию в ответ на Как достичь Image.Clone() в WPF?"